Используем PowerShell для работы с REST API

от автора

Многие из вас наверняка работают с разнообразными инфраструктурами, используя 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. Поддержка же пайплайна еще более упрощает работу.

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

ссылка на оригинал статьи https://habr.com/ru/company/veeam/blog/490022/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *