Многие из вас наверняка работают с разнообразными инфраструктурами, используя REST API. А поскольку все более широкие слои населения для автоматизации рутинных задач осваивают PowerShell, то почему бы и не начать применять его для работы с REST API?

Сегодня вашему вниманию предлагается перевод статьи Адама Бертрама, большого поклонника автоматизации, автора сайта adamtheautomator.com, в которой он показывает на примере, как это можно сделать.

Итак, добро пожаловать под кат.



В качестве примера API у нас будет выступать swagger petstore API.

Начну, как и положено в приличных руководствах, с требований к системе. Данный пример создавался с Windows PowerShell v5.1 — проверьте, что у вас он есть. Возможно, что пример будет работать также на v6 и выше, возможно, что на Mac или на Linux ОС — но это не точно (не тестировалось).

Готовим папку и файлы для нового модуля


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

  • В файле манифеста PowerShell (.psd1) будет храниться информация о новом модуле.
  • В файле собственно модуля PowerShell (.psm1) будут храниться все наши командлеты.
  • Ну и папка, в которой будут лежать оба эти файла.

Создаем папку PetStore в каталоге Source на диске C:

New-Item -Type Directory -Path C:\Source\PetStore


Затем создадим файл манифеста для нашего модуля — это файл .psd1 с метаданными, где вы сможете прописать все требования к модулю и всю информацию о нём. Вообще-то никто вам не запретит создать такой файл вручную, но можно сделать это и с помощью командлета New-ModuleManifest. Само собой, нужно будет указать необходимые вам параметры.
Создаем файл манифеста под названием Petstore.psd1 в нашей новой папке на диске C:

# Use splatting technique to make it more readable
# Не забывайте про комментарии к коду для пущей читабельности

 $ModuleManifestParams = @{
 	Path        	= "C:\Source\PetStore\PetStore.psd1" 
# Notice that the psd1 file has the same name as the folder it resides in
# Файл манифеста имеет такое же имя, что и папка

 	Guid        	= [GUID]::NewGuid().Guid 
# A unique GUID for the module for identification
# GUID нового модуля

 	Author      	= "Your Name Here" 
# Optional - кто автор (необязательно)

     CompanyName 	= "Company Name here" 
# Optional - компания (необязательно)

 	ModuleVersion   = "0.0.1" 
# Semantic versioning as recommended best practice for PowerShell modules
# Проставим номер версии модуля, как рекомендуется в лучших домах (семантическое версионирование, т.е. по смыслу)

 	Description 	= "A PowerShell module to interact with the HttpBin API" 
# A short description of what the module does
# Кратко опишем, для чего нужен модуль
 }
 
 # Run New-ModuleManifest with the splatted parameters
 # А теперь запустим командлет с указанными параметрами для создания манифеста

 New-ModuleManifest @ModuleManifestParams


Файл собственно модуля с расширением .psm1 как раз и будет содержать все нужные нам функции. В любом PowerShell-редакторе создадим новый файл и положим его на диск C.

Прописываем URI для API-эндпойнтов


Строго говоря, это необязательная операция, но будет хорошим тоном в файле .psm1 прописать основной идентификатор ресурса (base URI) для API-эндпойнтов как переменную — это многое упростит, в том числе и в будущем (переход от версии к версии). Ну а в качестве формата данных будем использовать JSON.

Получится вот так:

$script:PetStoreBaseUri = "https://petstore.swagger.io/v2"
 $script:PetstoreInvokeParams = @{
 	ContentType = "application/json"
 }

Затем мы сможем использовать эту переменную в любой функции с вызовом
Invoke-RestMethod:

PS51> Invoke-RestMethod -Uri "$script:PetStoreBaseUri/pet/<petid>" @script:PetstoreInvokeParams

Создаем первую функцию


Для начала создадим функцию, которая будет получать от эндпойнта https://petstore.swagger.io/v2/pet/ данные о питомцах. Назовем ее Get-PetstorePet:

Function Get-PetstorePet {
 	[cmdletbinding()]
 	param(
     	[Parameter(Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName)]
     	[int64]$PetID
 	)
 	Begin {
     	# Using the module-scoped API endpoint URI to create a function-specific URL to query
	# Используем наш URI, заданный для модуля, чтобы создать для данной функции URL, куда будут идти запросы

     	$Uri = "$script:PetStoreBaseUri/pet/{petId}"
 	}
 	Process {
     	# Create a temporary variable from the Uri variable 
	# Создаем временную переменную из нашей переменной URI

contain int the PetId parameter
     	$TempUri = $Uri -replace "\{petId\}", $PetId
         	
     	# Call the API endpoint using the $TempUri
	# Используем эту временную переменную для обращения к API-эндпойнту

     	Invoke-RestMethod -Uri $TempUri -Method Get @script:PetStoreInvokeParams
 	}
 }


Мы определили параметр PetId так, чтобы он соответствовал определенному параметру REST API для этого эндпойнта.

Примечание: Полный перечень параметров для данного примера эндпойнта можно найти тут.

Используя ValueFromPipeline и ValueFromPipelineByPropertyName в нашей функции, можно передавать ей параметры из пайплайна:

# Define pet id's to fetch 
# Указываем идентификаторы питомцев, которые нам нужны

 $PetIds = @(100,101,102,103)
 $PetIds | Get-PetstorePet

Импортируем модуль


Для этого выполняем команду Import-Module:

PS51> Import-Module C:\Source\Petstore

После чего можно будет запускать выполнение нашей функции Get-PetstorePet:

PS51> Get-PetstorePet -PetId 123

Добавляем новую запись


Как разъясняется в документации для API, чтобы добавить нового питомца, нужно написать тело запроса в формате JSON. Ну и логично, что для добавления нового питомца мы будем использовать функцию Add-PetstorePet.

Поскольку PowerShell умеет в объекты, мы не будем валить всё в кучу, а создадим хэш-таблицу, которую затем преобразуем в JSON:

$BodyJson = @{
 	id    	= $PetId
 	category  = @{
     	id   = $CategoryId
     	name = $CategoryName
 	}
 	name  	= $PetName
 	photoUrls = $PhotoUrls
 	tags  	= $Tags
 	status	= $Status
 } | ConvertTo-Json

Это куда проще и короче, чем создавать всеобъемлющий JSON. На основе нашей хэш-таблички определим параметры, которые понадобятся нам для команды:

param(
         	# Id of the pet that you'll create
		# ID питомца, которого будем создавать

     	[Parameter(Mandatory, ValueFromPipelineByPropertyName)]
           	[Alias("Id")] 
# This is an alias so that we can use it with pipeline properly by piping output from other Functions in this module
# Это алиас, который мы сможем использовать в пайплайне, забирая то, что получается на выходе у всяких разных функций в нашем модуле

     	[int64]$PetId,
 
         	# Category ID of the pet to create
		# ID категории, к которой будет относиться новый питомец
     	[Parameter(Mandatory, ValueFromPipelineByPropertyName)]
     	[int64]$CategoryId,
 
     	[Parameter(Mandatory, ValueFromPipelineByPropertyName)]
     	[string]$CategoryName,
 
         	# Name of the pet
		# Имя питомца

     	[Parameter(Mandatory, ValueFromPipelineByPropertyName)]
     	[string]$PetName,
 
         	# Urls to photos of the pet
		# URLs для фото питомца

     	[Parameter(Mandatory, ValueFromPipelineByPropertyName)]
     	[string[]]$PhotoUrls,
 
         	# Objects with tags containing Id and Name properties
		# Объекты с тэгами, содержащими ID и Name

     	[Parameter(ValueFromPipelineByPropertyName)]
     	[PSCustomObject[]]$Tags,
 
         	# Status of the pet "Available","Taken" etc.
		# Статус питомца (Доступен, Пристроен и пр.)
     	[Parameter(Mandatory, ValueFromPipelineByPropertyName)]
     	[string]$Status
 
 	)

Поскольку блок Begin ровно такой же, как и у предыдущей команды, мы можем его переиспользовать, слегка изменив URI (т.к. параметр PetID тут уже не нужен):

Begin {
 	$Uri = "$script:PetStoreBaseUri/pet"
 }

Затем определим блок Process, который создаст post-запрос, используя формат JSON, и отправит его к REST API-эндпойнту, задействуя Invoke-RestMethod.

Process {
 	# Create the JSON that we'll post to the endpoint
        # Создаем JSON для отправки post-запроса на эндпойнт

 	$BodyObject = @{
     	id    	= $PetId
     	category  = $Category
     	name  	= $PetName
     	photoUrls = $PhotoUrls
     	tags  	= $Tags
     	status	= $Status
 	}
             	
 	# This will remove properties with a null value since it may mess around with some API's
	# Удаляем свойства, у которых значение равно null, чтобы не мешали

 	$BodyObject.psobject.properties | ? {$_.Value -eq $Null} | Foreach {
     	$BodyObject.psobject.properties.Remove($_.Name)
 	}
 
 	$BodyJson = $BodyObject | ConvertTo-Json
 
 	# Call the API endpoint using the $TempUri
	# Обращаемся к эндпойнту, используя $TempUri

 	Invoke-RestMethod -Uri $Script:Uri -Method POST -Body $BodyJson @PetStoreInvokeParams
 }

Заметьте, что мы удалили у $BodyObject свойства со значением null — ибо некоторые REST API не любят, когда им пытаются скормить такие значения.

Наша функция добавления питомца будет выглядеть в итоге так:

Function Add-PetstorePet {
 	[cmdletbinding()]
 	param(
        
         # Id of the pet that you'll create
         # ID питомца, которого будем создавать

     	[Parameter(Mandatory, ValueFromPipelineByPropertyName)]

     	[Alias("Id")] 
        # This is an alias so that we can use it with pipeline properly by piping output from other Functions in this module
       # Это алиас, который мы сможем использовать в пайплайне, забирая то, что получается на выходе у всяких разных функций в нашем модуле

     	[int64]$PetId,

       	# hash containing Id and name of category
        # Имя и ID категории, к которой будет относиться новый питомец, берем из хэш-таблички

     	[Parameter(ValueFromPipelineByPropertyName)]
     	[hashtable]$Category,
 
         # Name of the pet
         # Имя питомца

     	[Parameter(Mandatory, ValueFromPipelineByPropertyName)]
     	[string]$PetName,
 
        # Urls to photos of the pet
        # URLs для фото питомца

     	[Parameter(Mandatory, ValueFromPipelineByPropertyName)]
     	[string[]]$PhotoUrls,
 
         # Objects with tags containing Id and Name properties
         # Объекты с тэгами, содержащими ID и Name

     	[Parameter(ValueFromPipelineByPropertyName)]
     	[PSCustomObject[]]$Tags,
 
         # Status of the pet "Available","Taken" etc.
         # Статус питомца (Доступен, Пристроен и пр.)

     	[Parameter(ValueFromPipelineByPropertyName)]
     	[string]$Status
 
 	)
 	Begin {
     	$Uri = "$script:PetStoreBaseUri/pet"
 	}
 	Process {
     	# Create the JSON that we'll post to the endpoint
        # Создаем JSON для отправки post-запроса на эндпойнт 

     	$BodyObject = @{
         	id    	= $PetId
         	category  = $Category
         	name  	= $PetName
         	photoUrls = $PhotoUrls
         	tags  	= $Tags
         	status	= $Status
     	}
 
     	$BodyObject.psobject.properties | ? {$_.Value -eq $Null} | Foreach {
         	$BodyObject.psobject.properties.Remove($_.Name)
     	}
 
     	$BodyJson = $BodyObject | ConvertTo-Json
 
     	# Call the API endpoint using the $TempUri
        # Обращаемся к эндпойнту, используя $TempUri

       
    	 Invoke-RestMethod -Uri $Uri -Method POST -Body $BodyJson @script:PetStoreInvokeParams
 
 	}
 }

Снова импортируем модуль и запускаем нашу функцию:

PS51> Import-Module C:\Source\Petstore -Force
 PS51> Add-PetstorePet -PetName Fido -PetId 1 -PhotoUrls "http://somesite.com/picture.jpg"
 PS51> Get-PetstorePet -PetId 1

Пишем новые функции


Теперь мы освоились с функциями и можем на основе этого подхода создавать функции для других методов, работающих с REST API.

Например, на базе функции Add-PetstorePet можно создать функцию удаления Remove-PetstorePet — для этого надо дать ей соответствующее название и заменить метод, который вызывается командой Invoke-RestMethod, на метод DELETE, как показано ниже:

Function Remove-PetstorePet {
     [cmdletbinding()]
 	param(
     	[Parameter(Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName)]
     	[Alias("Id")]
     	[int64]$PetId
 	)
 	Begin {
     	$Uri = "$script:PetStoreBaseUri/pet/{petId}"
 	}
 	Process {
    	 # Create a temporary variable from the Uri variable containing the PetId parameter
         # Создаем временную переменную из нашей переменной URI

     	$TempUri = $Uri -replace "\{petId\}", $PetId
     	
     	# Call the API endpoint using the $TempUri
        # Обращаемся к эндпойнту, используя $TempUri
        
     	Invoke-RestMethod -Uri $TempUri -Method DELETE @script:PetStoreInvokeParams 

        # NOTICE how we replaced GET with DELETE
        # Обратите внимание, что мы заменили тут GET на DELETE
 
 	}
 }

Поскольку параметры были настроены на прием pipeline input, вы можете спокойно организовать пайп питомцев в функцию Remove-PetstorePet:

PS51> Get-PetstorePet -PetId 1 | Remove-PetstorePet

В заключение


Как вы наверняка заметили, при создании модуля PowerShell для REST API многое завязано на повторное использование. Поскольку многие REST API весьма схожи с swagger API, то вполне можно переиспользовать модуль для работы с разными API. Поддержка же пайплайна еще более упрощает работу.

Ну а если у вас тоже есть чем поделиться из своей практики автоматизации, добро пожаловать в комментарии.