پاور سی ایل آئی اسکرپٹس کے لیے راکٹ بوسٹر کیسے بنایا جائے۔ 

جلد یا بدیر، کوئی بھی VMware سسٹم ایڈمنسٹریٹر معمول کے کاموں کو خودکار کرنے کے لیے آتا ہے۔ یہ سب کمانڈ لائن سے شروع ہوتا ہے، پھر پاور شیل یا VMware PowerCLI آتا ہے۔

فرض کریں کہ آپ نے ISE لانچ کرنے اور "کسی قسم کے جادو" کی وجہ سے کام کرنے والے ماڈیولز سے معیاری cmdlets استعمال کرنے سے تھوڑا آگے PowerShell میں مہارت حاصل کر لی ہے۔ جب آپ ورچوئل مشینوں کو سینکڑوں میں گننا شروع کریں گے، تو آپ کو معلوم ہوگا کہ چھوٹے پیمانے پر مدد کرنے والی اسکرپٹس بڑے پیمانے پر نمایاں طور پر آہستہ چلتی ہیں۔ 

اس صورت حال میں، 2 ٹولز مدد کریں گے:

  • پاور شیل رن اسپیسز - ایک نقطہ نظر جو آپ کو الگ الگ دھاگوں میں عمل کے عمل کو متوازی کرنے کی اجازت دیتا ہے۔ 
  • گیٹ ویو - ایک بنیادی PowerCLI فنکشن، ونڈوز میں Get-WMIObject کا ایک اینالاگ۔ یہ cmdlet اشیاء کے ساتھ موجود ہستیوں کو نہیں کھینچتا ہے، لیکن سادہ ڈیٹا کی اقسام کے ساتھ ایک سادہ آبجیکٹ کی شکل میں معلومات حاصل کرتا ہے۔ بہت سے معاملات میں یہ تیزی سے نکلتا ہے۔

اگلا، میں ہر ٹول کے بارے میں مختصراً بات کروں گا اور استعمال کی مثالیں دکھاؤں گا۔ آئیے مخصوص اسکرپٹ کا تجزیہ کرتے ہیں اور دیکھتے ہیں کہ کب ایک دوسرے سے بہتر کام کرتا ہے۔ جاؤ!

پاور سی ایل آئی اسکرپٹس کے لیے راکٹ بوسٹر کیسے بنایا جائے۔

پہلا مرحلہ: رن اسپیس

لہذا، Runspace کو مرکزی ماڈیول سے باہر کاموں کی متوازی پروسیسنگ کے لیے ڈیزائن کیا گیا ہے۔ بلاشبہ، آپ ایک اور عمل شروع کر سکتے ہیں جو کچھ میموری، پروسیسر وغیرہ کو کھا جائے گا۔ اگر آپ کا اسکرپٹ چند منٹوں میں چلتا ہے اور ایک گیگا بائٹ میموری استعمال کرتا ہے، تو غالباً آپ کو رن اسپیس کی ضرورت نہیں ہوگی۔ لیکن دسیوں ہزار اشیاء کے اسکرپٹ کے لیے اس کی ضرورت ہے۔

آپ یہاں سیکھنا شروع کر سکتے ہیں: 
پاور شیل رن اسپیسز کا استعمال شروع کرنا: حصہ 1

Runspace کا استعمال کیا دیتا ہے:

  • پھانسی شدہ حکموں کی فہرست کو محدود کرکے رفتار،
  • کاموں کی متوازی عملدرآمد،
  • حفاظت

یہاں انٹرنیٹ سے ایک مثال ہے جب Runspace مدد کرتا ہے:

"اسٹوریج کا تنازعہ vSphere میں ٹریک کرنے کے لئے سب سے مشکل میٹرکس میں سے ایک ہے۔ vCenter کے اندر، آپ صرف جا کر یہ نہیں دیکھ سکتے کہ کون سا VM ذخیرہ کرنے کے زیادہ وسائل استعمال کر رہا ہے۔ خوش قسمتی سے، آپ پاور شیل کی بدولت منٹوں میں یہ ڈیٹا اکٹھا کر سکتے ہیں۔
میں ایک اسکرپٹ کا اشتراک کروں گا جو VMware سسٹم کے منتظمین کو پورے vCenter میں تیزی سے تلاش کرنے اور ان کے اوسط استعمال پر ڈیٹا کے ساتھ VMs کی فہرست حاصل کرنے کی اجازت دے گا۔  
اسکرپٹ پاور شیل رن اسپیس کا استعمال کرتی ہے تاکہ ہر ESXi میزبان کو الگ رن اسپیس میں اس کے اپنے VMs سے کھپت کی معلومات اکٹھی کر سکے اور فوری طور پر تکمیل کی اطلاع دے سکے۔ یہ پاور شیل کو میزبانوں کے ذریعے تکرار کرنے اور ہر ایک کی درخواست کو مکمل کرنے کا انتظار کرنے کے بجائے فوری طور پر ملازمتوں کو بند کرنے کی اجازت دیتا ہے۔

ماخذ: ESXi ڈیش بورڈ پر ورچوئل مشین I/O کیسے دکھائیں۔

مندرجہ ذیل صورت میں، Runspace اب مفید نہیں ہے:

"میں ایک اسکرپٹ لکھنے کی کوشش کر رہا ہوں جو VM سے بہت زیادہ ڈیٹا اکٹھا کرے اور جب ضروری ہو تو نیا ڈیٹا لکھے۔ مسئلہ یہ ہے کہ وہاں بہت سارے VMs ہیں، اور ایک مشین پر 5-8 سیکنڈز صرف ہوتے ہیں۔ 

ماخذ: رن اسپیس پول کے ساتھ ملٹی تھریڈنگ پاور سی ایل آئی

یہاں آپ کو گیٹ ویو کی ضرورت ہوگی، آئیے اس پر چلتے ہیں۔ 

دوسرا مرحلہ: گیٹ ویو

یہ سمجھنے کے لیے کہ Get-View کیوں مفید ہے، یہ یاد رکھنے کے قابل ہے کہ cmdlets عام طور پر کیسے کام کرتے ہیں۔ 

API حوالہ جاتی کتابوں کا مطالعہ کرنے اور اگلے پہیے کو دوبارہ ایجاد کرنے کی ضرورت کے بغیر آسانی سے معلومات حاصل کرنے کے لیے Cmdlets کی ضرورت ہے۔ پرانے دنوں میں جو کوڈ کی ایک سو یا دو لائنیں لگتی تھیں، پاور شیل آپ کو ایک کمانڈ کے ساتھ کرنے کی اجازت دیتا ہے۔ ہم رفتار کے ساتھ اس سہولت کی ادائیگی کرتے ہیں۔ خود cmdlets کے اندر کوئی جادو نہیں ہے: وہی اسکرپٹ، لیکن نچلی سطح پر، دھوپ والے ہندوستان کے ایک ماسٹر کے ہنر مند ہاتھوں سے لکھا گیا ہے۔

اب، Get-View کے ساتھ موازنہ کے لیے، آئیے Get-VM cmdlet لیتے ہیں: یہ ورچوئل مشین تک رسائی حاصل کرتا ہے اور ایک جامع آبجیکٹ کو لوٹاتا ہے، یعنی یہ دیگر متعلقہ اشیاء کو اس سے منسلک کرتا ہے: VMHost، Datastore، وغیرہ۔  

گیٹ ویو اپنی جگہ پر واپسی ہوئی چیز میں کوئی غیر ضروری چیز شامل نہیں کرتا ہے۔ مزید یہ کہ، یہ ہمیں سختی سے یہ بتانے کی اجازت دیتا ہے کہ ہمیں کس معلومات کی ضرورت ہے، جو آؤٹ پٹ آبجیکٹ کو آسان بنا دے گی۔ عام طور پر ونڈوز سرور میں اور خاص طور پر Hyper-V میں، Get-WMIObject cmdlet ایک براہ راست اینالاگ ہے - خیال بالکل ایک جیسا ہے۔

گیٹ ویو پوائنٹ اشیاء پر معمول کی کارروائیوں کے لیے تکلیف دہ ہے۔ لیکن جب بات ہزاروں اور دسیوں ہزار اشیاء کی ہو تو اس کی کوئی قیمت نہیں ہوتی۔

آپ VMware بلاگ پر مزید پڑھ سکتے ہیں: Get-View کا تعارف

اب میں آپ کو ایک حقیقی کیس کا استعمال کرتے ہوئے سب کچھ دکھاؤں گا۔ 

VM اتارنے کے لیے اسکرپٹ لکھنا

ایک دن میرے ساتھی نے مجھ سے اس کی اسکرپٹ کو بہتر بنانے کو کہا۔ یہ کام ایک عام معمول ہے: تمام VMs کو ڈپلیکیٹ cloud.uuid پیرامیٹر کے ساتھ تلاش کریں (جی ہاں، vCloud ڈائریکٹر میں VM کی کلوننگ کرتے وقت یہ ممکن ہے)۔ 

ذہن میں آنے والا واضح حل یہ ہے:

  1. تمام VMs کی فہرست حاصل کریں۔
  2. فہرست کو کسی طرح پارس کریں۔

اصل ورژن یہ سادہ اسکرپٹ تھا:

function Get-CloudUUID1 {
   # Получаем список всех ВМ
   $vms = Get-VM
   $report = @()

   # Обрабатываем каждый объект, получая из него только 2 свойства: Имя ВМ и Cloud UUID.
   # Заносим данные в новый PS-объект с полями VM и UUID
   foreach ($vm in $vms)
   {
       $table = "" | select VM,UUID

       $table.VM = $vm.name
       $table.UUID = ($vm | Get-AdvancedSetting -Name cloud.uuid).Value
          
       $report += $table
   }
# Возвращаем все объекты
   $report
}
# Далее РУКАМИ парсим полученный результат

سب کچھ انتہائی سادہ اور واضح ہے۔ اسے ایک دو منٹ میں کافی کے وقفے کے ساتھ لکھا جا سکتا ہے۔ فلٹریشن پر سکرو اور یہ ہو گیا ہے.

لیکن آئیے وقت کی پیمائش کریں:

پاور سی ایل آئی اسکرپٹس کے لیے راکٹ بوسٹر کیسے بنایا جائے۔

پاور سی ایل آئی اسکرپٹس کے لیے راکٹ بوسٹر کیسے بنایا جائے۔

2 منٹ 47 سیکنڈ تقریباً 10k VMs پر کارروائی کرتے وقت۔ بونس فلٹرز کی عدم موجودگی اور نتائج کو دستی طور پر ترتیب دینے کی ضرورت ہے۔ ظاہر ہے، اسکرپٹ کو اصلاح کی ضرورت ہے۔

جب آپ کو بیک وقت vCenter سے میزبان میٹرکس حاصل کرنے کی ضرورت ہو یا دسیوں ہزار اشیاء پر کارروائی کرنے کی ضرورت ہو تو رن اسپیسز سب سے پہلے بچاؤ کے لیے آتے ہیں۔ آئیے دیکھتے ہیں کہ یہ نقطہ نظر کیا لاتا ہے۔

پہلی رفتار کو آن کریں: پاور شیل رن اسپیسز

اس اسکرپٹ کے لیے سب سے پہلی چیز جو ذہن میں آتی ہے وہ یہ ہے کہ لوپ کو ترتیب وار نہیں بلکہ متوازی تھریڈز میں، تمام ڈیٹا کو ایک آبجیکٹ میں اکٹھا کریں اور اسے فلٹر کریں۔ 

لیکن ایک مسئلہ ہے: PowerCLI ہمیں vCenter میں بہت سے آزاد سیشن کھولنے کی اجازت نہیں دے گا اور ایک مضحکہ خیز غلطی پھینک دے گا:

You have modified the global:DefaultVIServer and global:DefaultVIServers system variables. This is not allowed. Please reset them to $null and reconnect to the vSphere server.

اسے حل کرنے کے لیے، آپ کو پہلے سیشن کی معلومات کو سٹریم کے اندر پاس کرنا ہوگا۔ آئیے یاد رکھیں کہ پاور شیل ایسی اشیاء کے ساتھ کام کرتی ہے جو کسی فنکشن یا اسکرپٹ بلاک کو پیرامیٹر کے طور پر منتقل کی جا سکتی ہیں۔ آئیے $global:DefaultVIServers (-NotDefault کلید کے ساتھ Connect-VIServer) کو نظرانداز کرتے ہوئے سیشن کو ایسی آبجیکٹ کی شکل میں پاس کریں:

$ConnectionString = @()
foreach ($vCenter in $vCenters)
   {
       try {
           $ConnectionString += Connect-VIServer -Server $vCenter -Credential $Credential -NotDefault -AllLinked -Force -ErrorAction stop -WarningAction SilentlyContinue -ErrorVariable er
       }
       catch {
           if ($er.Message -like "*not part of a linked mode*")
           {
               try {
                   $ConnectionString += Connect-VIServer -Server $vCenter -Credential $Credential -NotDefault -Force -ErrorAction stop -WarningAction SilentlyContinue -ErrorVariable er
               }
               catch {
                   throw $_
               }
              
           }
           else {
               throw $_
           }
       }
   }

اب رن اسپیس پولز کے ذریعے ملٹی تھریڈنگ کو نافذ کریں۔  

الگورتھم مندرجہ ذیل ہے:

  1. ہمیں تمام VMs کی فہرست ملتی ہے۔
  2. متوازی سلسلے میں ہمیں cloud.uuid ملتا ہے۔
  3. ہم ایک آبجیکٹ میں اسٹریمز سے ڈیٹا اکٹھا کرتے ہیں۔
  4. ہم آبجیکٹ کو کلاؤڈ یو یو آئی ڈی فیلڈ کی قدر کے مطابق گروپ کرکے فلٹر کرتے ہیں: وہ جہاں منفرد اقدار کی تعداد 1 سے زیادہ ہے وہ VMs ہیں جن کی ہم تلاش کر رہے ہیں۔

نتیجے کے طور پر، ہمیں اسکرپٹ ملتا ہے:


function Get-VMCloudUUID {
   param (
       [string[]]
       [ValidateNotNullOrEmpty()]
       $vCenters = @(),
       [int]$MaxThreads,
       [System.Management.Automation.PSCredential]
       [System.Management.Automation.Credential()]
       $Credential
   )

   $ConnectionString = @()

   # Создаем объект с сессионным ключом
   foreach ($vCenter in $vCenters)
   {
       try {
           $ConnectionString += Connect-VIServer -Server $vCenter -Credential $Credential -NotDefault -AllLinked -Force -ErrorAction stop -WarningAction SilentlyContinue -ErrorVariable er
       }
       catch {
           if ($er.Message -like "*not part of a linked mode*")
           {
               try {
                   $ConnectionString += Connect-VIServer -Server $vCenter -Credential $Credential -NotDefault -Force -ErrorAction stop -WarningAction SilentlyContinue -ErrorVariable er
               }
               catch {
                   throw $_
               }
              
           }
           else {
               throw $_
           }
       }
   }

   # Получаем список всех ВМ
   $Global:AllVMs = Get-VM -Server $ConnectionString

   # Поехали!
   $ISS = [system.management.automation.runspaces.initialsessionstate]::CreateDefault()
   $RunspacePool = [runspacefactory]::CreateRunspacePool(1, $MaxThreads, $ISS, $Host)
   $RunspacePool.ApartmentState = "MTA"
   $RunspacePool.Open()
   $Jobs = @()

# ScriptBlock с магией!)))
# Именно он будет выполняться в потоке
   $scriptblock = {
       Param (
       $ConnectionString,
       $VM
       )

       $Data = $VM | Get-AdvancedSetting -Name Cloud.uuid -Server $ConnectionString | Select-Object @{N="VMName";E={$_.Entity.Name}},@{N="CloudUUID";E={$_.Value}},@{N="PowerState";E={$_.Entity.PowerState}}

       return $Data
   }
# Генерируем потоки

   foreach($VM in $AllVMs)
   {
       $PowershellThread = [PowerShell]::Create()
# Добавляем скрипт
       $null = $PowershellThread.AddScript($scriptblock)
# И объекты, которые передадим в качестве параметров скрипту
       $null = $PowershellThread.AddArgument($ConnectionString)
       $null = $PowershellThread.AddArgument($VM)
       $PowershellThread.RunspacePool = $RunspacePool
       $Handle = $PowershellThread.BeginInvoke()
       $Job = "" | Select-Object Handle, Thread, object
       $Job.Handle = $Handle
       $Job.Thread = $PowershellThread
       $Job.Object = $VM.ToString()
       $Jobs += $Job
   }

# Ставим градусник, чтобы наглядно отслеживать выполнение заданий
# И здесь же прибиваем отработавшие задания
   While (@($Jobs | Where-Object {$_.Handle -ne $Null}).count -gt 0)
   {
       $Remaining = "$($($Jobs | Where-Object {$_.Handle.IsCompleted -eq $False}).object)"

       If ($Remaining.Length -gt 60) {
           $Remaining = $Remaining.Substring(0,60) + "..."
       }

       Write-Progress -Activity "Waiting for Jobs - $($MaxThreads - $($RunspacePool.GetAvailableRunspaces())) of $MaxThreads threads running" -PercentComplete (($Jobs.count - $($($Jobs | Where-Object {$_.Handle.IsCompleted -eq $False}).count)) / $Jobs.Count * 100) -Status "$(@($($Jobs | Where-Object {$_.Handle.IsCompleted -eq $False})).count) remaining - $remaining"

       ForEach ($Job in $($Jobs | Where-Object {$_.Handle.IsCompleted -eq $True})){
           $Job.Thread.EndInvoke($Job.Handle)     
           $Job.Thread.Dispose()
           $Job.Thread = $Null
           $Job.Handle = $Null
       }
   }

   $RunspacePool.Close() | Out-Null
   $RunspacePool.Dispose() | Out-Null
}


function Get-CloudUUID2
{
   [CmdletBinding()]
   param(
   [string[]]
   [ValidateNotNullOrEmpty()]
   $vCenters = @(),
   [int]$MaxThreads = 50,
   [System.Management.Automation.PSCredential]
   [System.Management.Automation.Credential()]
   $Credential)

   if(!$Credential)
   {
       $Credential = Get-Credential -Message "Please enter vCenter credentials."
   }

   # Вызов функции Get-VMCloudUUID, где мы распараллеливаем операцию
   $AllCloudVMs = Get-VMCloudUUID -vCenters $vCenters -MaxThreads $MaxThreads -Credential $Credential
   $Result = $AllCloudVMs | Sort-Object Value | Group-Object -Property CloudUUID | Where-Object -FilterScript {$_.Count -gt 1} | Select-Object -ExpandProperty Group
   $Result
}

اس اسکرپٹ کی خوبصورتی یہ ہے کہ اسے صرف اسکرپٹ بلاک اور پیرامیٹرز کو تبدیل کرکے اسی طرح کے دیگر معاملات میں استعمال کیا جاسکتا ہے جو اسٹریم میں منتقل کیے جائیں گے۔ اس کا استحصال کریں!

ہم وقت کی پیمائش کرتے ہیں:

پاور سی ایل آئی اسکرپٹس کے لیے راکٹ بوسٹر کیسے بنایا جائے۔

55 سیکنڈ۔ یہ بہتر ہے، لیکن یہ اب بھی تیز تر ہو سکتا ہے۔ 

آئیے دوسری رفتار پر چلتے ہیں: GetView

آئیے معلوم کریں کہ کیا غلط ہے۔
سب سے پہلے اور اہم بات یہ کہ Get-VM cmdlet کو عمل میں لانے میں کافی وقت لگتا ہے۔
دوسرا، Get-AdvancedOptions cmdlet کو مکمل ہونے میں اور بھی زیادہ وقت لگتا ہے۔
آئیے پہلے دوسرے سے نمٹتے ہیں۔ 

Get-AdvancedOptions انفرادی VM اشیاء کے لیے آسان ہے، لیکن بہت سی اشیاء کے ساتھ کام کرتے وقت بہت اناڑی ہے۔ ہم وہی معلومات ورچوئل مشین آبجیکٹ (Get-VM) سے حاصل کر سکتے ہیں۔ یہ صرف ExtensionData آبجیکٹ میں اچھی طرح سے دفن ہے۔ فلٹرنگ سے لیس، ہم ضروری ڈیٹا حاصل کرنے کے عمل کو تیز کرتے ہیں۔

ہاتھ کی ہلکی سی حرکت کے ساتھ یہ ہے:


VM | Get-AdvancedSetting -Name Cloud.uuid -Server $ConnectionString | Select-Object @{N="VMName";E={$_.Entity.Name}},@{N="CloudUUID";E={$_.Value}},@{N="PowerState";E={$_.Entity.PowerState}}

اس میں بدل جاتا ہے:


$VM | Where-Object {($_.ExtensionData.Config.ExtraConfig | Where-Object {$_.key -eq "cloud.uuid"}).Value -ne $null} | Select-Object @{N="VMName";E={$_.Name}},@{N="CloudUUID";E={($_.ExtensionData.Config.ExtraConfig | Where-Object {$_.key -eq "cloud.uuid"}).Value}},@{N="PowerState";E={$_.summary.runtime.powerstate}}

آؤٹ پٹ Get-AdvancedOptions جیسا ہی ہے، لیکن یہ کئی گنا تیزی سے کام کرتا ہے۔ 

اب Get-VM پر۔ یہ تیز نہیں ہے کیونکہ یہ پیچیدہ اشیاء سے نمٹتا ہے۔ ایک منطقی سوال پیدا ہوتا ہے: ہمیں اس معاملے میں اضافی معلومات اور ایک خوفناک PSO آبجیکٹ کی ضرورت کیوں ہے، جب ہمیں صرف VM کے نام، اس کی حالت اور ایک مشکل وصف کی قدر کی ضرورت ہے؟  

اس کے علاوہ Get-AdvancedOptions کی شکل میں حائل رکاوٹ کو اسکرپٹ سے ہٹا دیا گیا ہے۔ رن اسپیس پولز کا استعمال اب حد سے زیادہ حد تک لگ رہا ہے کیونکہ سیشن کے حوالے کرتے وقت اسکواٹ تھریڈز میں سست کام کو متوازی کرنے کی ضرورت نہیں ہے۔ ٹول اچھا ہے، لیکن اس کیس کے لیے نہیں۔ 

آئیے ایکسٹینشن ڈیٹا کے آؤٹ پٹ کو دیکھیں: یہ گیٹ ویو آبجیکٹ سے زیادہ کچھ نہیں ہے۔ 

آئیے پاور شیل ماسٹرز کی قدیم تکنیک پر کال کریں: فلٹرز، چھانٹنا اور گروپ بندی کا استعمال کرتے ہوئے ایک لائن۔ تمام پچھلی ہولناکی خوبصورتی سے ایک لائن میں سمٹ کر ایک سیشن میں عمل میں لائی گئی ہے۔


$AllVMs = Get-View -viewtype VirtualMachine -Property Name,Config.ExtraConfig,summary.runtime.powerstate | Where-Object {($_.Config.ExtraConfig | Where-Object {$_.key -eq "cloud.uuid"}).Value -ne $null} | Select-Object @{N="VMName";E={$_.Name}},@{N="CloudUUID";E={($_.Config.ExtraConfig | Where-Object {$_.key -eq "cloud.uuid"}).Value}},@{N="PowerState";E={$_.summary.runtime.powerstate}} | Sort-Object CloudUUID | Group-Object -Property CloudUUID | Where-Object -FilterScript {$_.Count -gt 1} | Select-Object -ExpandProperty Group

ہم وقت کی پیمائش کرتے ہیں:

پاور سی ایل آئی اسکرپٹس کے لیے راکٹ بوسٹر کیسے بنایا جائے۔

9 سیکنڈ مطلوبہ حالت کے مطابق فلٹرنگ کے ساتھ تقریباً 10k اشیاء کے لیے۔ زبردست!

اس کے بجائے کسی نتیجے کے

ایک قابل قبول نتیجہ براہ راست ٹول کے انتخاب پر منحصر ہے۔ یقینی طور پر یہ کہنا اکثر مشکل ہوتا ہے کہ اسے حاصل کرنے کے لیے بالکل کیا انتخاب کیا جانا چاہیے۔ اسکرپٹ کو تیز کرنے کے لیے درج کردہ طریقوں میں سے ہر ایک اس کے قابل اطلاق کی حدود میں اچھا ہے۔ مجھے امید ہے کہ یہ مضمون آپ کے بنیادی ڈھانچے میں پروسیس آٹومیشن اور آپٹیمائزیشن کی بنیادی باتوں کو سمجھنے کے مشکل کام میں آپ کی مدد کرے گا۔

PS: مصنف مضمون کی تیاری میں مدد اور تعاون کے لیے کمیونٹی کے تمام اراکین کا شکریہ ادا کرتا ہے۔ یہاں تک کہ جن کے پنجے ہیں۔ اور یہاں تک کہ جن کی ٹانگیں نہیں ہیں، جیسے بوا کنسٹریکٹر۔

ماخذ: www.habr.com

نیا تبصرہ شامل کریں