API to create a pool

I’m attempting to use the API to create a RAIDZ1 pool of 4 disks.

Truenas Scale: Dragonfish-24.04-RC.1

{
  "name": "testzpool1",
  "encryption": false,
  "deduplication": null,
  "checksum": null,
  },
  "topology: {
      "data": [
       {
            "disks": ["sdb", "sdc", "sdd", "sde"],
            "type": "RAIDZ1"
       }
  },
  "allow_duplicate_serials": false
}

The job result has an error
ValidationErrors: [EINVAL] pool_create: A dict was expected

id            : 2470
method        : pool.create
arguments     : {
                {
                  "name": "testzpool1",
                  "encryption": false,
                  "deduplication": null,
                  "checksum": null,
                  },
                  "topology: {
                      "data": [
                       {
                            "disks": ["sdb", "sdc", "sdd", "sde"],
                            "type": "RAIDZ1"
                       }
                  },
                  "allow_duplicate_serials": false
                }}
transient     : False
description   : 
abortable     : False
logs_path     : 
logs_excerpt  : 
progress      : @{percent=0; description=; extra=}
result        : 
error         : [EINVAL] pool_create: A dict was expected
                
exception     : Traceback (most recent call last):
                  File "/usr/lib/python3/dist-packages/middlewared/job.py", line 469, in run
                    await self.future
                  File "/usr/lib/python3/dist-packages/middlewared/job.py", line 511, in __run_body
                    rv = await self.method(*args)
                         ^^^^^^^^^^^^^^^^^^^^^^^^
                  File "/usr/lib/python3/dist-packages/middlewared/service/crud_service.py", line 210, in nf
                    rv = await func(*args, **kwargs)
                         ^^^^^^^^^^^^^^^^^^^^^^^^^^^
                  File "/usr/lib/python3/dist-packages/middlewared/schema/processor.py", line 47, in nf
                    res = await f(*args, **kwargs)
                          ^^^^^^^^^^^^^^^^^^^^^^^^
                  File "/usr/lib/python3/dist-packages/middlewared/schema/processor.py", line 186, in nf
                    args, kwargs = clean_and_validate_args(args, kwargs)
                                   ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
                  File "/usr/lib/python3/dist-packages/middlewared/schema/processor.py", line 180, in clean_and_validate_args
                    verrors.check()
                  File "/usr/lib/python3/dist-packages/middlewared/service_exception.py", line 70, in check
                    raise self
                middlewared.service_exception.ValidationErrors: [EINVAL] pool_create: A dict was expected
                
                
exc_info      : @{repr=ValidationErrors(); type=VALIDATION; extra=System.Object[]}
state         : FAILED
time_started  : @{$date=1712608164000}
time_finished : @{$date=1712608164000}
credentials   : @{type=API_KEY; data=}

any ideas what I have wrong?

Here’s the powershell script

function Invoke-TrueNASAPI {
  param (
    [string] $method,
    [string] $url,
    [object] $body = $null
  )



  $apiKey = "{yourapikey}"
  $apiUrl = "https://{yourtruenasip}/api/v2.0"




  $headers = New-Object System.Net.WebHeaderCollection
  $headers = @{
    accept="*/*"
    Authorization="Bearer $apiKey"
}

 
add-type @"
   using System.Net;
   using System.Security.Cryptography.X509Certificates;
   public class TrustAllCertsPolicy : ICertificatePolicy {
      public bool CheckValidationResult(
      ServicePoint srvPoint, X509Certificate certificate,
      WebRequest request, int certificateProblem) {
      return true;
   }
}
"@
  [System.Net.ServicePointManager]::CertificatePolicy = New-Object TrustAllCertsPolicy
  [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12

  $contentType = "application/json"
  
  if ($body) {
    $body = ConvertTo-Json $body
  }

  Invoke-RestMethod -Uri "$apiUrl/$url" -Method $method -Headers $headers -UseBasicParsing -ContentType $contentType -Body $body
}


Invoke-TrueNASAPI -method Get -url "system/info"


# Get information on all disks
Invoke-TrueNASAPI -method Get -url "disk"

# Get information on all pools
Invoke-TrueNASAPI -method Get -url "pool"







###################################################################################################
####show unused disks
<#
$disks = Invoke-TrueNASAPI -method Get -url "disk"

$poolDisks = @()

foreach ($disk in $disks) {
  $diskId = $disk.identifier  # Store disk identifier
  $poolUrl = "pool?filter=disks+like+'$diskId'"  # Construct API endpoint URL
  $poolsWithDisk = Invoke-TrueNASAPI -method Get -url $poolUrl

  if ($poolsWithDisk) {
    $poolDisks += $disk
  }
}

$unusedDisks = $disks | Where-Object { -not $poolDisks.Contains($_) }

Write-Host "Unused Disks:"
Write-Host $unusedDisks 
#>
###################################################################################################
####create z1 pool of unused disks
#helpful ref: https://www.truenas.com/community/threads/creating-a-pool-with-the-api-v2-0.106495/

# Create ZFS pool (RAID-Z1)
$createPoolResponseID = Invoke-TrueNASAPI -Method Post -url "pool" -Body @('
{
  "name": "testzpool1",
  "encryption": false,
  "deduplication": null,
  "checksum": null,
  },
  "topology: {
      "data": [
       {
            "disks": ["sdb", "sdc", "sdd", "sde"],
            "type": "RAIDZ1"
       }
  },
  "allow_duplicate_serials": false
}') 


$joburl = "core/get_jobs/?id=" + $createPoolResponseID 
Invoke-TrueNASAPI -method Get -url $jobUrl

here are the disks

identifier     : {serial_lunid}6000c290b4c69e24f1189f61362e1fbf_6000c290b4c69e24
name           : sda
subsystem      : scsi
number         : 2048
serial         : 6000c290b4c69e24f1189f61362e1fbf
lunid          : 6000c290b4c69e24
size           : 42949672960
description    : 
transfermode   : Auto
hddstandby     : ALWAYS ON
advpowermgmt   : DISABLED
togglesmart    : True
smartoptions   : 
expiretime     : 
critical       : 
difference     : 
informational  : 
model          : Virtual_disk
rotationrate   : 
type           : SSD
zfs_guid       : 
bus            : SCSI
devname        : sda
enclosure      : 
supports_smart : 
pool           : 

identifier     : {serial_lunid}6000c294a3f7274f18f241e7f33a4c1b_6000c294a3f7274f
name           : sdb
subsystem      : scsi
number         : 2064
serial         : 6000c294a3f7274f18f241e7f33a4c1b
lunid          : 6000c294a3f7274f
size           : 107374182400
description    : 
transfermode   : Auto
hddstandby     : ALWAYS ON
advpowermgmt   : DISABLED
togglesmart    : True
smartoptions   : 
expiretime     : 
critical       : 
difference     : 
informational  : 
model          : Virtual_disk
rotationrate   : 
type           : SSD
zfs_guid       : 7678729438661677905
bus            : SCSI
devname        : sdb
enclosure      : 
supports_smart : 
pool           : 

identifier     : {serial_lunid}6000c29b9222c11399de609b7495b36c_6000c29b9222c113
name           : sde
subsystem      : scsi
number         : 2112
serial         : 6000c29b9222c11399de609b7495b36c
lunid          : 6000c29b9222c113
size           : 107374182400
description    : 
transfermode   : Auto
hddstandby     : ALWAYS ON
advpowermgmt   : DISABLED
togglesmart    : True
smartoptions   : 
expiretime     : 
critical       : 
difference     : 
informational  : 
model          : Virtual_disk
rotationrate   : 
type           : SSD
zfs_guid       : 15510185469459324292
bus            : SCSI
devname        : sde
enclosure      : 
supports_smart : 
pool           : 

identifier     : {serial_lunid}6000c297d67d56672a1888de59e8fcf7_6000c297d67d5667
name           : sdc
subsystem      : scsi
number         : 2080
serial         : 6000c297d67d56672a1888de59e8fcf7
lunid          : 6000c297d67d5667
size           : 107374182400
description    : 
transfermode   : Auto
hddstandby     : ALWAYS ON
advpowermgmt   : DISABLED
togglesmart    : True
smartoptions   : 
expiretime     : 
critical       : 
difference     : 
informational  : 
model          : Virtual_disk
rotationrate   : 
type           : SSD
zfs_guid       : 7451886028752644964
bus            : SCSI
devname        : sdc
enclosure      : 
supports_smart : 
pool           : 

identifier     : {serial_lunid}6000c29b2c17271f31418694d3ef8d45_6000c29b2c17271f
name           : sdd
subsystem      : scsi
number         : 2096
serial         : 6000c29b2c17271f31418694d3ef8d45
lunid          : 6000c29b2c17271f
size           : 107374182400
description    : 
transfermode   : Auto
hddstandby     : ALWAYS ON
advpowermgmt   : DISABLED
togglesmart    : True
smartoptions   : 
expiretime     : 
critical       : 
difference     : 
informational  : 
model          : Virtual_disk
rotationrate   : 
type           : SSD
zfs_guid       : 9432486293175599696
bus            : SCSI
devname        : sdd
enclosure      : 
supports_smart : 
pool           : 

Without looking too closely at the endpoint itself, you very clearly have a typo in the payload that you’re sending. You’re missing a closing square bracket after the data: [

Also seems to be an extra }, after "checksum": null,.
And "topology missing ".

I’m curious… Why do you want to create a pool through the API? That’s not the sort of thing most people would automate.

Here’s the updated JSON portion
validation screengrab below

same error message
error : [EINVAL] pool_create: A dict was expected

$createPoolResponseID = Invoke-TrueNASAPI -Method Post -url "pool" -Body @('
	"name": "testzpool1",
	"encryption": false,
	"deduplication": null,
	"checksum": null,
	"topology": {
		"data": [
			{
				"disks": [
					"sdb",
					"sdc",
					"sdd",
					"sde"
				],
				"type": "RAIDZ1"
			}
		]
	},
	"allow_duplicate_serials": false
')

We are considering purchasing an M50HA.
I figured I’d checkout the API and support and community.
To test any Truenas storage functionality, one of the first things needed is a pool. Seemed like a good place to start testing the API.

It would be great if there was an IX Systems Terraform module. There is not.
Here’s a user provided Terraform module

It would be great if there was an IX Systems Powershell module. There is not.

Being GUI centrix seems so Microsoft’ish :wink:

Sure, but you’d just use the GUI for that, since it’s a one-time thing that requires a bunch of cross-checks if your setup is not trivial. You can then use the APIs for whatever regular tasks you might have, like creating datasets.

I understand your position. We take a different approach.

Hi,
I believe you were almost there with the above. The POST data is still missing a leading and training { } … hence the API complaining about a missing dict.

I used curl to replicate your approach:

brian@debian11:~$ curl -u $HOSTUSER:$HOSTPASS -X POST --data '@data.txt' http://$HOSTIP/api/v2.0/pool
65
brian@debian11:~$ cat data.txt
{
        "name": "testzpool1",
        "encryption": false,
        "deduplication": null,
        "checksum": null,
        "topology": {
                "data": [
                        {
                                "disks": [
                                        "sdb",
                                        "sdc",
                                        "sdd",
                                        "sde"
                                ],
                                "type": "RAIDZ1"
                        }
                ]
        },
        "allow_duplicate_serials": false
}
brian@debian11:~$ curl -s -u $HOSTUSER:$HOSTPASS -X GET http://$HOSTIP/api/v2.0/pool | head -20
[
 {
  "id": 1,
  "name": "testzpool1",
  "guid": "14558409606271626371",
  "path": "/mnt/testzpool1",
  "status": "ONLINE",
  "scan": {
   "function": null,
   "state": null,
   "start_time": null,
   "end_time": null,
   "percentage": null,
   "bytes_to_process": null,
   "bytes_processed": null,
   "bytes_issued": null,
   "pause": null,
   "errors": null,
   "total_secs_left": null
  },
...

The 65 returned is a job id, as the operation may take a while to complete.

Hope this helps!
-Brian.

Thanks!
The curl verification helped.

After all the typos/syntax corrections I was getting the same error.
This is due to converting json to json line #42 in the script.
Commented that part out and the script completes without error.

Here’s the final working script if anyone is interested.
Thanks for everyone’s input.

function Invoke-TrueNASAPI {
  param (
    [string] $method,
    [string] $url,
    [object] $body = $null
  )



  $apiKey = "{yourapikey}"
  $apiUrl = "https://{yourtruenasip}/api/v2.0"




  $headers = New-Object System.Net.WebHeaderCollection
  $headers = @{
    accept="*/*"
    Authorization="Bearer $apiKey"
}

 
add-type @"
   using System.Net;
   using System.Security.Cryptography.X509Certificates;
   public class TrustAllCertsPolicy : ICertificatePolicy {
      public bool CheckValidationResult(
      ServicePoint srvPoint, X509Certificate certificate,
      WebRequest request, int certificateProblem) {
      return true;
   }
}
"@
  [System.Net.ServicePointManager]::CertificatePolicy = New-Object TrustAllCertsPolicy
  [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12

  $contentType = "application/json"
  
  
  ###commented out this and the dataset is created
  #if ($body) {
  #  $body = ConvertTo-Json $body
  #}

  Invoke-RestMethod -Uri "$apiUrl/$url" -Method $method -Headers $headers -UseBasicParsing -ContentType $contentType -Body $body
}


Invoke-TrueNASAPI -method Get -url "system/info"


# Get information on all disks
Invoke-TrueNASAPI -method Get -url "disk"

# Get information on all pools
Invoke-TrueNASAPI -method Get -url "pool"







###################################################################################################
####show unused disks

$disks = Invoke-TrueNASAPI -method Get -url "disk"

$poolDisks = @()

foreach ($disk in $disks) {
  $diskId = $disk.identifier  # Store disk identifier
  $poolUrl = "pool?filter=disks+like+'$diskId'"  # Construct API endpoint URL
  $poolsWithDisk = Invoke-TrueNASAPI -method Get -url $poolUrl

  if ($poolsWithDisk) {
    $poolDisks += $disk
  }
}

$unusedDisks = $disks | Where-Object { -not $poolDisks.Contains($_) }

Write-Host "Unused Disks:"
Write-Host $unusedDisks 
###################################################################################################
####create z1 pool of unused disks
# Identify unused disks
#helpful ref: https://www.truenas.com/community/threads/creating-a-pool-with-the-api-v2-0.106495/

$disks = Invoke-TrueNASAPI -method Get -url "disk"
$unusedDisks = @()
foreach ($disk in $disks) {
  $diskId = $disk.identifier  # Store disk identifier
  $poolUrl = "pool?filter=disks+like+'$diskId'"  # Construct API endpoint URL
  $poolsWithDisk = Invoke-TrueNASAPI -method Get -url $poolUrl
  if ($poolsWithDisk) {
    continue  # Skip disk if found in a pool
  }
  $unusedDisks += $disk  # Add unused disk to the array
}

# Check for unused disks
if ($unusedDisks.Count -eq 0) {
  Write-Host "No unused disks found!"
  exit
}else{
  Write-Host "[$($unusedDisks.Count)] unused disks found!"
}

# Select at least two disks for RAID-Z1
$LastDisk = $unusedDisks.Count - 1
$selectedDisks = $unusedDisks[1..$LastDisk]  # work needed here to determine correct disks ***

$DiskString = "workongthis later"

# Create ZFS pool (RAID-Z1)
$createPoolResponseID = Invoke-TrueNASAPI -Method Post -url "pool" -Body @('
{
	"name": "testzpool1",
	"encryption": false,
	"deduplication": null,
	"checksum": null,
	"topology": {
		"data": [
			{
				"disks": [
					"sdb",
					"sdc",
					"sdd",
					"sde"
				],
				"type": "RAIDZ1"
			}
		]
	},
	"allow_duplicate_serials": false
}') 




$joburl = "core/get_jobs/?id=" + $createPoolResponseID 
Invoke-TrueNASAPI -method Get -url $jobUrl