Skip to content

Variables audit: every action, threshold and literal in ZTP ​

Owner's requirement (2026-11-07): with ZTP every action taken is backed by a variable, the environment file copes with servers that have several ports on different adapter types, and everything in the ZTP environment file is a key of the master infrastructure.yml schema in azurelocal-toolkit. One file for every tool; no key that only ZTP knows.

This page walks every stage and script in scripts/simplified-provisioning/ (Resolve, Media, Boot, Bootstrap, Vouchers, Device, Azure, Ready, Network, Restore, Handoff, the orchestrator Invoke-ZtpProvisioning.ps1, Invoke-ZtpEnvironment.ps1, ZtpEnvironment.Common.ps1 and the Bicep) and lists what changes behaviour. Paths are schema 5.4 paths of the master file; the environment-file key is what the schema 5 reader (ConvertFrom-ZtpInfrastructureV5) hands to the scripts, which is also the key of the older ZTP layout. Placeholder names and documentation address ranges only.

Status words. controlled: a variable already backed it. controlled (new): it was fixed in code or in the script wrapper, and 5.4 makes it a key with today's value as the default. HARD-CODED (fixed): it assumed a vendor, and this change removes the assumption. HARD-CODED (kept): it stays in code, for the reason given.

Summary ​

101 items. Before this change 29 were controlled and 72 were not (a literal in code, or in the wrapper that writes the settings). Now 64 are backed by a variable (the 29, plus 35 that became keys with today's value as the default), 2 vendor assumptions were fixed, and 35 stay hard-coded with a reason each:

GroupItems kept hard-codedWhy
Microsoft's formats and APIsM3, M4, B18, D10, A15, A23, A24, A27, H1They are the service's contract (GPT and FAT32 with label CDROM, the USBStick media type, the Configurator REST operations, built-in role IDs, API versions, what Ready means, the handoff format). A value can't change them; a code change would.
Security designB16, B17A generated share password and TCP 445 only from the BMCs, read-write because the voucher is written through it.
Safety and correctnessB5, B6, B21, B22, V3, A26, A29, N2The BIOS read before any change, the restart type (the machine is reinstalled next), idempotence, one retry, the unreliable ApplyConfiguration path kept off.
Layout and evidenceR6, M7, M8, H2, H3Sub-folder and file names under the controlled media_path and evidence_path; Sonar follows the log names.
Internal polling and tuningR4, R5, M5, B8, Bt4, Bt6, V2, D9Probe windows, poll intervals, cluster size: no environment has needed another value.
Dell protocol, auto-detectedB19The iDRAC 7.30 boot-target workaround is chosen from the BMC's AllowableValues, not by the operator. A gap on an HPE or Lenovo BMC without Usb.
Unsupported by the service pathA11, V5Connectivity Public (no proxy details in the site setting), and the Configurator App voucher fallback.

Resolve (Get-ZtpBootstrapIso.ps1) ​

#What it doesSchema 5 pathEnvironment-file keyDefaultStatus
R1Which maintenance ISO: latest, a train (2610), a build, or a Microsoft download URLprovisioning.simplified.maintenance_isoztp.provisioning_isonone (required)controlled
R2Consider preview trains (a month above 12)provisioning.simplified.include_previewztp.include_previewfalsecontrolled (new)
R3Highest train to consider with latestprovisioning.simplified.max_trainztp.max_trainnonecontrolled (new)
R4How many calendar months of https://aka.ms/aep/installeros/<YYMM> it probes (24)nonenone24HARD-CODED (kept): Microsoft's publication pattern; a pin (R1) covers anything older
R5The publication URL pattern and the Microsoft download hosts it acceptsnonenoneaka.ms/aep/installerosHARD-CODED (kept): Microsoft's location, not an environment choice; R1 takes any Microsoft URL
R6Download cache layout (<media>\bootstrap\<version>\provision-os.iso), SHA-256 trust on first use, 256 MB headroom checkprovisioning.simplified.media_path roots itztp.media_path%ProgramData%\AzureLocalZtp\<site_name>\mediaHARD-CODED (kept): the layout is the integrity design; the root is controlled

Media (New-ZtpVirtualUsbImage.ps1) ​

#What it doesSchema 5 pathEnvironment-file keyDefaultStatus
M1Image file name <prefix><node>.imgprovisioning.simplified.usb_image.name_prefixztp.usb_image.name_prefixztp-usb-controlled (new)
M2Image size (4 GiB booted on Dell 16G; the ISO needs about 1 GiB)provisioning.simplified.usb_image.size_gbztp.usb_image.size_gb4controlled (new)
M3GPT with one FAT32 partition (what Microsoft's usb_prep.exe writes)nonenonefixedHARD-CODED (kept): Microsoft's format
M4Volume label CDROMnonenoneCDROMHARD-CODED (kept): the maintenance boot searches for the label
M5FAT32 cluster size 4096nonenone4096HARD-CODED (kept): script parameter, no environment needs another
M6The edgeuser password hash written to prov_config\download\hash_password.txt (only when a password is set)provisioning.simplified.maintenance_passwordztp.maintenance_password_secretnone: the image keeps Microsoft's out-of-box passwordcontrolled
M7Rebuild this tool's own images when a run starts from the beginning (never after Boot)nonenoneonHARD-CODED (kept): a fresh provisioning needs a fresh image; the vouchers live in the image after Boot
M8Sub-folders ztp-virtual-usb, vouchers, ztp-state of the media folderprovisioning.simplified.media_path roots themztp.media_pathas aboveHARD-CODED (kept): layout under a controlled root

Boot (Set-ZtpBios.ps1, Publish-MaintenanceIso.ps1, Mount-ZtpVirtualUsb.ps1, Start-MaintenanceBoot.ps1) ​

#What it doesSchema 5 pathEnvironment-file keyDefaultStatus
B1BIOS attributes set before the maintenance boot (read first, change only what differs)hardware.bios, nodes[].bios, hardware.bios_during_provisioning, nodes[].bios_during_provisioningthe same namesnonecontrolled
B2BIOS: turn the SAN adapter slots off for the install; Restore puts back the value Boot found (the vendor's enabled value if it was already disabled)provisioning.simplified.isolate_san_adapters with nodes[].san.hbas[].slotztp.isolate_san_adapters with cluster_nodes[].san.hbas[].slottrue when this node lists san.hbascontrolled (new): was only possible by writing Dell's Slot2 by hand
B3BIOS: enable the embedded NICs for a management or provisioning port in slot Embeddedprovisioning.simplified.enable_embedded_nics with nodes[].network_adapters[].slotztp.enable_embedded_nicstruecontrolled (new)
B4The slot label to attribute mapping (Slot{n}, Disabled, Enabled, EmbNic1Nic2; verified_models AX-760)none (vendor data)config/oem/<vendor>.json, biosDell onlycontrolled (new): data, not code; other vendors name the attribute per HBA
B4aThe BIOS attribute and values of one HBA, for a vendor without a mapping or a label that is not Slot <n>nodes[].san.hbas[].bios_attribute, bios_disabled_value, bios_enabled_valuecluster_nodes[].san.hbas[].*the mapping's values, else Disabled and Enabledcontrolled (new)
B5Before any node changes: read every node's BIOS and attribute registry, and stop on an attribute the BMC lacks or a value it does not allow (a registry that cannot be read warns and continues)nonenonealwaysHARD-CODED (kept): a safety check, no reason to switch it off
B6BIOS applied OnReset, then the node is restarted (ForceRestart, or On when it is off)nonenoneOnReset, ForceRestartHARD-CODED (kept): the machine is reinstalled next; a graceful restart buys nothing
B7How long to wait for the BIOS to report the new valuesprovisioning.simplified.timeouts.bios_minutesztp.timeouts.bios_minutes30controlled (new)
B8BIOS poll interval while waitingnonenone30 sHARD-CODED (kept): script parameter, internal polling
B9Which BMC, which loginnodes[].bmc.ip_address, hardware.bmc.username, hardware.bmc.passwordcluster_nodes[].idrac_ip, ztp.bmc_secret.*none (required)controlled
B10Accept the BMC's self-signed TLS certificatehardware.bmc.verify_certificateztp.verify_bmc_certificatefalse (accept)controlled (new): was always on
B11Also set the iDRAC Remote File Share write attribute before inserting mediahardware.bmc.rfs_write_attributeztp.rfs_write_attributetrue for Dell, false for the other vendors (config/oem/<vendor>.json, bmc.rfsWriteAttribute)HARD-CODED (fixed): was true for every vendor, and fails on a BMC that has no RFS.<n>.WriteProtected
B12SMB share nameprovisioning.simplified.media_share.nameztp.share_namenone (required)controlled
B13The address the BMCs reach this host onprovisioning.simplified.media_share.host or host_adapterztp.share_host, ztp.share_host_adapternone (required)controlled
B14The local account the BMCs sign in withprovisioning.simplified.media_share.account_nameztp.share_account_namesvc-ztp-mediacontrolled (new)
B15Keep the share, firewall rule and account after the vouchers are collectedprovisioning.simplified.media_share.keepztp.keep_sharefalsecontrolled (new)
B16The share password: 32 random characters, never stored or printednonenonegeneratedHARD-CODED (kept): security design
B17The share is read-write (the maintenance environment writes its voucher) and TCP 445 is open only from the BMC addressesnonenoneread-write, 445HARD-CODED (kept): security design; read-write is how the voucher gets out
B18Virtual media: the first slot whose MediaTypes has USBStick, TransferProtocolType CIFSnonenonefixedHARD-CODED (kept): the standard Redfish type; the OEM file's virtualMediaSlot is informational
B19One-time boot target: Usb when the BMC offers it, else the iDRAC 7.30 workaround (ServerBoot.1.FirstBootDevice=vFDD plus a Floppy override, 120 s wait for it to land)nonenoneauto-detected from the BMC's AllowableValuesHARD-CODED (kept): detected, not chosen; the fallback is iDRAC-specific (see Hardware below)
B20After the restart, watch 10 minutes (every 20 s) for the iDRAC Lifecycle log message OSE1002 ("booted its installed OS") and stop if it appearsnonenone10 min, LclogHARD-CODED (fixed): skipped with a warning on a BMC without an Lclog service (it threw a 404); the 10 minutes stay, see Hardware
B21Manager and system: the first member of /redfish/v1/Managers and /redfish/v1/SystemsnonenonefirstHARD-CODED (kept): vendor-neutral Redfish; one system per BMC
B22-ConfirmNotClustered and the restart are passed on -Execute; every node boots at once (throttle = node count)nonenoneonHARD-CODED (kept): -Execute is the operator's confirmation; the machines are reinstalled
B23Which BMC family's log services and signals the watch readshardware.vendorhardware.vendornone (required)controlled

Bootstrap (Wait-ZtpBootstrapComplete.ps1) ​

#What it doesSchema 5 pathEnvironment-file keyDefaultStatus
Bt1Overall budget until every node is ready for the portalprovisioning.simplified.timeouts.bootstrap_minutesztp.timeouts.bootstrap_minutes120controlled (new)
Bt2Budget until a valid voucher appearsprovisioning.simplified.timeouts.bootstrap_voucher_minutesztp.timeouts.bootstrap_voucher_minutes90controlled (new)
Bt3How long the BMC may show no change before a stall is reportedprovisioning.simplified.timeouts.bootstrap_stall_minutesztp.timeouts.bootstrap_stall_minutes20controlled (new)
Bt4Poll interval (15 s, backing off to 120 s)nonenone15 s, 120 sHARD-CODED (kept): internal polling
Bt5The Azure prerequisite check (feature Microsoft.DeviceOnboarding/AzureLocalZTP; providers Microsoft.AzureStackHCI, Microsoft.Edge, Microsoft.HybridCompute, Microsoft.DeviceOnboarding; the OS image catalog)provisioning.simplified.skip_azure_checkztp.skip_azure_checkfalse (check)controlled (new)
Bt6The list in Bt5nonenonefixedHARD-CODED (kept): what Microsoft requires for the claim
Bt7Catalog region of the checkazure.locationazure_resources.locationnone (required)controlled
Bt8The voucher: \vouchers\<serial>\*.pem in the image, validated as PEM, matched to the node by serial numbernodes[].serial_numbercluster_nodes[].service_tagnone (required)controlled
Bt9Log services and message patterns the watch readsnone (vendor data)config/oem/<vendor>.json, bmc.logServices, bmc.signalsDell patterns marked unverified; other vendors emptycontrolled: data file

Vouchers (Wait-ZtpVoucher.ps1, Read-ZtpVoucherFromImage.ps1) ​

#What it doesSchema 5 pathEnvironment-file keyDefaultStatus
V1How long to wait for each node's voucher in its imageprovisioning.simplified.timeouts.voucher_wait_minutesztp.timeouts.voucher_wait_minutes120controlled (new)
V2Poll interval 60 s; eject retries every 30 snonenone60 s, 30 sHARD-CODED (kept): internal polling
V3Replace a local voucher (a voucher is one-time use; the image holds the current one)nonenoneonHARD-CODED (kept): correctness
V4Where <serial>.pem is writtenprovisioning.simplified.media_pathztp.media_path<media>\voucherscontrolled (root)
V5The Configurator App voucher folder fallbacknonenone (orchestrator -VoucherFolder)offHARD-CODED (kept): a manual fallback, not part of the unattended flow

Device (Set-ZtpDeviceConfiguration.ps1) ​

#What it doesSchema 5 pathEnvironment-file keyDefaultStatus
D1TCP port of the maintenance environment's Configurator serviceprovisioning.simplified.maintenance_environment.portztp.maintenance_environment.port20001controlled (new)
D2The service's userprovisioning.simplified.maintenance_environment.usernameztp.maintenance_environment.usernameedgeusercontrolled (new)
D3Its passwordprovisioning.simplified.maintenance_passwordztp.maintenance_password_secretMicrosoft's published out-of-box passwordcontrolled
D4Where it scans for the service of a node without an address (one TCP connect to the port per address)networking.management.provisioning_dhcp_rangenetwork_intents.management.provisioning_dhcp_rangethe management subnet; at most 4096 addressescontrolled
D5A node's own maintenance addressnodes[].maintenance_address, else the ip_address of its provisioning portcluster_nodes[].maintenance_addressnonecontrolled
D6How long the scan and the wait for the address may takeprovisioning.simplified.timeouts.device_minutesztp.timeouts.device_minutes30controlled (new)
D7Which port gets the static address: the one with the management MACnodes[].network_adapters[] (role management), networking.management.static_methodcluster_nodes[].mac_address, network_intents.management.static_methoddevicecontrolled
D8Address, mask, gateway, DNS, VLAN sentnodes[].network_adapters[].ip_address, networking.management.*cluster_nodes[].ip_address, network_intents.management.*nonecontrolled
D9Skip the certificate check of the maintenance environment; probe timeout 500 ms; request timeout 30 s; poll interval from the Ready stagenonenonefixedHARD-CODED (kept): the maintenance environment's certificate is self-generated and can't be pinned; the timeouts are tuning
D10The REST operations (GetNetworkAdapters, SetArcBootstrapDeviceConfiguration, ...)nonenonefixedHARD-CODED (kept): Microsoft's API

Azure and Ready (Invoke-ZtpAzureProvisioning.ps1, Wait-ZtpMachineReady.ps1, bicep/main.bicep, Invoke-ZtpEnvironment.ps1) ​

#What it doesSchema 5 pathEnvironment-file keyDefaultStatus
A1Subscription and resource group of the site and edge machinesprovisioning.simplified.subscription_id, resource_group, else azure.subscription_id, azure.resource_groupztp.subscription_id, ztp.resource_groupnone (required)controlled
A2Create the resource group when it is missing (in azure.location, with the tags)provisioning.simplified.create_resource_groupztp.create_resource_grouptruecontrolled (new)
A3Site resource name and display nameprovisioning.simplified.site_name, site.nameztp.site_name, site.namenone (required)controlled
A4Name of the site configuration (Microsoft.Edge/configurations)provisioning.simplified.configuration_nameztp.configuration_namedefaultcontrolled (new)
A5Managed resource group nameprovisioning.simplified.managed_resource_group_nameztp.managed_resource_group_nameempty: the service choosescontrolled (new)
A6Create the site's network, time, connectivity and security settingsprovisioning.simplified.apply_site_settingsztp.apply_site_settingstruecontrolled (new)
A7Network setting kindprovisioning.simplified.network_kindztp.network_kindLANcontrolled (new)
A8Network setting: Manual or Automatic, with the range, gateway, DNS, VLANnetworking.management.*, static_method, the nodes' ip_assignmentnetwork_intents.management.*Automatic with static_method: devicecontrolled
A9Time setting: Automatic or Manual NTP and the serversprovisioning.simplified.ntp.configuration, server, alternate_serverztp.ntp.*Automaticcontrolled (new)
A10Time zonesite.time_zoneztp.time_zonenone (required)controlled
A11Connectivity setting PublicnonenonePublicHARD-CODED (kept): the site setting carries no proxy details, and Arc gateway is not supported with simplified provisioning (rule AZL-016); a proxy path needs a new Bicep shape first
A12Security setting: reference the Key Vault (purpose LocalAdminCredentials)provisioning.simplified.use_key_vault_configurationztp.use_key_vault_configurationtruecontrolled (new)
A13Role assignment: the Azure Local resource provider's Connected Machine Resource Manager role on the resource groupprovisioning.simplified.assign_hci_rp_roleztp.assign_hci_rp_roletruecontrolled (new)
A14Role assignment: each edge machine's constrained delegation (RBAC Administrator, conditioned to Connected Machine Resource Administrator) on the resource group and the Key Vaultprovisioning.simplified.assign_provisioning_rolesztp.assign_provisioning_rolestruecontrolled (new)
A15The role definition IDs and the delegation conditionnonenonefixedHARD-CODED (kept): Azure's built-in roles and the portal template's condition
A16Resource provider object IDazure.hci_resource_provider_object_idztp.azl_rp_object_idlooked up by the first-party application ID 1412d89f-b8a8-4111-b4fd-e82905cbd85d when the directory is readablecontrolled (the application ID is Microsoft's constant)
A17Tags on the resource group, configuration and edge machines (Microsoft.Edge sites take none)tagsztp.tagsnonecontrolled
A18Site mode: create what is missing, create only, or reuseprovisioning.simplified.site_modeztp.site_modeAutocontrolled (new)
A19ARM deployment nameprovisioning.simplified.deployment_nameztp.deployment_nameztp-<site_name>controlled (new)
A20The OS version (latest, latest-preview, or a validated solution recipe version), resolved from the osImages catalogprovisioning.os_versionztp.os_versionnone (required; AZL-013 warns about latest)controlled
A21The provisioning administrator: user name and the Key Vault secretprovisioning.simplified.admin.username, admin.passwordztp.admin_secret.*none (required)controlled
A22Arc machine name (the service may overwrite it at claim time: open item in infrastructure-schema-5.md)nodes[].arc_machine_namecluster_nodes[].arc_machine_namethe node namecontrolled
A23Edge machine identity SystemAssigned, owner key type MicrosoftManaged, osType HCI, secret type KeyVaultnonenonefixedHARD-CODED (kept): the API's constants for Azure Local
A24API versions: sites 2025-06-01, configurations 2025-12-01-preview, edge machines and jobs 2026-10-01nonenonefixedHARD-CODED (kept): the scripts and the Bicep are written against those shapes; another version needs code, not a value
A25Resubmit a failed OS install jobprovisioning.simplified.retry_provision_osztp.retry_provision_ostruecontrolled (new)
A26How many times (1)nonenone1HARD-CODED (kept): script parameter; a second failure is a real failure, not one to retry past
A27Ready means: lifecycle Provisioned, job DeploymentSuccess, Arc Connected, the four required extensions (AzureEdgeDeviceManagement, AzureEdgeLifecycleManager, AzureEdgeRemoteSupport, AzureEdgeTelemetryAndDiagnostics)nonenonefixedHARD-CODED (kept): Azure Local's own definition
A28Ready polling budget and intervalprovisioning.simplified.timeouts.ready_minutes, poll_interval_secondsztp.timeouts.ready_minutes, ztp.timeouts.poll_interval_seconds180, 60controlled (new)
A29Edge machines that already exist are left out of the claimnonenoneonHARD-CODED (kept): idempotence
A30Sign in as a managed identitynone (-UseManagedIdentity)noneoffcontrolled: a switch of the script

Network, Restore, Handoff ​

#What it doesSchema 5 pathEnvironment-file keyDefaultStatus
N1Network: find the management port by MAC and compare the address with the file; verify onlynodes[].network_adapters[]cluster_nodes[].mac_address, ip_address, ip_assignmentverifycontrolled
N2Network: move the address through Azure's ApplyConfigurationnone (-ApplyThroughAzure)noneoffHARD-CODED (kept): documented unreliable (the job never started on two live runs); opt-in on the orchestrator
N3Network: how long each Azure configuration job may takeprovisioning.simplified.timeouts.network_minutesztp.timeouts.network_minutes60controlled (new)
Rs1Restore: which BIOS values are set back (the final values, or the value saved before Boot)hardware.bios*, nodes[].bios*, the derived planbios_restore in the node listnonecontrolled
Rs2Restore: settle time before the Arc checkprovisioning.simplified.timeouts.restore_settle_minutesztp.timeouts.restore_settle_minutes5controlled (new)
Rs3Restore: how long the Arc machines may take to reconnectprovisioning.simplified.timeouts.ready_minutesztp.timeouts.ready_minutes180controlled
H1Handoff content: edge machine and Arc machine names and IDs, the site, the installed solution version, each node's Network result, the BIOS values Restore set backnonenonefixedHARD-CODED (kept): the output contract
H2Handoff and cluster-input files, run name (nodes joined by +), event and change log names (ztp-events.jsonl, bios-changes.jsonl, device-changes.jsonl, network-changes.jsonl)provisioning.simplified.media_path, evidence_path root themztp.media_path, ztp.evidence_path%ProgramData%\AzureLocalZtp\<site_name>\...HARD-CODED (kept): names are the evidence contract Sonar follows; the folders are controlled
H3A transcript of every -Execute run in the evidence foldernonenoneonHARD-CODED (kept): evidence

Is ZTP generic about the hardware? ​

QuestionAnswer
Port countsYes. Nothing counts ports. The Device stage matches the maintenance environment's adapter list to the management MAC, the Network stage matches Azure's per-NIC report to the same MAC, and the management port is the one network_adapters[] entry with role management and an ip_assignment other than none. A node with 2, 6 or 12 ports is read the same way; the OS names them Port0 to PortN, which ZTP never relies on.
Adapter models and typesYes. An onboard LOM plus a PCIe NIC of another vendor, or several PCIe cards, are just entries with a MAC, roles, and (new) a slot. model is carried to the handoff and read by nothing else.
Slot namesOnly the BIOS plan reads them: Slot <n> for a PCIe slot and Embedded for a LOM, and only to find the BIOS attribute through the vendor's mapping (B2 to B4). An unreadable label stops the run before anything changes.
VendorPartly. The Redfish client is vendor-neutral (first manager, first system, the USBStick media type). Dell-specific behaviours found: B11 (the RFS attribute, fixed: default by vendor), B20 (the Lclog gate, fixed: skipped with a warning on another BMC), B19 (the ServerBoot.1.* vFDD workaround, used only when the BMC doesn't offer Usb; left, and a gap on an HPE or Lenovo BMC that lacks Usb), the Dell-only log signals in config/oem/dell.json (unverified; HPE, Lenovo and Supermicro have none, so their Bootstrap watch relies on the voucher in the image), the default -Oem Dell of the orchestrator and Wait-ZtpBootstrapComplete.ps1 when they are run by hand (Invoke-ZtpEnvironment.ps1 always passes hardware.vendor), and the BIOS slot mapping (B4), which only Dell has. Messages that say "iDRAC" are wording only.
BMC typenodes[].bmc.type (idrac, ilo, xcc, other) is not read; the BMC is whatever answers Redfish. One BMC login serves every node (hardware.bmc); a node with its own login is a validation error.
Flagged: assumes a particular serverThe 4 GiB image size (booted on Dell 16G): now M2. The 10 minute disk-boot gate "covers POST on these machines" (B20): kept; a server with a longer POST would see the gate end early, not fail. The OSE1002 message ID is Dell's. Comments naming the AX-760 and the ConnectX port are history, not logic.

Moving an existing file to 5.4 ​

A file that uses none of the new keys behaves exactly as before. To let ZTP derive the BIOS plan instead of the raw Dell attributes: set _metadata.schema_version to 5.4.0, list each machine's HBA in nodes[].san.hbas[] with slot: "Slot 2", give the onboard ports slot: "Embedded", and delete the raw Slot2 and EmbNic1Nic2 entries from hardware.bios and hardware.bios_during_provisioning (or keep them: an explicit key wins and gives the same values). The derived plan then reads Slot2 Disabled for the install, the value Boot found put back after it (Enabled on the lab machines, whose slot reads Enabled before the install), and EmbNic1Nic2 Enabled.

Released under the MIT License.