From eb456cea85c911aed286a3a0c32d4b879f682fff Mon Sep 17 00:00:00 2001 From: DanteMustCode <52395211+DanteMustCode@users.noreply.github.com> Date: Wed, 12 Feb 2025 20:33:58 +0800 Subject: [PATCH 1/7] feat: New module `avm/res/azure-stack-hci/cluster` module (#4417) ## Description Adds new Azure Stack HCI Cluster AVM module. Some helper modules for e2e testing are placed here `avm\utilities\e2e-template-assets\templates\azure-stack-hci`, which will also be using them for other HCI resource modules (always needing to deploy an HCI cluster in Azure to test on). If this is not the right location for this sort of shared asset, please let us know. For some reason Set-AVMModule does not generate cross-ref correctly on my dev machine, it is manually added back to pass the static validation. ## Pipeline Reference | Pipeline | | -------- | | [![avm.res.azure-stack-hci.cluster](https://github.com/Infrastructure-as-code-Automation/bicep-registry-modules/actions/workflows/avm.res.azure-stack-hci.cluster.yml/badge.svg?branch=user%2FDanteMustCode%2Fhci-cs)](https://github.com/Infrastructure-as-code-Automation/bicep-registry-modules/actions/workflows/avm.res.azure-stack-hci.cluster.yml) | ## Type of Change - [ ] Update to CI Environment or utilities (Non-module affecting changes) - [x] Azure Verified Module updates: - [ ] Bugfix containing backwards-compatible bug fixes, and I have NOT bumped the MAJOR or MINOR version in `version.json`: - [ ] Someone has opened a bug report issue, and I have included "Closes #{bug_report_issue_number}" in the PR description. - [ ] The bug was found by the module author, and no one has opened an issue to report it yet. - [ ] Feature update backwards compatible feature updates, and I have bumped the MINOR version in `version.json`. - [ ] Breaking changes and I have bumped the MAJOR version in `version.json`. - [ ] Update to documentation ## Checklist - [x] I'm sure there are no other open Pull Requests for the same update/change - [x] I have run `Set-AVMModule` locally to generate the supporting module files. - [x] My corresponding pipelines / checks run clean and green without any errors or warnings As agreed offline, [the existing draft pull request](https://github.com/Azure/bicep-registry-modules/pull/4155) by @AlexanderSehr and [the previous pull request](https://github.com/Azure/bicep-registry-modules/pull/3364) by @mbrat2005 will be superseded by this. Special thanks to their contribution to this pull request. ## Maintainer checklist: * [ ] Microsoft.AzureStackHCI RP registered (in order to grant permissions before starting) * [ ] Service Principal created and granted Azure Resource Bridge Deployment role on the management group (deployment will create subscription-level permissions, but parallel deployments/failures may clean up the required role). Create a secret credential. * [ ] CI-arbDeploymentAppId * [ ] CI-arbDeploymentSPObjectId (from Enterprise Application) * [ ] CI-arbDeploymentServicePrincipalSecret * [ ] CI-hciResourceProviderObjectId (Get-AzADServicePrincipal -ApplicationId 1412d89f-b8a8-4111-b4fd-e82905cbd85d) --------- Co-authored-by: Matthew Bratschun Co-authored-by: Matthew Bratschun <25390936+mbrat2005@users.noreply.github.com> Co-authored-by: Alexander Sehr Co-authored-by: Hangyu Xu --- .github/CODEOWNERS | 1 + .github/ISSUE_TEMPLATE/avm_module_issue.yml | 1 + .../avm.res.azure-stack-hci.cluster.yml | 88 ++ avm/res/azure-stack-hci/cluster/README.md | 1327 +++++++++++++++++ .../cluster/deployment-setting/README.md | 349 +++++ .../cluster/deployment-setting/main.bicep | 246 +++ .../cluster/deployment-setting/main.json | 397 +++++ avm/res/azure-stack-hci/cluster/main.bicep | 405 +++++ avm/res/azure-stack-hci/cluster/main.json | 1273 ++++++++++++++++ .../ashciARBSPRoleAssignment.bicep | 19 + .../ashciPrereqs.bicep | 346 +++++ .../azureStackHCIHost/README.md | 67 + .../azureStackHCIHost/hciHostDeployment.bicep | 535 +++++++ .../modules/subscriptionRoleAssignment.bicep | 16 + .../scripts/hciHostStage1.ps1 | 53 + .../scripts/hciHostStage2.ps1 | 40 + .../scripts/hciHostStage3.ps1 | 333 +++++ .../scripts/hciHostStage4.ps1 | 41 + .../scripts/hciHostStage5.ps1 | 398 +++++ .../scripts/hciHostStage6.ps1 | 354 +++++ .../scripts/hciHostStage7.ps1 | 207 +++ .../azureStackHCIHost/scripts/proxyConfig.sh | 153 ++ .../scripts/proxyConfigArcGW.sh | 171 +++ .../tests/e2e/defaults/dependencies.bicep | 145 ++ .../tests/e2e/defaults/main.test.bicep | 176 +++ .../tests/e2e/waf-aligned/dependencies.bicep | 145 ++ .../tests/e2e/waf-aligned/main.test.bicep | 187 +++ avm/res/azure-stack-hci/cluster/version.json | 7 + 28 files changed, 7480 insertions(+) create mode 100644 .github/workflows/avm.res.azure-stack-hci.cluster.yml create mode 100644 avm/res/azure-stack-hci/cluster/README.md create mode 100644 avm/res/azure-stack-hci/cluster/deployment-setting/README.md create mode 100644 avm/res/azure-stack-hci/cluster/deployment-setting/main.bicep create mode 100644 avm/res/azure-stack-hci/cluster/deployment-setting/main.json create mode 100644 avm/res/azure-stack-hci/cluster/main.bicep create mode 100644 avm/res/azure-stack-hci/cluster/main.json create mode 100644 avm/res/azure-stack-hci/cluster/tests/e2e-template-assets/azureStackHCIClusterPreqs/ashciARBSPRoleAssignment.bicep create mode 100644 avm/res/azure-stack-hci/cluster/tests/e2e-template-assets/azureStackHCIClusterPreqs/ashciPrereqs.bicep create mode 100644 avm/res/azure-stack-hci/cluster/tests/e2e-template-assets/azureStackHCIHost/README.md create mode 100644 avm/res/azure-stack-hci/cluster/tests/e2e-template-assets/azureStackHCIHost/hciHostDeployment.bicep create mode 100644 avm/res/azure-stack-hci/cluster/tests/e2e-template-assets/azureStackHCIHost/modules/subscriptionRoleAssignment.bicep create mode 100644 avm/res/azure-stack-hci/cluster/tests/e2e-template-assets/azureStackHCIHost/scripts/hciHostStage1.ps1 create mode 100644 avm/res/azure-stack-hci/cluster/tests/e2e-template-assets/azureStackHCIHost/scripts/hciHostStage2.ps1 create mode 100644 avm/res/azure-stack-hci/cluster/tests/e2e-template-assets/azureStackHCIHost/scripts/hciHostStage3.ps1 create mode 100644 avm/res/azure-stack-hci/cluster/tests/e2e-template-assets/azureStackHCIHost/scripts/hciHostStage4.ps1 create mode 100644 avm/res/azure-stack-hci/cluster/tests/e2e-template-assets/azureStackHCIHost/scripts/hciHostStage5.ps1 create mode 100644 avm/res/azure-stack-hci/cluster/tests/e2e-template-assets/azureStackHCIHost/scripts/hciHostStage6.ps1 create mode 100644 avm/res/azure-stack-hci/cluster/tests/e2e-template-assets/azureStackHCIHost/scripts/hciHostStage7.ps1 create mode 100644 avm/res/azure-stack-hci/cluster/tests/e2e-template-assets/azureStackHCIHost/scripts/proxyConfig.sh create mode 100644 avm/res/azure-stack-hci/cluster/tests/e2e-template-assets/azureStackHCIHost/scripts/proxyConfigArcGW.sh create mode 100644 avm/res/azure-stack-hci/cluster/tests/e2e/defaults/dependencies.bicep create mode 100644 avm/res/azure-stack-hci/cluster/tests/e2e/defaults/main.test.bicep create mode 100644 avm/res/azure-stack-hci/cluster/tests/e2e/waf-aligned/dependencies.bicep create mode 100644 avm/res/azure-stack-hci/cluster/tests/e2e/waf-aligned/main.test.bicep create mode 100644 avm/res/azure-stack-hci/cluster/version.json diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 1c1f38b409..aafe6f8a1a 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -45,6 +45,7 @@ /avm/res/app/managed-environment/ @Azure/avm-res-app-managedenvironment-module-owners-bicep @Azure/avm-module-reviewers-bicep /avm/res/app-configuration/configuration-store/ @Azure/avm-res-appconfiguration-configurationstore-module-owners-bicep @Azure/avm-module-reviewers-bicep /avm/res/automation/automation-account/ @Azure/avm-res-automation-automationaccount-module-owners-bicep @Azure/avm-module-reviewers-bicep +/avm/res/azure-stack-hci/cluster/ @Azure/avm-res-azurestackhci-cluster-module-owners-bicep @Azure/avm-module-reviewers-bicep /avm/res/batch/batch-account/ @Azure/avm-res-batch-batchaccount-module-owners-bicep @Azure/avm-module-reviewers-bicep /avm/res/cache/redis/ @Azure/avm-res-cache-redis-module-owners-bicep @Azure/avm-module-reviewers-bicep /avm/res/cdn/profile/ @Azure/avm-res-cdn-profile-module-owners-bicep @Azure/avm-module-reviewers-bicep diff --git a/.github/ISSUE_TEMPLATE/avm_module_issue.yml b/.github/ISSUE_TEMPLATE/avm_module_issue.yml index 7e02a41292..ed4b8fb00d 100644 --- a/.github/ISSUE_TEMPLATE/avm_module_issue.yml +++ b/.github/ISSUE_TEMPLATE/avm_module_issue.yml @@ -80,6 +80,7 @@ body: - "avm/res/app/job" - "avm/res/app/managed-environment" - "avm/res/automation/automation-account" + - "avm/res/azure-stack-hci/cluster" - "avm/res/batch/batch-account" - "avm/res/cache/redis" - "avm/res/cdn/profile" diff --git a/.github/workflows/avm.res.azure-stack-hci.cluster.yml b/.github/workflows/avm.res.azure-stack-hci.cluster.yml new file mode 100644 index 0000000000..f9a35e7914 --- /dev/null +++ b/.github/workflows/avm.res.azure-stack-hci.cluster.yml @@ -0,0 +1,88 @@ +name: "avm.res.azure-stack-hci.cluster" + +on: + workflow_dispatch: + inputs: + staticValidation: + type: boolean + description: "Execute static validation" + required: false + default: true + deploymentValidation: + type: boolean + description: "Execute deployment validation" + required: false + default: true + removeDeployment: + type: boolean + description: "Remove deployed module" + required: false + default: true + customLocation: + type: string + description: "Default location overwrite (e.g., eastus)" + required: false + push: + branches: + - main + paths: + - ".github/actions/templates/avm-**" + - ".github/workflows/avm.template.module.yml" + - ".github/workflows/avm.res.azure-stack-hci.cluster.yml" + - "avm/res/azure-stack-hci/cluster/**" + - "avm/utilities/pipelines/**" + - "!avm/utilities/pipelines/platform/**" + - "!*/**/README.md" + +env: + modulePath: "avm/res/azure-stack-hci/cluster" + workflowPath: ".github/workflows/avm.res.azure-stack-hci.cluster.yml" + +concurrency: + group: ${{ github.workflow }} + +jobs: + ########################### + # Initialize pipeline # + ########################### + job_initialize_pipeline: + runs-on: ubuntu-latest + name: "Initialize pipeline" + steps: + - name: "Checkout" + uses: actions/checkout@v4 + with: + fetch-depth: 0 + - name: "Set input parameters to output variables" + id: get-workflow-param + uses: ./.github/actions/templates/avm-getWorkflowInput + with: + workflowPath: "${{ env.workflowPath}}" + - name: "Get module test file paths" + id: get-module-test-file-paths + uses: ./.github/actions/templates/avm-getModuleTestFiles + with: + modulePath: "${{ env.modulePath }}" + outputs: + workflowInput: ${{ steps.get-workflow-param.outputs.workflowInput }} + moduleTestFilePaths: ${{ steps.get-module-test-file-paths.outputs.moduleTestFilePaths }} + psRuleModuleTestFilePaths: ${{ steps.get-module-test-file-paths.outputs.psRuleModuleTestFilePaths }} + modulePath: "${{ env.modulePath }}" + + ############################## + # Call reusable workflow # + ############################## + call-workflow-passing-data: + name: "Run" + permissions: + id-token: write # For OIDC + contents: write # For release tags + needs: + - job_initialize_pipeline + uses: ./.github/workflows/avm.template.module.yml + with: + workflowInput: "${{ needs.job_initialize_pipeline.outputs.workflowInput }}" + moduleTestFilePaths: "${{ needs.job_initialize_pipeline.outputs.moduleTestFilePaths }}" + psRuleModuleTestFilePaths: "${{ needs.job_initialize_pipeline.outputs.psRuleModuleTestFilePaths }}" + modulePath: "${{ needs.job_initialize_pipeline.outputs.modulePath}}" + secrets: inherit diff --git a/avm/res/azure-stack-hci/cluster/README.md b/avm/res/azure-stack-hci/cluster/README.md new file mode 100644 index 0000000000..4cb635ad3f --- /dev/null +++ b/avm/res/azure-stack-hci/cluster/README.md @@ -0,0 +1,1327 @@ +# Azure Stack HCI Cluster `[Microsoft.AzureStackHCI/clusters]` + +This module deploys an Azure Stack HCI Cluster on the provided Arc Machines. + +## Navigation + +- [Resource Types](#Resource-Types) +- [Usage examples](#Usage-examples) +- [Parameters](#Parameters) +- [Outputs](#Outputs) +- [Cross-referenced modules](#Cross-referenced-modules) +- [Data Collection](#Data-Collection) + +## Resource Types + +| Resource Type | API Version | +| :-- | :-- | +| `Microsoft.Authorization/roleAssignments` | [2022-04-01](https://learn.microsoft.com/en-us/azure/templates/Microsoft.Authorization/2022-04-01/roleAssignments) | +| `Microsoft.AzureStackHCI/clusters` | [2024-04-01](https://learn.microsoft.com/en-us/azure/templates/Microsoft.AzureStackHCI/clusters) | +| `Microsoft.AzureStackHCI/clusters/deploymentSettings` | [2024-04-01](https://learn.microsoft.com/en-us/azure/templates/Microsoft.AzureStackHCI/clusters/deploymentSettings) | + +## Usage examples + +The following section provides usage examples for the module, which were used to validate and deploy the module successfully. For a full reference, please review the module's test folder in its repository. + +>**Note**: Each example lists all the required parameters first, followed by the rest - each in alphabetical order. + +>**Note**: To reference the module, please use the following syntax `br/public:avm/res/azure-stack-hci/cluster:`. + +- [Deploy Azure Stack HCI Cluster in Azure with a 2 node switched configuration](#example-1-deploy-azure-stack-hci-cluster-in-azure-with-a-2-node-switched-configuration) +- [Deploy Azure Stack HCI Cluster in Azure with a 2 node switched configuration WAF aligned](#example-2-deploy-azure-stack-hci-cluster-in-azure-with-a-2-node-switched-configuration-waf-aligned) + +### Example 1: _Deploy Azure Stack HCI Cluster in Azure with a 2 node switched configuration_ + +This test deploys an Azure VM to host a 2 node switched Azure Stack HCI cluster, validates the cluster configuration, and then deploys the cluster. + + +
+ +via Bicep module + +```bicep +module cluster 'br/public:avm/res/azure-stack-hci/cluster:' = { + name: 'clusterDeployment' + params: { + // Required parameters + name: '' + // Non-required parameters + deploymentSettings: { + clusterNodeNames: '' + clusterWitnessStorageAccountName: '' + customLocationName: 'ashc2nmin-location' + defaultGateway: '172.20.0.1' + deploymentPrefix: '' + dnsServers: [ + '172.20.0.1' + ] + domainFqdn: 'hci.local' + domainOUPath: '' + enableStorageAutoIp: true + endingIPAddress: '172.20.0.7' + keyVaultName: '' + networkIntents: [ + { + adapter: [ + 'mgmt' + ] + adapterPropertyOverrides: { + jumboPacket: '9014' + networkDirect: 'Disabled' + networkDirectTechnology: 'iWARP' + } + name: 'management' + overrideAdapterProperty: true + overrideQosPolicy: false + overrideVirtualSwitchConfiguration: false + qosPolicyOverrides: { + bandwidthPercentage_SMB: '50' + priorityValue8021Action_Cluster: '7' + priorityValue8021Action_SMB: '3' + } + trafficType: [ + 'Management' + ] + virtualSwitchConfigurationOverrides: { + enableIov: 'true' + loadBalancingAlgorithm: 'Dynamic' + } + } + { + adapter: [ + 'comp0' + 'comp1' + ] + adapterPropertyOverrides: { + jumboPacket: '9014' + networkDirect: 'Disabled' + networkDirectTechnology: 'iWARP' + } + name: 'compute' + overrideAdapterProperty: true + overrideQosPolicy: false + overrideVirtualSwitchConfiguration: false + qosPolicyOverrides: { + bandwidthPercentage_SMB: '50' + priorityValue8021Action_Cluster: '7' + priorityValue8021Action_SMB: '3' + } + trafficType: [ + 'Compute' + ] + virtualSwitchConfigurationOverrides: { + enableIov: 'true' + loadBalancingAlgorithm: 'Dynamic' + } + } + { + adapter: [ + 'smb0' + 'smb1' + ] + adapterPropertyOverrides: { + jumboPacket: '9014' + networkDirect: 'Disabled' + networkDirectTechnology: 'iWARP' + } + name: 'storage' + overrideAdapterProperty: true + overrideQosPolicy: true + overrideVirtualSwitchConfiguration: false + qosPolicyOverrides: { + bandwidthPercentage_SMB: '50' + priorityValue8021Action_Cluster: '7' + priorityValue8021Action_SMB: '3' + } + trafficType: [ + 'Storage' + ] + virtualSwitchConfigurationOverrides: { + enableIov: 'true' + loadBalancingAlgorithm: 'Dynamic' + } + } + ] + startingIPAddress: '172.20.0.2' + storageConnectivitySwitchless: false + storageNetworks: [ + { + adapterName: 'smb0' + vlan: '711' + } + { + adapterName: 'smb1' + vlan: '712' + } + ] + subnetMask: '255.255.255.0' + } + } +} +``` + +
+

+ +

+ +via JSON parameters file + +```json +{ + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#", + "contentVersion": "1.0.0.0", + "parameters": { + // Required parameters + "name": { + "value": "" + }, + // Non-required parameters + "deploymentSettings": { + "value": { + "clusterNodeNames": "", + "clusterWitnessStorageAccountName": "", + "customLocationName": "ashc2nmin-location", + "defaultGateway": "172.20.0.1", + "deploymentPrefix": "", + "dnsServers": [ + "172.20.0.1" + ], + "domainFqdn": "hci.local", + "domainOUPath": "", + "enableStorageAutoIp": true, + "endingIPAddress": "172.20.0.7", + "keyVaultName": "", + "networkIntents": [ + { + "adapter": [ + "mgmt" + ], + "adapterPropertyOverrides": { + "jumboPacket": "9014", + "networkDirect": "Disabled", + "networkDirectTechnology": "iWARP" + }, + "name": "management", + "overrideAdapterProperty": true, + "overrideQosPolicy": false, + "overrideVirtualSwitchConfiguration": false, + "qosPolicyOverrides": { + "bandwidthPercentage_SMB": "50", + "priorityValue8021Action_Cluster": "7", + "priorityValue8021Action_SMB": "3" + }, + "trafficType": [ + "Management" + ], + "virtualSwitchConfigurationOverrides": { + "enableIov": "true", + "loadBalancingAlgorithm": "Dynamic" + } + }, + { + "adapter": [ + "comp0", + "comp1" + ], + "adapterPropertyOverrides": { + "jumboPacket": "9014", + "networkDirect": "Disabled", + "networkDirectTechnology": "iWARP" + }, + "name": "compute", + "overrideAdapterProperty": true, + "overrideQosPolicy": false, + "overrideVirtualSwitchConfiguration": false, + "qosPolicyOverrides": { + "bandwidthPercentage_SMB": "50", + "priorityValue8021Action_Cluster": "7", + "priorityValue8021Action_SMB": "3" + }, + "trafficType": [ + "Compute" + ], + "virtualSwitchConfigurationOverrides": { + "enableIov": "true", + "loadBalancingAlgorithm": "Dynamic" + } + }, + { + "adapter": [ + "smb0", + "smb1" + ], + "adapterPropertyOverrides": { + "jumboPacket": "9014", + "networkDirect": "Disabled", + "networkDirectTechnology": "iWARP" + }, + "name": "storage", + "overrideAdapterProperty": true, + "overrideQosPolicy": true, + "overrideVirtualSwitchConfiguration": false, + "qosPolicyOverrides": { + "bandwidthPercentage_SMB": "50", + "priorityValue8021Action_Cluster": "7", + "priorityValue8021Action_SMB": "3" + }, + "trafficType": [ + "Storage" + ], + "virtualSwitchConfigurationOverrides": { + "enableIov": "true", + "loadBalancingAlgorithm": "Dynamic" + } + } + ], + "startingIPAddress": "172.20.0.2", + "storageConnectivitySwitchless": false, + "storageNetworks": [ + { + "adapterName": "smb0", + "vlan": "711" + }, + { + "adapterName": "smb1", + "vlan": "712" + } + ], + "subnetMask": "255.255.255.0" + } + } + } +} +``` + +
+

+ +

+ +via Bicep parameters file + +```bicep-params +using 'br/public:avm/res/azure-stack-hci/cluster:' + +// Required parameters +param name = '' +// Non-required parameters +param deploymentSettings = { + clusterNodeNames: '' + clusterWitnessStorageAccountName: '' + customLocationName: 'ashc2nmin-location' + defaultGateway: '172.20.0.1' + deploymentPrefix: '' + dnsServers: [ + '172.20.0.1' + ] + domainFqdn: 'hci.local' + domainOUPath: '' + enableStorageAutoIp: true + endingIPAddress: '172.20.0.7' + keyVaultName: '' + networkIntents: [ + { + adapter: [ + 'mgmt' + ] + adapterPropertyOverrides: { + jumboPacket: '9014' + networkDirect: 'Disabled' + networkDirectTechnology: 'iWARP' + } + name: 'management' + overrideAdapterProperty: true + overrideQosPolicy: false + overrideVirtualSwitchConfiguration: false + qosPolicyOverrides: { + bandwidthPercentage_SMB: '50' + priorityValue8021Action_Cluster: '7' + priorityValue8021Action_SMB: '3' + } + trafficType: [ + 'Management' + ] + virtualSwitchConfigurationOverrides: { + enableIov: 'true' + loadBalancingAlgorithm: 'Dynamic' + } + } + { + adapter: [ + 'comp0' + 'comp1' + ] + adapterPropertyOverrides: { + jumboPacket: '9014' + networkDirect: 'Disabled' + networkDirectTechnology: 'iWARP' + } + name: 'compute' + overrideAdapterProperty: true + overrideQosPolicy: false + overrideVirtualSwitchConfiguration: false + qosPolicyOverrides: { + bandwidthPercentage_SMB: '50' + priorityValue8021Action_Cluster: '7' + priorityValue8021Action_SMB: '3' + } + trafficType: [ + 'Compute' + ] + virtualSwitchConfigurationOverrides: { + enableIov: 'true' + loadBalancingAlgorithm: 'Dynamic' + } + } + { + adapter: [ + 'smb0' + 'smb1' + ] + adapterPropertyOverrides: { + jumboPacket: '9014' + networkDirect: 'Disabled' + networkDirectTechnology: 'iWARP' + } + name: 'storage' + overrideAdapterProperty: true + overrideQosPolicy: true + overrideVirtualSwitchConfiguration: false + qosPolicyOverrides: { + bandwidthPercentage_SMB: '50' + priorityValue8021Action_Cluster: '7' + priorityValue8021Action_SMB: '3' + } + trafficType: [ + 'Storage' + ] + virtualSwitchConfigurationOverrides: { + enableIov: 'true' + loadBalancingAlgorithm: 'Dynamic' + } + } + ] + startingIPAddress: '172.20.0.2' + storageConnectivitySwitchless: false + storageNetworks: [ + { + adapterName: 'smb0' + vlan: '711' + } + { + adapterName: 'smb1' + vlan: '712' + } + ] + subnetMask: '255.255.255.0' +} +``` + +
+

+ +### Example 2: _Deploy Azure Stack HCI Cluster in Azure with a 2 node switched configuration WAF aligned_ + +This test deploys an Azure VM to host a 2 node switched Azure Stack HCI cluster, validates the cluster configuration, and then deploys the cluster WAF aligned. + + +

+ +via Bicep module + +```bicep +module cluster 'br/public:avm/res/azure-stack-hci/cluster:' = { + name: 'clusterDeployment' + params: { + // Required parameters + name: '' + // Non-required parameters + deploymentSettings: { + bitlockerBootVolume: true + bitlockerDataVolumes: true + clusterNodeNames: '' + clusterWitnessStorageAccountName: '' + customLocationName: 'ashc2nwaf-location' + defaultGateway: '172.20.0.1' + deploymentPrefix: '' + dnsServers: [ + '172.20.0.1' + ] + domainFqdn: 'hci.local' + domainOUPath: '' + driftControlEnforced: true + enableStorageAutoIp: true + endingIPAddress: '172.20.0.7' + keyVaultName: '' + networkIntents: [ + { + adapter: [ + 'mgmt' + ] + adapterPropertyOverrides: { + jumboPacket: '9014' + networkDirect: 'Disabled' + networkDirectTechnology: 'iWARP' + } + name: 'management' + overrideAdapterProperty: true + overrideQosPolicy: false + overrideVirtualSwitchConfiguration: false + qosPolicyOverrides: { + bandwidthPercentage_SMB: '50' + priorityValue8021Action_Cluster: '7' + priorityValue8021Action_SMB: '3' + } + trafficType: [ + 'Management' + ] + virtualSwitchConfigurationOverrides: { + enableIov: 'true' + loadBalancingAlgorithm: 'Dynamic' + } + } + { + adapter: [ + 'comp0' + 'comp1' + ] + adapterPropertyOverrides: { + jumboPacket: '9014' + networkDirect: 'Disabled' + networkDirectTechnology: 'iWARP' + } + name: 'compute' + overrideAdapterProperty: true + overrideQosPolicy: false + overrideVirtualSwitchConfiguration: false + qosPolicyOverrides: { + bandwidthPercentage_SMB: '50' + priorityValue8021Action_Cluster: '7' + priorityValue8021Action_SMB: '3' + } + trafficType: [ + 'Compute' + ] + virtualSwitchConfigurationOverrides: { + enableIov: 'true' + loadBalancingAlgorithm: 'Dynamic' + } + } + { + adapter: [ + 'smb0' + 'smb1' + ] + adapterPropertyOverrides: { + jumboPacket: '9014' + networkDirect: 'Disabled' + networkDirectTechnology: 'iWARP' + } + name: 'storage' + overrideAdapterProperty: true + overrideQosPolicy: true + overrideVirtualSwitchConfiguration: false + qosPolicyOverrides: { + bandwidthPercentage_SMB: '50' + priorityValue8021Action_Cluster: '7' + priorityValue8021Action_SMB: '3' + } + trafficType: [ + 'Storage' + ] + virtualSwitchConfigurationOverrides: { + enableIov: 'true' + loadBalancingAlgorithm: 'Dynamic' + } + } + ] + sideChannelMitigationEnforced: true + smbClusterEncryption: true + smbSigningEnforced: true + startingIPAddress: '172.20.0.2' + storageConnectivitySwitchless: false + storageNetworks: [ + { + adapterName: 'smb0' + vlan: '711' + } + { + adapterName: 'smb1' + vlan: '712' + } + ] + subnetMask: '255.255.255.0' + } + tags: { + Environment: 'Non-Prod' + 'hidden-title': 'This is visible in the resource name' + Role: 'DeploymentValidation' + } + } +} +``` + +
+

+ +

+ +via JSON parameters file + +```json +{ + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#", + "contentVersion": "1.0.0.0", + "parameters": { + // Required parameters + "name": { + "value": "" + }, + // Non-required parameters + "deploymentSettings": { + "value": { + "bitlockerBootVolume": true, + "bitlockerDataVolumes": true, + "clusterNodeNames": "", + "clusterWitnessStorageAccountName": "", + "customLocationName": "ashc2nwaf-location", + "defaultGateway": "172.20.0.1", + "deploymentPrefix": "", + "dnsServers": [ + "172.20.0.1" + ], + "domainFqdn": "hci.local", + "domainOUPath": "", + "driftControlEnforced": true, + "enableStorageAutoIp": true, + "endingIPAddress": "172.20.0.7", + "keyVaultName": "", + "networkIntents": [ + { + "adapter": [ + "mgmt" + ], + "adapterPropertyOverrides": { + "jumboPacket": "9014", + "networkDirect": "Disabled", + "networkDirectTechnology": "iWARP" + }, + "name": "management", + "overrideAdapterProperty": true, + "overrideQosPolicy": false, + "overrideVirtualSwitchConfiguration": false, + "qosPolicyOverrides": { + "bandwidthPercentage_SMB": "50", + "priorityValue8021Action_Cluster": "7", + "priorityValue8021Action_SMB": "3" + }, + "trafficType": [ + "Management" + ], + "virtualSwitchConfigurationOverrides": { + "enableIov": "true", + "loadBalancingAlgorithm": "Dynamic" + } + }, + { + "adapter": [ + "comp0", + "comp1" + ], + "adapterPropertyOverrides": { + "jumboPacket": "9014", + "networkDirect": "Disabled", + "networkDirectTechnology": "iWARP" + }, + "name": "compute", + "overrideAdapterProperty": true, + "overrideQosPolicy": false, + "overrideVirtualSwitchConfiguration": false, + "qosPolicyOverrides": { + "bandwidthPercentage_SMB": "50", + "priorityValue8021Action_Cluster": "7", + "priorityValue8021Action_SMB": "3" + }, + "trafficType": [ + "Compute" + ], + "virtualSwitchConfigurationOverrides": { + "enableIov": "true", + "loadBalancingAlgorithm": "Dynamic" + } + }, + { + "adapter": [ + "smb0", + "smb1" + ], + "adapterPropertyOverrides": { + "jumboPacket": "9014", + "networkDirect": "Disabled", + "networkDirectTechnology": "iWARP" + }, + "name": "storage", + "overrideAdapterProperty": true, + "overrideQosPolicy": true, + "overrideVirtualSwitchConfiguration": false, + "qosPolicyOverrides": { + "bandwidthPercentage_SMB": "50", + "priorityValue8021Action_Cluster": "7", + "priorityValue8021Action_SMB": "3" + }, + "trafficType": [ + "Storage" + ], + "virtualSwitchConfigurationOverrides": { + "enableIov": "true", + "loadBalancingAlgorithm": "Dynamic" + } + } + ], + "sideChannelMitigationEnforced": true, + "smbClusterEncryption": true, + "smbSigningEnforced": true, + "startingIPAddress": "172.20.0.2", + "storageConnectivitySwitchless": false, + "storageNetworks": [ + { + "adapterName": "smb0", + "vlan": "711" + }, + { + "adapterName": "smb1", + "vlan": "712" + } + ], + "subnetMask": "255.255.255.0" + } + }, + "tags": { + "value": { + "Environment": "Non-Prod", + "hidden-title": "This is visible in the resource name", + "Role": "DeploymentValidation" + } + } + } +} +``` + +
+

+ +

+ +via Bicep parameters file + +```bicep-params +using 'br/public:avm/res/azure-stack-hci/cluster:' + +// Required parameters +param name = '' +// Non-required parameters +param deploymentSettings = { + bitlockerBootVolume: true + bitlockerDataVolumes: true + clusterNodeNames: '' + clusterWitnessStorageAccountName: '' + customLocationName: 'ashc2nwaf-location' + defaultGateway: '172.20.0.1' + deploymentPrefix: '' + dnsServers: [ + '172.20.0.1' + ] + domainFqdn: 'hci.local' + domainOUPath: '' + driftControlEnforced: true + enableStorageAutoIp: true + endingIPAddress: '172.20.0.7' + keyVaultName: '' + networkIntents: [ + { + adapter: [ + 'mgmt' + ] + adapterPropertyOverrides: { + jumboPacket: '9014' + networkDirect: 'Disabled' + networkDirectTechnology: 'iWARP' + } + name: 'management' + overrideAdapterProperty: true + overrideQosPolicy: false + overrideVirtualSwitchConfiguration: false + qosPolicyOverrides: { + bandwidthPercentage_SMB: '50' + priorityValue8021Action_Cluster: '7' + priorityValue8021Action_SMB: '3' + } + trafficType: [ + 'Management' + ] + virtualSwitchConfigurationOverrides: { + enableIov: 'true' + loadBalancingAlgorithm: 'Dynamic' + } + } + { + adapter: [ + 'comp0' + 'comp1' + ] + adapterPropertyOverrides: { + jumboPacket: '9014' + networkDirect: 'Disabled' + networkDirectTechnology: 'iWARP' + } + name: 'compute' + overrideAdapterProperty: true + overrideQosPolicy: false + overrideVirtualSwitchConfiguration: false + qosPolicyOverrides: { + bandwidthPercentage_SMB: '50' + priorityValue8021Action_Cluster: '7' + priorityValue8021Action_SMB: '3' + } + trafficType: [ + 'Compute' + ] + virtualSwitchConfigurationOverrides: { + enableIov: 'true' + loadBalancingAlgorithm: 'Dynamic' + } + } + { + adapter: [ + 'smb0' + 'smb1' + ] + adapterPropertyOverrides: { + jumboPacket: '9014' + networkDirect: 'Disabled' + networkDirectTechnology: 'iWARP' + } + name: 'storage' + overrideAdapterProperty: true + overrideQosPolicy: true + overrideVirtualSwitchConfiguration: false + qosPolicyOverrides: { + bandwidthPercentage_SMB: '50' + priorityValue8021Action_Cluster: '7' + priorityValue8021Action_SMB: '3' + } + trafficType: [ + 'Storage' + ] + virtualSwitchConfigurationOverrides: { + enableIov: 'true' + loadBalancingAlgorithm: 'Dynamic' + } + } + ] + sideChannelMitigationEnforced: true + smbClusterEncryption: true + smbSigningEnforced: true + startingIPAddress: '172.20.0.2' + storageConnectivitySwitchless: false + storageNetworks: [ + { + adapterName: 'smb0' + vlan: '711' + } + { + adapterName: 'smb1' + vlan: '712' + } + ] + subnetMask: '255.255.255.0' +} +param tags = { + Environment: 'Non-Prod' + 'hidden-title': 'This is visible in the resource name' + Role: 'DeploymentValidation' +} +``` + +
+

+ +## Parameters + +**Required parameters** + +| Parameter | Type | Description | +| :-- | :-- | :-- | +| [`name`](#parameter-name) | string | The name of the Azure Stack HCI cluster - this must be a valid Active Directory computer name and will be the name of your cluster in Azure. | + +**Optional parameters** + +| Parameter | Type | Description | +| :-- | :-- | :-- | +| [`deploymentOperations`](#parameter-deploymentoperations) | array | The cluster deployment operations to execute. Defaults to "[Validate, Deploy]". | +| [`deploymentSettings`](#parameter-deploymentsettings) | object | The deployment settings of the cluster. | +| [`enableTelemetry`](#parameter-enabletelemetry) | bool | Enable/Disable usage telemetry for module. | +| [`location`](#parameter-location) | string | Location for all resources. | +| [`roleAssignments`](#parameter-roleassignments) | array | Array of role assignments to create. | +| [`tags`](#parameter-tags) | object | Tags of the resource. | +| [`useSharedKeyVault`](#parameter-usesharedkeyvault) | bool | Specify whether to use the shared key vault for the HCI cluster. | + +### Parameter: `name` + +The name of the Azure Stack HCI cluster - this must be a valid Active Directory computer name and will be the name of your cluster in Azure. + +- Required: Yes +- Type: string + +### Parameter: `deploymentOperations` + +The cluster deployment operations to execute. Defaults to "[Validate, Deploy]". + +- Required: No +- Type: array +- Default: + ```Bicep + [ + 'Deploy' + 'Validate' + ] + ``` +- Allowed: + ```Bicep + [ + 'Deploy' + 'Validate' + ] + ``` + +### Parameter: `deploymentSettings` + +The deployment settings of the cluster. + +- Required: No +- Type: object + +**Required parameters** + +| Parameter | Type | Description | +| :-- | :-- | :-- | +| [`clusterNodeNames`](#parameter-deploymentsettingsclusternodenames) | array | Names of the cluster node Arc Machine resources. These are the name of the Arc Machine resources created when the new HCI nodes were Arc initialized. Example: [hci-node-1, hci-node-2]. | +| [`clusterWitnessStorageAccountName`](#parameter-deploymentsettingsclusterwitnessstorageaccountname) | string | The name of the storage account to be used as the witness for the HCI Windows Failover Cluster. | +| [`customLocationName`](#parameter-deploymentsettingscustomlocationname) | string | The name of the Custom Location associated with the Arc Resource Bridge for this cluster. This value should reflect the physical location and identifier of the HCI cluster. Example: cl-hci-den-clu01. | +| [`defaultGateway`](#parameter-deploymentsettingsdefaultgateway) | string | The default gateway of the Management Network. Example: 192.168.0.1. | +| [`deploymentPrefix`](#parameter-deploymentsettingsdeploymentprefix) | string | The prefix for the resource for the deployment. This value is used in key vault and storage account names in this template, as well as for the deploymentSettings.properties.deploymentConfiguration.scaleUnits.deploymentData.namingPrefix property which requires regex pattern: ^[a-zA-Z0-9-]{1,8}$. | +| [`dnsServers`](#parameter-deploymentsettingsdnsservers) | array | The DNS servers accessible from the Management Network for the HCI cluster. | +| [`domainFqdn`](#parameter-deploymentsettingsdomainfqdn) | string | The domain name of the Active Directory Domain Services. Example: "contoso.com". | +| [`domainOUPath`](#parameter-deploymentsettingsdomainoupath) | string | The ADDS OU path - ex "OU=HCI,DC=contoso,DC=com". | +| [`endingIPAddress`](#parameter-deploymentsettingsendingipaddress) | string | The ending IP address for the Infrastructure Network IP pool. There must be at least 6 IPs between startingIPAddress and endingIPAddress and this pool should be not include the node IPs. | +| [`keyVaultName`](#parameter-deploymentsettingskeyvaultname) | string | The name of the key vault to be used for storing secrets for the HCI cluster. This currently needs to be unique per HCI cluster. | +| [`networkIntents`](#parameter-deploymentsettingsnetworkintents) | array | An array of Network ATC Network Intent objects that define the Compute, Management, and Storage network configuration for the cluster. | +| [`startingIPAddress`](#parameter-deploymentsettingsstartingipaddress) | string | The starting IP address for the Infrastructure Network IP pool. There must be at least 6 IPs between startingIPAddress and endingIPAddress and this pool should be not include the node IPs. | +| [`storageConnectivitySwitchless`](#parameter-deploymentsettingsstorageconnectivityswitchless) | bool | Specify whether the Storage Network connectivity is switched or switchless. | +| [`storageNetworks`](#parameter-deploymentsettingsstoragenetworks) | array | An array of JSON objects that define the storage network configuration for the cluster. Each object should contain the adapterName, VLAN properties, and (optionally) IP configurations. | +| [`subnetMask`](#parameter-deploymentsettingssubnetmask) | string | The subnet mask pf the Management Network for the HCI cluster - ex: 255.255.252.0. | + +**Optional parameters** + +| Parameter | Type | Description | +| :-- | :-- | :-- | +| [`bitlockerBootVolume`](#parameter-deploymentsettingsbitlockerbootvolume) | bool | When set to true, BitLocker XTS_AES 256-bit encryption is enabled for all data-at-rest on the OS volume of your Azure Stack HCI cluster. This setting is TPM-hardware dependent. | +| [`bitlockerDataVolumes`](#parameter-deploymentsettingsbitlockerdatavolumes) | bool | When set to true, BitLocker XTS-AES 256-bit encryption is enabled for all data-at-rest on your Azure Stack HCI cluster shared volumes. | +| [`cloudId`](#parameter-deploymentsettingscloudid) | string | If using a shared key vault or non-legacy secret naming, pass the properties.cloudId guid from the pre-created HCI cluster resource. | +| [`credentialGuardEnforced`](#parameter-deploymentsettingscredentialguardenforced) | bool | Enables the Credential Guard. | +| [`driftControlEnforced`](#parameter-deploymentsettingsdriftcontrolenforced) | bool | When set to true, the security baseline is re-applied regularly. | +| [`drtmProtection`](#parameter-deploymentsettingsdrtmprotection) | bool | The hardware-dependent Secure Boot setting. | +| [`enableStorageAutoIp`](#parameter-deploymentsettingsenablestorageautoip) | bool | Enable storage auto IP assignment. This should be true for most deployments except when deploying a three-node switchless cluster, in which case storage IPs should be configured before deployment and this value set to false. | +| [`episodicDataUpload`](#parameter-deploymentsettingsepisodicdataupload) | bool | The diagnostic data for deploying a HCI cluster. | +| [`hvciProtection`](#parameter-deploymentsettingshvciprotection) | bool | The Hypervisor-protected Code Integrity setting. | +| [`isEuropeanUnionLocation`](#parameter-deploymentsettingsiseuropeanunionlocation) | bool | The location data for deploying a HCI cluster. | +| [`sideChannelMitigationEnforced`](#parameter-deploymentsettingssidechannelmitigationenforced) | bool | When set to true, all the side channel mitigations are enabled. | +| [`smbClusterEncryption`](#parameter-deploymentsettingssmbclusterencryption) | bool | When set to true, cluster east-west traffic is encrypted. | +| [`smbSigningEnforced`](#parameter-deploymentsettingssmbsigningenforced) | bool | When set to true, the SMB default instance requires sign in for the client and server services. | +| [`storageConfigurationMode`](#parameter-deploymentsettingsstorageconfigurationmode) | string | The storage volume configuration mode. See documentation for details. | +| [`streamingDataClient`](#parameter-deploymentsettingsstreamingdataclient) | bool | The metrics data for deploying a HCI cluster. | +| [`wdacEnforced`](#parameter-deploymentsettingswdacenforced) | bool | Limits the applications and the code that you can run on your Azure Stack HCI cluster. | + +### Parameter: `deploymentSettings.clusterNodeNames` + +Names of the cluster node Arc Machine resources. These are the name of the Arc Machine resources created when the new HCI nodes were Arc initialized. Example: [hci-node-1, hci-node-2]. + +- Required: Yes +- Type: array + +### Parameter: `deploymentSettings.clusterWitnessStorageAccountName` + +The name of the storage account to be used as the witness for the HCI Windows Failover Cluster. + +- Required: Yes +- Type: string + +### Parameter: `deploymentSettings.customLocationName` + +The name of the Custom Location associated with the Arc Resource Bridge for this cluster. This value should reflect the physical location and identifier of the HCI cluster. Example: cl-hci-den-clu01. + +- Required: Yes +- Type: string + +### Parameter: `deploymentSettings.defaultGateway` + +The default gateway of the Management Network. Example: 192.168.0.1. + +- Required: Yes +- Type: string + +### Parameter: `deploymentSettings.deploymentPrefix` + +The prefix for the resource for the deployment. This value is used in key vault and storage account names in this template, as well as for the deploymentSettings.properties.deploymentConfiguration.scaleUnits.deploymentData.namingPrefix property which requires regex pattern: ^[a-zA-Z0-9-]{1,8}$. + +- Required: Yes +- Type: string + +### Parameter: `deploymentSettings.dnsServers` + +The DNS servers accessible from the Management Network for the HCI cluster. + +- Required: Yes +- Type: array + +### Parameter: `deploymentSettings.domainFqdn` + +The domain name of the Active Directory Domain Services. Example: "contoso.com". + +- Required: Yes +- Type: string + +### Parameter: `deploymentSettings.domainOUPath` + +The ADDS OU path - ex "OU=HCI,DC=contoso,DC=com". + +- Required: Yes +- Type: string + +### Parameter: `deploymentSettings.endingIPAddress` + +The ending IP address for the Infrastructure Network IP pool. There must be at least 6 IPs between startingIPAddress and endingIPAddress and this pool should be not include the node IPs. + +- Required: Yes +- Type: string + +### Parameter: `deploymentSettings.keyVaultName` + +The name of the key vault to be used for storing secrets for the HCI cluster. This currently needs to be unique per HCI cluster. + +- Required: Yes +- Type: string + +### Parameter: `deploymentSettings.networkIntents` + +An array of Network ATC Network Intent objects that define the Compute, Management, and Storage network configuration for the cluster. + +- Required: Yes +- Type: array + +### Parameter: `deploymentSettings.startingIPAddress` + +The starting IP address for the Infrastructure Network IP pool. There must be at least 6 IPs between startingIPAddress and endingIPAddress and this pool should be not include the node IPs. + +- Required: Yes +- Type: string + +### Parameter: `deploymentSettings.storageConnectivitySwitchless` + +Specify whether the Storage Network connectivity is switched or switchless. + +- Required: Yes +- Type: bool + +### Parameter: `deploymentSettings.storageNetworks` + +An array of JSON objects that define the storage network configuration for the cluster. Each object should contain the adapterName, VLAN properties, and (optionally) IP configurations. + +- Required: Yes +- Type: array + +### Parameter: `deploymentSettings.subnetMask` + +The subnet mask pf the Management Network for the HCI cluster - ex: 255.255.252.0. + +- Required: Yes +- Type: string + +### Parameter: `deploymentSettings.bitlockerBootVolume` + +When set to true, BitLocker XTS_AES 256-bit encryption is enabled for all data-at-rest on the OS volume of your Azure Stack HCI cluster. This setting is TPM-hardware dependent. + +- Required: No +- Type: bool + +### Parameter: `deploymentSettings.bitlockerDataVolumes` + +When set to true, BitLocker XTS-AES 256-bit encryption is enabled for all data-at-rest on your Azure Stack HCI cluster shared volumes. + +- Required: No +- Type: bool + +### Parameter: `deploymentSettings.cloudId` + +If using a shared key vault or non-legacy secret naming, pass the properties.cloudId guid from the pre-created HCI cluster resource. + +- Required: No +- Type: string + +### Parameter: `deploymentSettings.credentialGuardEnforced` + +Enables the Credential Guard. + +- Required: No +- Type: bool + +### Parameter: `deploymentSettings.driftControlEnforced` + +When set to true, the security baseline is re-applied regularly. + +- Required: No +- Type: bool + +### Parameter: `deploymentSettings.drtmProtection` + +The hardware-dependent Secure Boot setting. + +- Required: No +- Type: bool + +### Parameter: `deploymentSettings.enableStorageAutoIp` + +Enable storage auto IP assignment. This should be true for most deployments except when deploying a three-node switchless cluster, in which case storage IPs should be configured before deployment and this value set to false. + +- Required: No +- Type: bool + +### Parameter: `deploymentSettings.episodicDataUpload` + +The diagnostic data for deploying a HCI cluster. + +- Required: No +- Type: bool + +### Parameter: `deploymentSettings.hvciProtection` + +The Hypervisor-protected Code Integrity setting. + +- Required: No +- Type: bool + +### Parameter: `deploymentSettings.isEuropeanUnionLocation` + +The location data for deploying a HCI cluster. + +- Required: No +- Type: bool + +### Parameter: `deploymentSettings.sideChannelMitigationEnforced` + +When set to true, all the side channel mitigations are enabled. + +- Required: No +- Type: bool + +### Parameter: `deploymentSettings.smbClusterEncryption` + +When set to true, cluster east-west traffic is encrypted. + +- Required: No +- Type: bool + +### Parameter: `deploymentSettings.smbSigningEnforced` + +When set to true, the SMB default instance requires sign in for the client and server services. + +- Required: No +- Type: bool + +### Parameter: `deploymentSettings.storageConfigurationMode` + +The storage volume configuration mode. See documentation for details. + +- Required: No +- Type: string +- Allowed: + ```Bicep + [ + 'Express' + 'InfraOnly' + 'KeepStorage' + ] + ``` + +### Parameter: `deploymentSettings.streamingDataClient` + +The metrics data for deploying a HCI cluster. + +- Required: No +- Type: bool + +### Parameter: `deploymentSettings.wdacEnforced` + +Limits the applications and the code that you can run on your Azure Stack HCI cluster. + +- Required: No +- Type: bool + +### Parameter: `enableTelemetry` + +Enable/Disable usage telemetry for module. + +- Required: No +- Type: bool +- Default: `True` + +### Parameter: `location` + +Location for all resources. + +- Required: No +- Type: string +- Default: `[resourceGroup().location]` + +### Parameter: `roleAssignments` + +Array of role assignments to create. + +- Required: No +- Type: array +- Roles configurable by name: + - `'Contributor'` + - `'Owner'` + - `'Reader'` + - `'Role Based Access Control Administrator'` + - `'User Access Administrator'` + - `'Azure Stack HCI Administrator'` + - `'Windows Admin Center Administrator Login'` + +**Required parameters** + +| Parameter | Type | Description | +| :-- | :-- | :-- | +| [`principalId`](#parameter-roleassignmentsprincipalid) | string | The principal ID of the principal (user/group/identity) to assign the role to. | +| [`roleDefinitionIdOrName`](#parameter-roleassignmentsroledefinitionidorname) | string | The role to assign. You can provide either the display name of the role definition, the role definition GUID, or its fully qualified ID in the following format: '/providers/Microsoft.Authorization/roleDefinitions/c2f4ef07-c644-48eb-af81-4b1b4947fb11'. | + +**Optional parameters** + +| Parameter | Type | Description | +| :-- | :-- | :-- | +| [`condition`](#parameter-roleassignmentscondition) | string | The conditions on the role assignment. This limits the resources it can be assigned to. e.g.: @Resource[Microsoft.Storage/storageAccounts/blobServices/containers:ContainerName] StringEqualsIgnoreCase "foo_storage_container". | +| [`conditionVersion`](#parameter-roleassignmentsconditionversion) | string | Version of the condition. | +| [`delegatedManagedIdentityResourceId`](#parameter-roleassignmentsdelegatedmanagedidentityresourceid) | string | The Resource Id of the delegated managed identity resource. | +| [`description`](#parameter-roleassignmentsdescription) | string | The description of the role assignment. | +| [`name`](#parameter-roleassignmentsname) | string | The name (as GUID) of the role assignment. If not provided, a GUID will be generated. | +| [`principalType`](#parameter-roleassignmentsprincipaltype) | string | The principal type of the assigned principal ID. | + +### Parameter: `roleAssignments.principalId` + +The principal ID of the principal (user/group/identity) to assign the role to. + +- Required: Yes +- Type: string + +### Parameter: `roleAssignments.roleDefinitionIdOrName` + +The role to assign. You can provide either the display name of the role definition, the role definition GUID, or its fully qualified ID in the following format: '/providers/Microsoft.Authorization/roleDefinitions/c2f4ef07-c644-48eb-af81-4b1b4947fb11'. + +- Required: Yes +- Type: string + +### Parameter: `roleAssignments.condition` + +The conditions on the role assignment. This limits the resources it can be assigned to. e.g.: @Resource[Microsoft.Storage/storageAccounts/blobServices/containers:ContainerName] StringEqualsIgnoreCase "foo_storage_container". + +- Required: No +- Type: string + +### Parameter: `roleAssignments.conditionVersion` + +Version of the condition. + +- Required: No +- Type: string +- Allowed: + ```Bicep + [ + '2.0' + ] + ``` + +### Parameter: `roleAssignments.delegatedManagedIdentityResourceId` + +The Resource Id of the delegated managed identity resource. + +- Required: No +- Type: string + +### Parameter: `roleAssignments.description` + +The description of the role assignment. + +- Required: No +- Type: string + +### Parameter: `roleAssignments.name` + +The name (as GUID) of the role assignment. If not provided, a GUID will be generated. + +- Required: No +- Type: string + +### Parameter: `roleAssignments.principalType` + +The principal type of the assigned principal ID. + +- Required: No +- Type: string +- Allowed: + ```Bicep + [ + 'Device' + 'ForeignGroup' + 'Group' + 'ServicePrincipal' + 'User' + ] + ``` + +### Parameter: `tags` + +Tags of the resource. + +- Required: No +- Type: object + +### Parameter: `useSharedKeyVault` + +Specify whether to use the shared key vault for the HCI cluster. + +- Required: No +- Type: bool +- Default: `True` + +## Outputs + +| Output | Type | Description | +| :-- | :-- | :-- | +| `location` | string | The location of the cluster. | +| `name` | string | The name of the cluster. | +| `resourceGroupName` | string | The resource group of the cluster. | +| `resourceId` | string | The ID of the cluster. | +| `systemAssignedMIPrincipalId` | string | The managed identity of the cluster. | + +## Cross-referenced modules + +This section gives you an overview of all local-referenced module files (i.e., other modules that are referenced in this module) and all remote-referenced files (i.e., Bicep modules that are referenced from a Bicep Registry or Template Specs). + +| Reference | Type | +| :-- | :-- | +| `br/public:avm/utl/types/avm-common-types:0.5.1` | Remote reference | + +## Data Collection + +The software may collect information about you and your use of the software and send it to Microsoft. Microsoft may use this information to provide services and improve our products and services. You may turn off the telemetry as described in the [repository](https://aka.ms/avm/telemetry). There are also some features in the software that may enable you and Microsoft to collect data from users of your applications. If you use these features, you must comply with applicable law, including providing appropriate notices to users of your applications together with a copy of Microsoft’s privacy statement. Our privacy statement is located at . You can learn more about data collection and use in the help documentation and our privacy statement. Your use of the software operates as your consent to these practices. diff --git a/avm/res/azure-stack-hci/cluster/deployment-setting/README.md b/avm/res/azure-stack-hci/cluster/deployment-setting/README.md new file mode 100644 index 0000000000..fa18c97ce3 --- /dev/null +++ b/avm/res/azure-stack-hci/cluster/deployment-setting/README.md @@ -0,0 +1,349 @@ +# Azure Stack HCI Cluster Deployment Settings `[Microsoft.AzureStackHCI/clusters/deploymentSettings]` + +This module deploys an Azure Stack HCI Cluster Deployment Settings resource. + +## Navigation + +- [Resource Types](#Resource-Types) +- [Parameters](#Parameters) +- [Outputs](#Outputs) + +## Resource Types + +| Resource Type | API Version | +| :-- | :-- | +| `Microsoft.AzureStackHCI/clusters/deploymentSettings` | [2024-04-01](https://learn.microsoft.com/en-us/azure/templates/Microsoft.AzureStackHCI/clusters/deploymentSettings) | + +## Parameters + +**Required parameters** + +| Parameter | Type | Description | +| :-- | :-- | :-- | +| [`clusterNodeNames`](#parameter-clusternodenames) | array | Names of the cluster node Arc Machine resources. These are the name of the Arc Machine resources created when the new HCI nodes were Arc initialized. Example: [hci-node-1, hci-node-2]. | +| [`clusterWitnessStorageAccountName`](#parameter-clusterwitnessstorageaccountname) | string | The name of the storage account to be used as the witness for the HCI Windows Failover Cluster. | +| [`customLocationName`](#parameter-customlocationname) | string | The name of the Custom Location associated with the Arc Resource Bridge for this cluster. This value should reflect the physical location and identifier of the HCI cluster. Example: cl-hci-den-clu01. | +| [`defaultGateway`](#parameter-defaultgateway) | string | The default gateway of the Management Network. Example: 192.168.0.1. | +| [`deploymentMode`](#parameter-deploymentmode) | string | First must pass with this parameter set to Validate prior running with it set to Deploy. If either Validation or Deployment phases fail, fix the issue, then resubmit the template with the same deploymentMode to retry. | +| [`deploymentPrefix`](#parameter-deploymentprefix) | string | The prefix for the resource for the deployment. This value is used in key vault and storage account names in this template, as well as for the deploymentSettings.properties.deploymentConfiguration.scaleUnits.deploymentData.namingPrefix property which requires regex pattern: ^[a-zA-Z0-9-]{1,8}$. | +| [`dnsServers`](#parameter-dnsservers) | array | The DNS servers accessible from the Management Network for the HCI cluster. | +| [`domainFqdn`](#parameter-domainfqdn) | string | The domain name of the Active Directory Domain Services. Example: "contoso.com". | +| [`domainOUPath`](#parameter-domainoupath) | string | The ADDS OU path - ex "OU=HCI,DC=contoso,DC=com". | +| [`endingIPAddress`](#parameter-endingipaddress) | string | The ending IP address for the Infrastructure Network IP pool. There must be at least 6 IPs between startingIPAddress and endingIPAddress and this pool should be not include the node IPs. | +| [`keyVaultName`](#parameter-keyvaultname) | string | The name of the key vault to be used for storing secrets for the HCI cluster. This currently needs to be unique per HCI cluster. | +| [`networkIntents`](#parameter-networkintents) | array | An array of Network ATC Network Intent objects that define the Compute, Management, and Storage network configuration for the cluster. | +| [`startingIPAddress`](#parameter-startingipaddress) | string | The starting IP address for the Infrastructure Network IP pool. There must be at least 6 IPs between startingIPAddress and endingIPAddress and this pool should be not include the node IPs. | +| [`storageConnectivitySwitchless`](#parameter-storageconnectivityswitchless) | bool | Specify whether the Storage Network connectivity is switched or switchless. | +| [`storageNetworks`](#parameter-storagenetworks) | array | An array of JSON objects that define the storage network configuration for the cluster. Each object should contain the adapterName, VLAN properties, and (optionally) IP configurations. | +| [`subnetMask`](#parameter-subnetmask) | string | The subnet mask pf the Management Network for the HCI cluster - ex: 255.255.252.0. | + +**Conditional parameters** + +| Parameter | Type | Description | +| :-- | :-- | :-- | +| [`clusterName`](#parameter-clustername) | string | The name of the Azure Stack HCI cluster - this must be a valid Active Directory computer name and will be the name of your cluster in Azure. Required if the template is used in a standalone deployment. | + +**Optional parameters** + +| Parameter | Type | Description | +| :-- | :-- | :-- | +| [`bitlockerBootVolume`](#parameter-bitlockerbootvolume) | bool | When set to true, BitLocker XTS_AES 256-bit encryption is enabled for all data-at-rest on the OS volume of your Azure Stack HCI cluster. This setting is TPM-hardware dependent. | +| [`bitlockerDataVolumes`](#parameter-bitlockerdatavolumes) | bool | When set to true, BitLocker XTS-AES 256-bit encryption is enabled for all data-at-rest on your Azure Stack HCI cluster shared volumes. | +| [`cloudId`](#parameter-cloudid) | string | If using a shared key vault or non-legacy secret naming, pass the properties.cloudId guid from the pre-created HCI cluster resource. | +| [`credentialGuardEnforced`](#parameter-credentialguardenforced) | bool | Enables the Credential Guard. | +| [`driftControlEnforced`](#parameter-driftcontrolenforced) | bool | When set to true, the security baseline is re-applied regularly. | +| [`drtmProtection`](#parameter-drtmprotection) | bool | The hardware-dependent Secure Boot setting. | +| [`enableStorageAutoIp`](#parameter-enablestorageautoip) | bool | Enable storage auto IP assignment. This should be true for most deployments except when deploying a three-node switchless cluster, in which case storage IPs should be configured before deployment and this value set to false. | +| [`episodicDataUpload`](#parameter-episodicdataupload) | bool | The diagnostic data for deploying a HCI cluster. | +| [`hvciProtection`](#parameter-hvciprotection) | bool | The Hypervisor-protected Code Integrity setting. | +| [`isEuropeanUnionLocation`](#parameter-iseuropeanunionlocation) | bool | The location data for deploying a HCI cluster. | +| [`name`](#parameter-name) | string | The name of the deployment settings. | +| [`sideChannelMitigationEnforced`](#parameter-sidechannelmitigationenforced) | bool | When set to true, all the side channel mitigations are enabled. | +| [`smbClusterEncryption`](#parameter-smbclusterencryption) | bool | When set to true, cluster east-west traffic is encrypted. | +| [`smbSigningEnforced`](#parameter-smbsigningenforced) | bool | When set to true, the SMB default instance requires sign in for the client and server services. | +| [`storageConfigurationMode`](#parameter-storageconfigurationmode) | string | The storage volume configuration mode. See documentation for details. | +| [`streamingDataClient`](#parameter-streamingdataclient) | bool | The metrics data for deploying a HCI cluster. | +| [`wdacEnforced`](#parameter-wdacenforced) | bool | Limits the applications and the code that you can run on your Azure Stack HCI cluster. | + +### Parameter: `clusterNodeNames` + +Names of the cluster node Arc Machine resources. These are the name of the Arc Machine resources created when the new HCI nodes were Arc initialized. Example: [hci-node-1, hci-node-2]. + +- Required: Yes +- Type: array + +### Parameter: `clusterWitnessStorageAccountName` + +The name of the storage account to be used as the witness for the HCI Windows Failover Cluster. + +- Required: Yes +- Type: string + +### Parameter: `customLocationName` + +The name of the Custom Location associated with the Arc Resource Bridge for this cluster. This value should reflect the physical location and identifier of the HCI cluster. Example: cl-hci-den-clu01. + +- Required: Yes +- Type: string + +### Parameter: `defaultGateway` + +The default gateway of the Management Network. Example: 192.168.0.1. + +- Required: Yes +- Type: string + +### Parameter: `deploymentMode` + +First must pass with this parameter set to Validate prior running with it set to Deploy. If either Validation or Deployment phases fail, fix the issue, then resubmit the template with the same deploymentMode to retry. + +- Required: Yes +- Type: string +- Allowed: + ```Bicep + [ + 'Deploy' + 'Validate' + ] + ``` + +### Parameter: `deploymentPrefix` + +The prefix for the resource for the deployment. This value is used in key vault and storage account names in this template, as well as for the deploymentSettings.properties.deploymentConfiguration.scaleUnits.deploymentData.namingPrefix property which requires regex pattern: ^[a-zA-Z0-9-]{1,8}$. + +- Required: Yes +- Type: string + +### Parameter: `dnsServers` + +The DNS servers accessible from the Management Network for the HCI cluster. + +- Required: Yes +- Type: array + +### Parameter: `domainFqdn` + +The domain name of the Active Directory Domain Services. Example: "contoso.com". + +- Required: Yes +- Type: string + +### Parameter: `domainOUPath` + +The ADDS OU path - ex "OU=HCI,DC=contoso,DC=com". + +- Required: Yes +- Type: string + +### Parameter: `endingIPAddress` + +The ending IP address for the Infrastructure Network IP pool. There must be at least 6 IPs between startingIPAddress and endingIPAddress and this pool should be not include the node IPs. + +- Required: Yes +- Type: string + +### Parameter: `keyVaultName` + +The name of the key vault to be used for storing secrets for the HCI cluster. This currently needs to be unique per HCI cluster. + +- Required: Yes +- Type: string + +### Parameter: `networkIntents` + +An array of Network ATC Network Intent objects that define the Compute, Management, and Storage network configuration for the cluster. + +- Required: Yes +- Type: array + +### Parameter: `startingIPAddress` + +The starting IP address for the Infrastructure Network IP pool. There must be at least 6 IPs between startingIPAddress and endingIPAddress and this pool should be not include the node IPs. + +- Required: Yes +- Type: string + +### Parameter: `storageConnectivitySwitchless` + +Specify whether the Storage Network connectivity is switched or switchless. + +- Required: Yes +- Type: bool + +### Parameter: `storageNetworks` + +An array of JSON objects that define the storage network configuration for the cluster. Each object should contain the adapterName, VLAN properties, and (optionally) IP configurations. + +- Required: Yes +- Type: array + +### Parameter: `subnetMask` + +The subnet mask pf the Management Network for the HCI cluster - ex: 255.255.252.0. + +- Required: Yes +- Type: string + +### Parameter: `clusterName` + +The name of the Azure Stack HCI cluster - this must be a valid Active Directory computer name and will be the name of your cluster in Azure. Required if the template is used in a standalone deployment. + +- Required: Yes +- Type: string + +### Parameter: `bitlockerBootVolume` + +When set to true, BitLocker XTS_AES 256-bit encryption is enabled for all data-at-rest on the OS volume of your Azure Stack HCI cluster. This setting is TPM-hardware dependent. + +- Required: No +- Type: bool +- Default: `False` + +### Parameter: `bitlockerDataVolumes` + +When set to true, BitLocker XTS-AES 256-bit encryption is enabled for all data-at-rest on your Azure Stack HCI cluster shared volumes. + +- Required: No +- Type: bool +- Default: `False` + +### Parameter: `cloudId` + +If using a shared key vault or non-legacy secret naming, pass the properties.cloudId guid from the pre-created HCI cluster resource. + +- Required: No +- Type: string + +### Parameter: `credentialGuardEnforced` + +Enables the Credential Guard. + +- Required: No +- Type: bool +- Default: `False` + +### Parameter: `driftControlEnforced` + +When set to true, the security baseline is re-applied regularly. + +- Required: No +- Type: bool +- Default: `False` + +### Parameter: `drtmProtection` + +The hardware-dependent Secure Boot setting. + +- Required: No +- Type: bool +- Default: `True` + +### Parameter: `enableStorageAutoIp` + +Enable storage auto IP assignment. This should be true for most deployments except when deploying a three-node switchless cluster, in which case storage IPs should be configured before deployment and this value set to false. + +- Required: No +- Type: bool +- Default: `True` + +### Parameter: `episodicDataUpload` + +The diagnostic data for deploying a HCI cluster. + +- Required: No +- Type: bool +- Default: `True` + +### Parameter: `hvciProtection` + +The Hypervisor-protected Code Integrity setting. + +- Required: No +- Type: bool +- Default: `True` + +### Parameter: `isEuropeanUnionLocation` + +The location data for deploying a HCI cluster. + +- Required: No +- Type: bool +- Default: `False` + +### Parameter: `name` + +The name of the deployment settings. + +- Required: No +- Type: string +- Default: `'default'` +- Allowed: + ```Bicep + [ + 'default' + ] + ``` + +### Parameter: `sideChannelMitigationEnforced` + +When set to true, all the side channel mitigations are enabled. + +- Required: No +- Type: bool +- Default: `False` + +### Parameter: `smbClusterEncryption` + +When set to true, cluster east-west traffic is encrypted. + +- Required: No +- Type: bool +- Default: `False` + +### Parameter: `smbSigningEnforced` + +When set to true, the SMB default instance requires sign in for the client and server services. + +- Required: No +- Type: bool +- Default: `False` + +### Parameter: `storageConfigurationMode` + +The storage volume configuration mode. See documentation for details. + +- Required: No +- Type: string +- Default: `'Express'` +- Allowed: + ```Bicep + [ + 'Express' + 'InfraOnly' + 'KeepStorage' + ] + ``` + +### Parameter: `streamingDataClient` + +The metrics data for deploying a HCI cluster. + +- Required: No +- Type: bool +- Default: `True` + +### Parameter: `wdacEnforced` + +Limits the applications and the code that you can run on your Azure Stack HCI cluster. + +- Required: No +- Type: bool +- Default: `True` + +## Outputs + +| Output | Type | Description | +| :-- | :-- | :-- | +| `name` | string | The name of the cluster deployment settings. | +| `resourceGroupName` | string | The resource group of the cluster deployment settings. | +| `resourceId` | string | The ID of the cluster deployment settings. | diff --git a/avm/res/azure-stack-hci/cluster/deployment-setting/main.bicep b/avm/res/azure-stack-hci/cluster/deployment-setting/main.bicep new file mode 100644 index 0000000000..d7fb08a885 --- /dev/null +++ b/avm/res/azure-stack-hci/cluster/deployment-setting/main.bicep @@ -0,0 +1,246 @@ +metadata name = 'Azure Stack HCI Cluster Deployment Settings' +metadata description = 'This module deploys an Azure Stack HCI Cluster Deployment Settings resource.' + +@description('Optional. The name of the deployment settings.') +@allowed([ + 'default' +]) +param name string = 'default' + +@description('Conditional. The name of the Azure Stack HCI cluster - this must be a valid Active Directory computer name and will be the name of your cluster in Azure. Required if the template is used in a standalone deployment.') +@maxLength(15) +@minLength(4) +param clusterName string + +@description('Required. First must pass with this parameter set to Validate prior running with it set to Deploy. If either Validation or Deployment phases fail, fix the issue, then resubmit the template with the same deploymentMode to retry.') +@allowed([ + 'Validate' + 'Deploy' +]) +param deploymentMode string + +@minLength(4) +@maxLength(8) +@description('Required. The prefix for the resource for the deployment. This value is used in key vault and storage account names in this template, as well as for the deploymentSettings.properties.deploymentConfiguration.scaleUnits.deploymentData.namingPrefix property which requires regex pattern: ^[a-zA-Z0-9-]{1,8}$.') +param deploymentPrefix string + +@description('Required. Names of the cluster node Arc Machine resources. These are the name of the Arc Machine resources created when the new HCI nodes were Arc initialized. Example: [hci-node-1, hci-node-2].') +param clusterNodeNames array + +@description('Required. The domain name of the Active Directory Domain Services. Example: "contoso.com".') +param domainFqdn string + +@description('Required. The ADDS OU path - ex "OU=HCI,DC=contoso,DC=com".') +param domainOUPath string + +@description('Optional. The Hypervisor-protected Code Integrity setting.') +param hvciProtection bool = true + +@description('Optional. The hardware-dependent Secure Boot setting.') +param drtmProtection bool = true + +@description('Optional. When set to true, the security baseline is re-applied regularly.') +param driftControlEnforced bool = false + +@description('Optional. Enables the Credential Guard.') +param credentialGuardEnforced bool = false + +@description('Optional. When set to true, the SMB default instance requires sign in for the client and server services.') +param smbSigningEnforced bool = false + +@description('Optional. When set to true, cluster east-west traffic is encrypted.') +param smbClusterEncryption bool = false + +@description('Optional. When set to true, all the side channel mitigations are enabled.') +param sideChannelMitigationEnforced bool = false + +@description('Optional. When set to true, BitLocker XTS_AES 256-bit encryption is enabled for all data-at-rest on the OS volume of your Azure Stack HCI cluster. This setting is TPM-hardware dependent.') +param bitlockerBootVolume bool = false + +@description('Optional. When set to true, BitLocker XTS-AES 256-bit encryption is enabled for all data-at-rest on your Azure Stack HCI cluster shared volumes.') +param bitlockerDataVolumes bool = false + +@description('Optional. Limits the applications and the code that you can run on your Azure Stack HCI cluster.') +param wdacEnforced bool = true + +// cluster diagnostics and telemetry configuration +@description('Optional. The metrics data for deploying a HCI cluster.') +param streamingDataClient bool = true + +@description('Optional. The location data for deploying a HCI cluster.') +param isEuropeanUnionLocation bool = false + +@description('Optional. The diagnostic data for deploying a HCI cluster.') +param episodicDataUpload bool = true + +// storage configuration +@description('Optional. The storage volume configuration mode. See documentation for details.') +@allowed([ + 'Express' + 'InfraOnly' + 'KeepStorage' +]) +param storageConfigurationMode string = 'Express' + +// cluster network configuration details +@description('Required. The subnet mask pf the Management Network for the HCI cluster - ex: 255.255.252.0.') +param subnetMask string + +@description('Required. The default gateway of the Management Network. Example: 192.168.0.1.') +param defaultGateway string + +@description('Required. The starting IP address for the Infrastructure Network IP pool. There must be at least 6 IPs between startingIPAddress and endingIPAddress and this pool should be not include the node IPs.') +param startingIPAddress string + +@description('Required. The ending IP address for the Infrastructure Network IP pool. There must be at least 6 IPs between startingIPAddress and endingIPAddress and this pool should be not include the node IPs.') +param endingIPAddress string + +@description('Required. The DNS servers accessible from the Management Network for the HCI cluster.') +param dnsServers string[] + +@description('Required. An array of Network ATC Network Intent objects that define the Compute, Management, and Storage network configuration for the cluster.') +param networkIntents array + +@description('Required. Specify whether the Storage Network connectivity is switched or switchless.') +param storageConnectivitySwitchless bool + +@description('Optional. Enable storage auto IP assignment. This should be true for most deployments except when deploying a three-node switchless cluster, in which case storage IPs should be configured before deployment and this value set to false.') +param enableStorageAutoIp bool = true + +@description('Required. An array of JSON objects that define the storage network configuration for the cluster. Each object should contain the adapterName, VLAN properties, and (optionally) IP configurations.') +param storageNetworks array + +// other cluster configuration parameters +@description('Required. The name of the Custom Location associated with the Arc Resource Bridge for this cluster. This value should reflect the physical location and identifier of the HCI cluster. Example: cl-hci-den-clu01.') +param customLocationName string + +@description('Required. The name of the storage account to be used as the witness for the HCI Windows Failover Cluster.') +param clusterWitnessStorageAccountName string + +@description('Required. The name of the key vault to be used for storing secrets for the HCI cluster. This currently needs to be unique per HCI cluster.') +param keyVaultName string + +@description('Optional. If using a shared key vault or non-legacy secret naming, pass the properties.cloudId guid from the pre-created HCI cluster resource.') +param cloudId string? + +var arcNodeResourceIds = [ + for (nodeName, index) in clusterNodeNames: resourceId('Microsoft.HybridCompute/machines', nodeName) +] + +resource cluster 'Microsoft.AzureStackHCI/clusters@2024-04-01' existing = { + name: clusterName +} + +resource deploymentSettings 'Microsoft.AzureStackHCI/clusters/deploymentSettings@2024-04-01' = { + name: name + parent: cluster + properties: { + arcNodeResourceIds: arcNodeResourceIds + deploymentMode: deploymentMode + deploymentConfiguration: { + version: '10.0.0.0' + scaleUnits: [ + { + deploymentData: { + securitySettings: { + hvciProtection: hvciProtection + drtmProtection: drtmProtection + driftControlEnforced: driftControlEnforced + credentialGuardEnforced: credentialGuardEnforced + smbSigningEnforced: smbSigningEnforced + smbClusterEncryption: smbClusterEncryption + sideChannelMitigationEnforced: sideChannelMitigationEnforced + bitlockerBootVolume: bitlockerBootVolume + bitlockerDataVolumes: bitlockerDataVolumes + wdacEnforced: wdacEnforced + } + observability: { + streamingDataClient: streamingDataClient + euLocation: isEuropeanUnionLocation + episodicDataUpload: episodicDataUpload + } + cluster: { + name: clusterName + witnessType: 'Cloud' + witnessPath: '' + cloudAccountName: clusterWitnessStorageAccountName + azureServiceEndpoint: environment().suffixes.storage + } + storage: { + configurationMode: storageConfigurationMode + } + namingPrefix: deploymentPrefix + domainFqdn: domainFqdn + infrastructureNetwork: [ + { + subnetMask: subnetMask + gateway: defaultGateway + ipPools: [ + { + startingAddress: startingIPAddress + endingAddress: endingIPAddress + } + ] + dnsServers: dnsServers + } + ] + physicalNodes: [ + for hciNode in arcNodeResourceIds: { + name: reference(hciNode, '2022-12-27', 'Full').properties.displayName + // Getting the IP from the first NIC of the node with a default gateway. Only the first management pNIC should have a gateway defined. + // This reference call requires that the 'DeviceManagementExtension' extension be fully initialized on each node, which creates the + // edgeDevices sub-resource queried below, containing the IP configuration. View the edgedevice to troubleshoot by appending + // `/providers/microsoft.azurestackhci/edgedevices/default` to the end the HCI Arc Machine resource id and using `az resource show --id ` + ipv4Address: (filter( + reference('${hciNode}/providers/microsoft.azurestackhci/edgeDevices/default', '2024-01-01', 'Full').properties.deviceConfiguration.nicDetails, + nic => nic.?defaultGateway != null + ))[0].ip4Address + } + ] + hostNetwork: { + intents: networkIntents + storageConnectivitySwitchless: storageConnectivitySwitchless + storageNetworks: [ + for (storageAdapter, index) in storageNetworks: { + name: 'StorageNetwork${index + 1}' + networkAdapterName: storageAdapter.adapterName + vlanId: storageAdapter.vlan + storageAdapterIPInfo: storageAdapter.?storageAdapterIPInfo + } + ] + enableStorageAutoIp: enableStorageAutoIp + } + adouPath: domainOUPath + secretsLocation: 'https://${keyVaultName}${environment().suffixes.keyvaultDns}' + optionalServices: { + customLocation: customLocationName + } + secrets: [ + for secretName in [ + 'LocalAdminCredential' + 'AzureStackLCMUserCredential' + 'DefaultARBApplication' + 'WitnessStorageKey' + ]: { + secretName: empty(cloudId) ? secretName : '${clusterName}-${secretName}-${cloudId}' + eceSecretName: secretName + secretLocation: empty(cloudId) + ? 'https://${keyVaultName}${environment().suffixes.keyvaultDns}/secrets/${secretName}' + : 'https://${keyVaultName}${environment().suffixes.keyvaultDns}/secrets/${clusterName}-${secretName}-${cloudId}' + } + ] + } + } + ] + } + } +} + +@description('The name of the cluster deployment settings.') +output name string = deploymentSettings.name + +@description('The ID of the cluster deployment settings.') +output resourceId string = deploymentSettings.id + +@description('The resource group of the cluster deployment settings.') +output resourceGroupName string = resourceGroup().name diff --git a/avm/res/azure-stack-hci/cluster/deployment-setting/main.json b/avm/res/azure-stack-hci/cluster/deployment-setting/main.json new file mode 100644 index 0000000000..2edb982bfa --- /dev/null +++ b/avm/res/azure-stack-hci/cluster/deployment-setting/main.json @@ -0,0 +1,397 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "languageVersion": "2.0", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.33.93.31351", + "templateHash": "7982158538530558379" + }, + "name": "Azure Stack HCI Cluster Deployment Settings", + "description": "This module deploys an Azure Stack HCI Cluster Deployment Settings resource." + }, + "parameters": { + "name": { + "type": "string", + "defaultValue": "default", + "allowedValues": [ + "default" + ], + "metadata": { + "description": "Optional. The name of the deployment settings." + } + }, + "clusterName": { + "type": "string", + "minLength": 4, + "maxLength": 15, + "metadata": { + "description": "Conditional. The name of the Azure Stack HCI cluster - this must be a valid Active Directory computer name and will be the name of your cluster in Azure. Required if the template is used in a standalone deployment." + } + }, + "deploymentMode": { + "type": "string", + "allowedValues": [ + "Validate", + "Deploy" + ], + "metadata": { + "description": "Required. First must pass with this parameter set to Validate prior running with it set to Deploy. If either Validation or Deployment phases fail, fix the issue, then resubmit the template with the same deploymentMode to retry." + } + }, + "deploymentPrefix": { + "type": "string", + "minLength": 4, + "maxLength": 8, + "metadata": { + "description": "Required. The prefix for the resource for the deployment. This value is used in key vault and storage account names in this template, as well as for the deploymentSettings.properties.deploymentConfiguration.scaleUnits.deploymentData.namingPrefix property which requires regex pattern: ^[a-zA-Z0-9-]{1,8}$." + } + }, + "clusterNodeNames": { + "type": "array", + "metadata": { + "description": "Required. Names of the cluster node Arc Machine resources. These are the name of the Arc Machine resources created when the new HCI nodes were Arc initialized. Example: [hci-node-1, hci-node-2]." + } + }, + "domainFqdn": { + "type": "string", + "metadata": { + "description": "Required. The domain name of the Active Directory Domain Services. Example: \"contoso.com\"." + } + }, + "domainOUPath": { + "type": "string", + "metadata": { + "description": "Required. The ADDS OU path - ex \"OU=HCI,DC=contoso,DC=com\"." + } + }, + "hvciProtection": { + "type": "bool", + "defaultValue": true, + "metadata": { + "description": "Optional. The Hypervisor-protected Code Integrity setting." + } + }, + "drtmProtection": { + "type": "bool", + "defaultValue": true, + "metadata": { + "description": "Optional. The hardware-dependent Secure Boot setting." + } + }, + "driftControlEnforced": { + "type": "bool", + "defaultValue": false, + "metadata": { + "description": "Optional. When set to true, the security baseline is re-applied regularly." + } + }, + "credentialGuardEnforced": { + "type": "bool", + "defaultValue": false, + "metadata": { + "description": "Optional. Enables the Credential Guard." + } + }, + "smbSigningEnforced": { + "type": "bool", + "defaultValue": false, + "metadata": { + "description": "Optional. When set to true, the SMB default instance requires sign in for the client and server services." + } + }, + "smbClusterEncryption": { + "type": "bool", + "defaultValue": false, + "metadata": { + "description": "Optional. When set to true, cluster east-west traffic is encrypted." + } + }, + "sideChannelMitigationEnforced": { + "type": "bool", + "defaultValue": false, + "metadata": { + "description": "Optional. When set to true, all the side channel mitigations are enabled." + } + }, + "bitlockerBootVolume": { + "type": "bool", + "defaultValue": false, + "metadata": { + "description": "Optional. When set to true, BitLocker XTS_AES 256-bit encryption is enabled for all data-at-rest on the OS volume of your Azure Stack HCI cluster. This setting is TPM-hardware dependent." + } + }, + "bitlockerDataVolumes": { + "type": "bool", + "defaultValue": false, + "metadata": { + "description": "Optional. When set to true, BitLocker XTS-AES 256-bit encryption is enabled for all data-at-rest on your Azure Stack HCI cluster shared volumes." + } + }, + "wdacEnforced": { + "type": "bool", + "defaultValue": true, + "metadata": { + "description": "Optional. Limits the applications and the code that you can run on your Azure Stack HCI cluster." + } + }, + "streamingDataClient": { + "type": "bool", + "defaultValue": true, + "metadata": { + "description": "Optional. The metrics data for deploying a HCI cluster." + } + }, + "isEuropeanUnionLocation": { + "type": "bool", + "defaultValue": false, + "metadata": { + "description": "Optional. The location data for deploying a HCI cluster." + } + }, + "episodicDataUpload": { + "type": "bool", + "defaultValue": true, + "metadata": { + "description": "Optional. The diagnostic data for deploying a HCI cluster." + } + }, + "storageConfigurationMode": { + "type": "string", + "defaultValue": "Express", + "allowedValues": [ + "Express", + "InfraOnly", + "KeepStorage" + ], + "metadata": { + "description": "Optional. The storage volume configuration mode. See documentation for details." + } + }, + "subnetMask": { + "type": "string", + "metadata": { + "description": "Required. The subnet mask pf the Management Network for the HCI cluster - ex: 255.255.252.0." + } + }, + "defaultGateway": { + "type": "string", + "metadata": { + "description": "Required. The default gateway of the Management Network. Example: 192.168.0.1." + } + }, + "startingIPAddress": { + "type": "string", + "metadata": { + "description": "Required. The starting IP address for the Infrastructure Network IP pool. There must be at least 6 IPs between startingIPAddress and endingIPAddress and this pool should be not include the node IPs." + } + }, + "endingIPAddress": { + "type": "string", + "metadata": { + "description": "Required. The ending IP address for the Infrastructure Network IP pool. There must be at least 6 IPs between startingIPAddress and endingIPAddress and this pool should be not include the node IPs." + } + }, + "dnsServers": { + "type": "array", + "items": { + "type": "string" + }, + "metadata": { + "description": "Required. The DNS servers accessible from the Management Network for the HCI cluster." + } + }, + "networkIntents": { + "type": "array", + "metadata": { + "description": "Required. An array of Network ATC Network Intent objects that define the Compute, Management, and Storage network configuration for the cluster." + } + }, + "storageConnectivitySwitchless": { + "type": "bool", + "metadata": { + "description": "Required. Specify whether the Storage Network connectivity is switched or switchless." + } + }, + "enableStorageAutoIp": { + "type": "bool", + "defaultValue": true, + "metadata": { + "description": "Optional. Enable storage auto IP assignment. This should be true for most deployments except when deploying a three-node switchless cluster, in which case storage IPs should be configured before deployment and this value set to false." + } + }, + "storageNetworks": { + "type": "array", + "metadata": { + "description": "Required. An array of JSON objects that define the storage network configuration for the cluster. Each object should contain the adapterName, VLAN properties, and (optionally) IP configurations." + } + }, + "customLocationName": { + "type": "string", + "metadata": { + "description": "Required. The name of the Custom Location associated with the Arc Resource Bridge for this cluster. This value should reflect the physical location and identifier of the HCI cluster. Example: cl-hci-den-clu01." + } + }, + "clusterWitnessStorageAccountName": { + "type": "string", + "metadata": { + "description": "Required. The name of the storage account to be used as the witness for the HCI Windows Failover Cluster." + } + }, + "keyVaultName": { + "type": "string", + "metadata": { + "description": "Required. The name of the key vault to be used for storing secrets for the HCI cluster. This currently needs to be unique per HCI cluster." + } + }, + "cloudId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. If using a shared key vault or non-legacy secret naming, pass the properties.cloudId guid from the pre-created HCI cluster resource." + } + } + }, + "variables": { + "copy": [ + { + "name": "arcNodeResourceIds", + "count": "[length(parameters('clusterNodeNames'))]", + "input": "[resourceId('Microsoft.HybridCompute/machines', parameters('clusterNodeNames')[copyIndex('arcNodeResourceIds')])]" + } + ] + }, + "resources": { + "cluster": { + "existing": true, + "type": "Microsoft.AzureStackHCI/clusters", + "apiVersion": "2024-04-01", + "name": "[parameters('clusterName')]" + }, + "deploymentSettings": { + "type": "Microsoft.AzureStackHCI/clusters/deploymentSettings", + "apiVersion": "2024-04-01", + "name": "[format('{0}/{1}', parameters('clusterName'), parameters('name'))]", + "properties": { + "arcNodeResourceIds": "[variables('arcNodeResourceIds')]", + "deploymentMode": "[parameters('deploymentMode')]", + "deploymentConfiguration": { + "version": "10.0.0.0", + "scaleUnits": [ + { + "deploymentData": { + "copy": [ + { + "name": "physicalNodes", + "count": "[length(variables('arcNodeResourceIds'))]", + "input": { + "name": "[reference(variables('arcNodeResourceIds')[copyIndex('physicalNodes')], '2022-12-27', 'Full').properties.displayName]", + "ipv4Address": "[filter(reference(format('{0}/providers/microsoft.azurestackhci/edgeDevices/default', variables('arcNodeResourceIds')[copyIndex('physicalNodes')]), '2024-01-01', 'Full').properties.deviceConfiguration.nicDetails, lambda('nic', not(equals(tryGet(lambdaVariables('nic'), 'defaultGateway'), null()))))[0].ip4Address]" + } + }, + { + "name": "secrets", + "count": "[length(createArray('LocalAdminCredential', 'AzureStackLCMUserCredential', 'DefaultARBApplication', 'WitnessStorageKey'))]", + "input": { + "secretName": "[if(empty(parameters('cloudId')), createArray('LocalAdminCredential', 'AzureStackLCMUserCredential', 'DefaultARBApplication', 'WitnessStorageKey')[copyIndex('secrets')], format('{0}-{1}-{2}', parameters('clusterName'), createArray('LocalAdminCredential', 'AzureStackLCMUserCredential', 'DefaultARBApplication', 'WitnessStorageKey')[copyIndex('secrets')], parameters('cloudId')))]", + "eceSecretName": "[createArray('LocalAdminCredential', 'AzureStackLCMUserCredential', 'DefaultARBApplication', 'WitnessStorageKey')[copyIndex('secrets')]]", + "secretLocation": "[if(empty(parameters('cloudId')), format('https://{0}{1}/secrets/{2}', parameters('keyVaultName'), environment().suffixes.keyvaultDns, createArray('LocalAdminCredential', 'AzureStackLCMUserCredential', 'DefaultARBApplication', 'WitnessStorageKey')[copyIndex('secrets')]), format('https://{0}{1}/secrets/{2}-{3}-{4}', parameters('keyVaultName'), environment().suffixes.keyvaultDns, parameters('clusterName'), createArray('LocalAdminCredential', 'AzureStackLCMUserCredential', 'DefaultARBApplication', 'WitnessStorageKey')[copyIndex('secrets')], parameters('cloudId')))]" + } + } + ], + "securitySettings": { + "hvciProtection": "[parameters('hvciProtection')]", + "drtmProtection": "[parameters('drtmProtection')]", + "driftControlEnforced": "[parameters('driftControlEnforced')]", + "credentialGuardEnforced": "[parameters('credentialGuardEnforced')]", + "smbSigningEnforced": "[parameters('smbSigningEnforced')]", + "smbClusterEncryption": "[parameters('smbClusterEncryption')]", + "sideChannelMitigationEnforced": "[parameters('sideChannelMitigationEnforced')]", + "bitlockerBootVolume": "[parameters('bitlockerBootVolume')]", + "bitlockerDataVolumes": "[parameters('bitlockerDataVolumes')]", + "wdacEnforced": "[parameters('wdacEnforced')]" + }, + "observability": { + "streamingDataClient": "[parameters('streamingDataClient')]", + "euLocation": "[parameters('isEuropeanUnionLocation')]", + "episodicDataUpload": "[parameters('episodicDataUpload')]" + }, + "cluster": { + "name": "[parameters('clusterName')]", + "witnessType": "Cloud", + "witnessPath": "", + "cloudAccountName": "[parameters('clusterWitnessStorageAccountName')]", + "azureServiceEndpoint": "[environment().suffixes.storage]" + }, + "storage": { + "configurationMode": "[parameters('storageConfigurationMode')]" + }, + "namingPrefix": "[parameters('deploymentPrefix')]", + "domainFqdn": "[parameters('domainFqdn')]", + "infrastructureNetwork": [ + { + "subnetMask": "[parameters('subnetMask')]", + "gateway": "[parameters('defaultGateway')]", + "ipPools": [ + { + "startingAddress": "[parameters('startingIPAddress')]", + "endingAddress": "[parameters('endingIPAddress')]" + } + ], + "dnsServers": "[parameters('dnsServers')]" + } + ], + "hostNetwork": { + "copy": [ + { + "name": "storageNetworks", + "count": "[length(parameters('storageNetworks'))]", + "input": { + "name": "[format('StorageNetwork{0}', add(copyIndex('storageNetworks'), 1))]", + "networkAdapterName": "[parameters('storageNetworks')[copyIndex('storageNetworks')].adapterName]", + "vlanId": "[parameters('storageNetworks')[copyIndex('storageNetworks')].vlan]", + "storageAdapterIPInfo": "[tryGet(parameters('storageNetworks')[copyIndex('storageNetworks')], 'storageAdapterIPInfo')]" + } + } + ], + "intents": "[parameters('networkIntents')]", + "storageConnectivitySwitchless": "[parameters('storageConnectivitySwitchless')]", + "enableStorageAutoIp": "[parameters('enableStorageAutoIp')]" + }, + "adouPath": "[parameters('domainOUPath')]", + "secretsLocation": "[format('https://{0}{1}', parameters('keyVaultName'), environment().suffixes.keyvaultDns)]", + "optionalServices": { + "customLocation": "[parameters('customLocationName')]" + } + } + } + ] + } + } + } + }, + "outputs": { + "name": { + "type": "string", + "metadata": { + "description": "The name of the cluster deployment settings." + }, + "value": "[parameters('name')]" + }, + "resourceId": { + "type": "string", + "metadata": { + "description": "The ID of the cluster deployment settings." + }, + "value": "[resourceId('Microsoft.AzureStackHCI/clusters/deploymentSettings', parameters('clusterName'), parameters('name'))]" + }, + "resourceGroupName": { + "type": "string", + "metadata": { + "description": "The resource group of the cluster deployment settings." + }, + "value": "[resourceGroup().name]" + } + } +} \ No newline at end of file diff --git a/avm/res/azure-stack-hci/cluster/main.bicep b/avm/res/azure-stack-hci/cluster/main.bicep new file mode 100644 index 0000000000..565a4150b2 --- /dev/null +++ b/avm/res/azure-stack-hci/cluster/main.bicep @@ -0,0 +1,405 @@ +metadata name = 'Azure Stack HCI Cluster' +metadata description = 'This module deploys an Azure Stack HCI Cluster on the provided Arc Machines.' + +// ============== // +// Parameters // +// ============== // + +@description('Required. The name of the Azure Stack HCI cluster - this must be a valid Active Directory computer name and will be the name of your cluster in Azure.') +@maxLength(15) +@minLength(4) +param name string + +@description('Optional. Location for all resources.') +param location string = resourceGroup().location + +@description('Optional. Tags of the resource.') +param tags object? + +@description('Optional. The cluster deployment operations to execute. Defaults to "[Validate, Deploy]".') +@allowed([ + 'Deploy' + 'Validate' +]) +param deploymentOperations string[] = ['Validate', 'Deploy'] + +@description('Optional. Enable/Disable usage telemetry for module.') +param enableTelemetry bool = true + +@description('Optional. The deployment settings of the cluster.') +param deploymentSettings deploymentSettingsType? + +import { roleAssignmentType } from 'br/public:avm/utl/types/avm-common-types:0.5.1' +@description('Optional. Array of role assignments to create.') +param roleAssignments roleAssignmentType[]? + +@description('Optional. Specify whether to use the shared key vault for the HCI cluster.') +param useSharedKeyVault bool = true + +// ============= // +// Variables // +// ============= // + +var builtInRoleNames = { + // Add other relevant built-in roles here for your resource as per BCPNFR5 + Contributor: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'b24988ac-6180-42a0-ab88-20f7382dd24c') + Owner: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '8e3af657-a8ff-443c-a75c-2fe8c4bcb635') + Reader: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'acdd72a7-3385-48ef-bd42-f606fba81ae7') + 'Role Based Access Control Administrator': subscriptionResourceId( + 'Microsoft.Authorization/roleDefinitions', + 'f58310d9-a9f6-439a-9e8d-f62e7b41a168' + ) + 'User Access Administrator': subscriptionResourceId( + 'Microsoft.Authorization/roleDefinitions', + '18d7d88d-d35e-4fb5-a5c3-7773c20a72d9' + ) + 'Azure Stack HCI Administrator': subscriptionResourceId( + 'Microsoft.Authorization/roleDefinitions', + 'bda0d508-adf1-4af0-9c28-88919fc3ae06' + ) + 'Windows Admin Center Administrator Login': subscriptionResourceId( + 'Microsoft.Authorization/roleDefinitions', + 'a6333a3e-0164-44c3-b281-7a577aff287f' + ) +} + +var formattedRoleAssignments = [ + for (roleAssignment, index) in (roleAssignments ?? []): union(roleAssignment, { + roleDefinitionId: builtInRoleNames[?roleAssignment.roleDefinitionIdOrName] ?? (contains( + roleAssignment.roleDefinitionIdOrName, + '/providers/Microsoft.Authorization/roleDefinitions/' + ) + ? roleAssignment.roleDefinitionIdOrName + : subscriptionResourceId('Microsoft.Authorization/roleDefinitions', roleAssignment.roleDefinitionIdOrName)) + }) +] + +// if deployment operations requested, validation must be performed first so we reverse sort the array +var sortedDeploymentOperations = (!empty(deploymentOperations)) ? sort(deploymentOperations, (a, b) => a > b) : [] + +// ============= // +// Resources // +// ============= // + +#disable-next-line no-deployments-resources +resource avmTelemetry 'Microsoft.Resources/deployments@2023-07-01' = if (enableTelemetry) { + name: take( + '46d3xbcp.res.azurestackhci-cluster.${replace('-..--..-', '.', '-')}.${substring(uniqueString(deployment().name, location), 0, 4)}', + 64 + ) + properties: { + mode: 'Incremental' + template: { + '$schema': 'https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#' + contentVersion: '0.0.0' + resources: [] + outputs: { + telemetry: { + type: 'String' + value: 'For more information, see https://aka.ms/avm/TelemetryInfo' + } + } + } + } +} + +resource cluster 'Microsoft.AzureStackHCI/clusters@2024-04-01' = { + name: name + identity: { + type: 'SystemAssigned' + } + location: location + properties: {} + tags: tags +} +@batchSize(1) +module deploymentSetting 'deployment-setting/main.bicep' = [ + for deploymentOperation in sortedDeploymentOperations: if (!empty(deploymentOperation) && !empty(deploymentSettings)) { + name: 'deploymentSettings-${deploymentOperation}' + params: { + cloudId: useSharedKeyVault ? cluster.properties.cloudId : null + clusterName: cluster.name + deploymentMode: deploymentOperation + clusterNodeNames: deploymentSettings!.clusterNodeNames + clusterWitnessStorageAccountName: deploymentSettings!.clusterWitnessStorageAccountName + customLocationName: deploymentSettings!.customLocationName + defaultGateway: deploymentSettings!.defaultGateway + deploymentPrefix: deploymentSettings!.deploymentPrefix + dnsServers: deploymentSettings!.dnsServers + domainFqdn: deploymentSettings!.domainFqdn + domainOUPath: deploymentSettings!.domainOUPath + endingIPAddress: deploymentSettings!.endingIPAddress + keyVaultName: deploymentSettings!.keyVaultName + networkIntents: deploymentSettings!.networkIntents + startingIPAddress: deploymentSettings!.startingIPAddress + storageConnectivitySwitchless: deploymentSettings!.storageConnectivitySwitchless + storageNetworks: deploymentSettings!.storageNetworks + subnetMask: deploymentSettings!.subnetMask + bitlockerBootVolume: deploymentSettings!.?bitlockerBootVolume + bitlockerDataVolumes: deploymentSettings!.?bitlockerDataVolumes + credentialGuardEnforced: deploymentSettings!.?credentialGuardEnforced + driftControlEnforced: deploymentSettings!.?driftControlEnforced + drtmProtection: deploymentSettings!.?drtmProtection + enableStorageAutoIp: deploymentSettings!.?enableStorageAutoIp + episodicDataUpload: deploymentSettings!.?episodicDataUpload + hvciProtection: deploymentSettings!.?hvciProtection + isEuropeanUnionLocation: deploymentSettings!.?isRFEuropeanUnionLocation + sideChannelMitigationEnforced: deploymentSettings!.?sideChannelMitigationEnforced + smbClusterEncryption: deploymentSettings!.?smbClusterEncryption + smbSigningEnforced: deploymentSettings!.?smbSigningEnforced + storageConfigurationMode: deploymentSettings!.?storageConfigurationMode + streamingDataClient: deploymentSettings!.?streamingDataClient + wdacEnforced: deploymentSettings!.?wdacEnforced + } + } +] + +resource cluster_roleAssignments 'Microsoft.Authorization/roleAssignments@2022-04-01' = [ + for (roleAssignment, index) in (formattedRoleAssignments ?? []): { + name: roleAssignment.?name ?? guid(cluster.id, roleAssignment.principalId, roleAssignment.roleDefinitionId) + properties: { + roleDefinitionId: roleAssignment.roleDefinitionId + principalId: roleAssignment.principalId + description: roleAssignment.?description + principalType: roleAssignment.?principalType + condition: roleAssignment.?condition + conditionVersion: !empty(roleAssignment.?condition) ? (roleAssignment.?conditionVersion ?? '2.0') : null // Must only be set if condtion is set + delegatedManagedIdentityResourceId: roleAssignment.?delegatedManagedIdentityResourceId + } + scope: cluster + } +] + +@description('The name of the cluster.') +output name string = cluster.name + +@description('The ID of the cluster.') +output resourceId string = cluster.id + +@description('The resource group of the cluster.') +output resourceGroupName string = resourceGroup().name + +@description('The managed identity of the cluster.') +output systemAssignedMIPrincipalId string = cluster.identity.principalId + +@description('The location of the cluster.') +output location string = cluster.location + +// =============== // +// Definitions // +// =============== // + +@export() +type networkIntentType = { + @description('Required. The names of the network adapters to include in the intent.') + adapter: string[] + + @description('Required. The name of the network intent.') + name: string + + @description('Required. Specify whether to override the adapter property. Use false by default.') + overrideAdapterProperty: bool + + @description('Required. The adapter property overrides for the network intent.') + adapterPropertyOverrides: { + @description('Required. The jumboPacket configuration for the network adapters.') + jumboPacket: string + + @description('Required. The networkDirect configuration for the network adapters.') + networkDirect: ('Enabled' | 'Disabled') + + @description('Required. The networkDirectTechnology configuration for the network adapters.') + networkDirectTechnology: ('RoCEv2' | 'iWARP') + } + + @description('Required. Specify whether to override the qosPolicy property. Use false by default.') + overrideQosPolicy: bool + + @description('Required. The qosPolicy overrides for the network intent.') + qosPolicyOverrides: { + @description('Required. The bandwidthPercentage for the network intent. Recommend 50.') + bandwidthPercentage_SMB: string + + @description('Required. Recommend 7.') + priorityValue8021Action_Cluster: string + + @description('Required. Recommend 3.') + priorityValue8021Action_SMB: string + } + + @description('Required. Specify whether to override the virtualSwitchConfiguration property. Use false by default.') + overrideVirtualSwitchConfiguration: bool + + @description('Required. The virtualSwitchConfiguration overrides for the network intent.') + virtualSwitchConfigurationOverrides: { + @description('Required. The enableIov configuration for the network intent.') + enableIov: ('true' | 'false') + + @description('Required. The loadBalancingAlgorithm configuration for the network intent.') + loadBalancingAlgorithm: ('Dynamic' | 'HyperVPort' | 'IPHash') + } + + @description('Required. The traffic types for the network intent.') + trafficType: ('Compute' | 'Management' | 'Storage')[] +} + +// define custom type for storage adapter IP info for 3-node switchless deployments +@export() +type storageAdapterIPInfoType = { + @description('Required. The HCI node name.') + physicalNode: string + + @description('Required. The IPv4 address for the storage adapter.') + ipv4Address: string + + @description('Required. The subnet mask for the storage adapter.') + subnetMask: string +} + +// define custom type for storage network objects +@export() +type storageNetworksType = { + @description('Required. The name of the storage adapter.') + adapterName: string + + @description('Required. The VLAN for the storage adapter.') + vlan: string + + @description('Optional. The storage adapter IP information for 3-node switchless or manual config deployments.') + storageAdapterIPInfo: storageAdapterIPInfoType[]? // optional for switched deployments +} + +// cluster security configuration settings +@export() +type securityConfigurationType = { + @description('Required. Enable/Disable HVCI protection.') + hvciProtection: bool + + @description('Required. Enable/Disable DRTM protection.') + drtmProtection: bool + + @description('Required. Enable/Disable Drift Control enforcement.') + driftControlEnforced: bool + + @description('Required. Enable/Disable Credential Guard enforcement.') + credentialGuardEnforced: bool + + @description('Required. Enable/Disable SMB signing enforcement.') + smbSigningEnforced: bool + + @description('Required. Enable/Disable SMB cluster encryption.') + smbClusterEncryption: bool + + @description('Required. Enable/Disable Side Channel Mitigation enforcement.') + sideChannelMitigationEnforced: bool + + @description('Required. Enable/Disable BitLocker protection for boot volume.') + bitlockerBootVolume: bool + + @description('Required. Enable/Disable BitLocker protection for data volumes.') + bitlockerDataVolumes: bool + + @description('Required. Enable/Disable WDAC enforcement.') + wdacEnforced: bool +} + +type deploymentSettingsType = { + @minLength(4) + @maxLength(8) + @description('Required. The prefix for the resource for the deployment. This value is used in key vault and storage account names in this template, as well as for the deploymentSettings.properties.deploymentConfiguration.scaleUnits.deploymentData.namingPrefix property which requires regex pattern: ^[a-zA-Z0-9-]{1,8}$.') + deploymentPrefix: string + + @description('Required. Names of the cluster node Arc Machine resources. These are the name of the Arc Machine resources created when the new HCI nodes were Arc initialized. Example: [hci-node-1, hci-node-2].') + clusterNodeNames: array + + @description('Required. The domain name of the Active Directory Domain Services. Example: "contoso.com".') + domainFqdn: string + + @description('Required. The ADDS OU path - ex "OU=HCI,DC=contoso,DC=com".') + domainOUPath: string + + @description('Optional. The Hypervisor-protected Code Integrity setting.') + hvciProtection: bool? + + @description('Optional. The hardware-dependent Secure Boot setting.') + drtmProtection: bool? + + @description('Optional. When set to true, the security baseline is re-applied regularly.') + driftControlEnforced: bool? + + @description('Optional. Enables the Credential Guard.') + credentialGuardEnforced: bool? + + @description('Optional. When set to true, the SMB default instance requires sign in for the client and server services.') + smbSigningEnforced: bool? + + @description('Optional. When set to true, cluster east-west traffic is encrypted.') + smbClusterEncryption: bool? + + @description('Optional. When set to true, all the side channel mitigations are enabled.') + sideChannelMitigationEnforced: bool? + + @description('Optional. When set to true, BitLocker XTS_AES 256-bit encryption is enabled for all data-at-rest on the OS volume of your Azure Stack HCI cluster. This setting is TPM-hardware dependent.') + bitlockerBootVolume: bool? + + @description('Optional. When set to true, BitLocker XTS-AES 256-bit encryption is enabled for all data-at-rest on your Azure Stack HCI cluster shared volumes.') + bitlockerDataVolumes: bool? + + @description('Optional. Limits the applications and the code that you can run on your Azure Stack HCI cluster.') + wdacEnforced: bool? + + // cluster diagnostics and telemetry configuration + @description('Optional. The metrics data for deploying a HCI cluster.') + streamingDataClient: bool? + + @description('Optional. The location data for deploying a HCI cluster.') + isEuropeanUnionLocation: bool? + + @description('Optional. The diagnostic data for deploying a HCI cluster.') + episodicDataUpload: bool? + + // storage configuration + @description('Optional. The storage volume configuration mode. See documentation for details.') + storageConfigurationMode: ('Express' | 'InfraOnly' | 'KeepStorage')? + + // cluster network configuration details + @description('Required. The subnet mask pf the Management Network for the HCI cluster - ex: 255.255.252.0.') + subnetMask: string + + @description('Required. The default gateway of the Management Network. Example: 192.168.0.1.') + defaultGateway: string + + @description('Required. The starting IP address for the Infrastructure Network IP pool. There must be at least 6 IPs between startingIPAddress and endingIPAddress and this pool should be not include the node IPs.') + startingIPAddress: string + + @description('Required. The ending IP address for the Infrastructure Network IP pool. There must be at least 6 IPs between startingIPAddress and endingIPAddress and this pool should be not include the node IPs.') + endingIPAddress: string + + @description('Required. The DNS servers accessible from the Management Network for the HCI cluster.') + dnsServers: string[] + + @description('Required. An array of Network ATC Network Intent objects that define the Compute, Management, and Storage network configuration for the cluster.') + networkIntents: array + + @description('Required. Specify whether the Storage Network connectivity is switched or switchless.') + storageConnectivitySwitchless: bool + + @description('Optional. Enable storage auto IP assignment. This should be true for most deployments except when deploying a three-node switchless cluster, in which case storage IPs should be configured before deployment and this value set to false.') + enableStorageAutoIp: bool? + + @description('Required. An array of JSON objects that define the storage network configuration for the cluster. Each object should contain the adapterName, VLAN properties, and (optionally) IP configurations.') + storageNetworks: array + + // other cluster configuration parameters + @description('Required. The name of the Custom Location associated with the Arc Resource Bridge for this cluster. This value should reflect the physical location and identifier of the HCI cluster. Example: cl-hci-den-clu01.') + customLocationName: string + + @description('Required. The name of the storage account to be used as the witness for the HCI Windows Failover Cluster.') + clusterWitnessStorageAccountName: string + + @description('Required. The name of the key vault to be used for storing secrets for the HCI cluster. This currently needs to be unique per HCI cluster.') + keyVaultName: string + + @description('Optional. If using a shared key vault or non-legacy secret naming, pass the properties.cloudId guid from the pre-created HCI cluster resource.') + cloudId: string? +} diff --git a/avm/res/azure-stack-hci/cluster/main.json b/avm/res/azure-stack-hci/cluster/main.json new file mode 100644 index 0000000000..f082101b8d --- /dev/null +++ b/avm/res/azure-stack-hci/cluster/main.json @@ -0,0 +1,1273 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "languageVersion": "2.0", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.33.93.31351", + "templateHash": "2661952410917464461" + }, + "name": "Azure Stack HCI Cluster", + "description": "This module deploys an Azure Stack HCI Cluster on the provided Arc Machines." + }, + "definitions": { + "networkIntentType": { + "type": "object", + "properties": { + "adapter": { + "type": "array", + "items": { + "type": "string" + }, + "metadata": { + "description": "Required. The names of the network adapters to include in the intent." + } + }, + "name": { + "type": "string", + "metadata": { + "description": "Required. The name of the network intent." + } + }, + "overrideAdapterProperty": { + "type": "bool", + "metadata": { + "description": "Required. Specify whether to override the adapter property. Use false by default." + } + }, + "adapterPropertyOverrides": { + "type": "object", + "properties": { + "jumboPacket": { + "type": "string", + "metadata": { + "description": "Required. The jumboPacket configuration for the network adapters." + } + }, + "networkDirect": { + "type": "string", + "allowedValues": [ + "Disabled", + "Enabled" + ], + "metadata": { + "description": "Required. The networkDirect configuration for the network adapters." + } + }, + "networkDirectTechnology": { + "type": "string", + "allowedValues": [ + "RoCEv2", + "iWARP" + ], + "metadata": { + "description": "Required. The networkDirectTechnology configuration for the network adapters." + } + } + }, + "metadata": { + "description": "Required. The adapter property overrides for the network intent." + } + }, + "overrideQosPolicy": { + "type": "bool", + "metadata": { + "description": "Required. Specify whether to override the qosPolicy property. Use false by default." + } + }, + "qosPolicyOverrides": { + "type": "object", + "properties": { + "bandwidthPercentage_SMB": { + "type": "string", + "metadata": { + "description": "Required. The bandwidthPercentage for the network intent. Recommend 50." + } + }, + "priorityValue8021Action_Cluster": { + "type": "string", + "metadata": { + "description": "Required. Recommend 7." + } + }, + "priorityValue8021Action_SMB": { + "type": "string", + "metadata": { + "description": "Required. Recommend 3." + } + } + }, + "metadata": { + "description": "Required. The qosPolicy overrides for the network intent." + } + }, + "overrideVirtualSwitchConfiguration": { + "type": "bool", + "metadata": { + "description": "Required. Specify whether to override the virtualSwitchConfiguration property. Use false by default." + } + }, + "virtualSwitchConfigurationOverrides": { + "type": "object", + "properties": { + "enableIov": { + "type": "string", + "allowedValues": [ + "false", + "true" + ], + "metadata": { + "description": "Required. The enableIov configuration for the network intent." + } + }, + "loadBalancingAlgorithm": { + "type": "string", + "allowedValues": [ + "Dynamic", + "HyperVPort", + "IPHash" + ], + "metadata": { + "description": "Required. The loadBalancingAlgorithm configuration for the network intent." + } + } + }, + "metadata": { + "description": "Required. The virtualSwitchConfiguration overrides for the network intent." + } + }, + "trafficType": { + "type": "array", + "allowedValues": [ + "Compute", + "Management", + "Storage" + ], + "metadata": { + "description": "Required. The traffic types for the network intent." + } + } + }, + "metadata": { + "__bicep_export!": true + } + }, + "storageAdapterIPInfoType": { + "type": "object", + "properties": { + "physicalNode": { + "type": "string", + "metadata": { + "description": "Required. The HCI node name." + } + }, + "ipv4Address": { + "type": "string", + "metadata": { + "description": "Required. The IPv4 address for the storage adapter." + } + }, + "subnetMask": { + "type": "string", + "metadata": { + "description": "Required. The subnet mask for the storage adapter." + } + } + }, + "metadata": { + "__bicep_export!": true + } + }, + "storageNetworksType": { + "type": "object", + "properties": { + "adapterName": { + "type": "string", + "metadata": { + "description": "Required. The name of the storage adapter." + } + }, + "vlan": { + "type": "string", + "metadata": { + "description": "Required. The VLAN for the storage adapter." + } + }, + "storageAdapterIPInfo": { + "type": "array", + "items": { + "$ref": "#/definitions/storageAdapterIPInfoType" + }, + "nullable": true, + "metadata": { + "description": "Optional. The storage adapter IP information for 3-node switchless or manual config deployments." + } + } + }, + "metadata": { + "__bicep_export!": true + } + }, + "securityConfigurationType": { + "type": "object", + "properties": { + "hvciProtection": { + "type": "bool", + "metadata": { + "description": "Required. Enable/Disable HVCI protection." + } + }, + "drtmProtection": { + "type": "bool", + "metadata": { + "description": "Required. Enable/Disable DRTM protection." + } + }, + "driftControlEnforced": { + "type": "bool", + "metadata": { + "description": "Required. Enable/Disable Drift Control enforcement." + } + }, + "credentialGuardEnforced": { + "type": "bool", + "metadata": { + "description": "Required. Enable/Disable Credential Guard enforcement." + } + }, + "smbSigningEnforced": { + "type": "bool", + "metadata": { + "description": "Required. Enable/Disable SMB signing enforcement." + } + }, + "smbClusterEncryption": { + "type": "bool", + "metadata": { + "description": "Required. Enable/Disable SMB cluster encryption." + } + }, + "sideChannelMitigationEnforced": { + "type": "bool", + "metadata": { + "description": "Required. Enable/Disable Side Channel Mitigation enforcement." + } + }, + "bitlockerBootVolume": { + "type": "bool", + "metadata": { + "description": "Required. Enable/Disable BitLocker protection for boot volume." + } + }, + "bitlockerDataVolumes": { + "type": "bool", + "metadata": { + "description": "Required. Enable/Disable BitLocker protection for data volumes." + } + }, + "wdacEnforced": { + "type": "bool", + "metadata": { + "description": "Required. Enable/Disable WDAC enforcement." + } + } + }, + "metadata": { + "__bicep_export!": true + } + }, + "deploymentSettingsType": { + "type": "object", + "properties": { + "deploymentPrefix": { + "type": "string", + "minLength": 4, + "maxLength": 8, + "metadata": { + "description": "Required. The prefix for the resource for the deployment. This value is used in key vault and storage account names in this template, as well as for the deploymentSettings.properties.deploymentConfiguration.scaleUnits.deploymentData.namingPrefix property which requires regex pattern: ^[a-zA-Z0-9-]{1,8}$." + } + }, + "clusterNodeNames": { + "type": "array", + "metadata": { + "description": "Required. Names of the cluster node Arc Machine resources. These are the name of the Arc Machine resources created when the new HCI nodes were Arc initialized. Example: [hci-node-1, hci-node-2]." + } + }, + "domainFqdn": { + "type": "string", + "metadata": { + "description": "Required. The domain name of the Active Directory Domain Services. Example: \"contoso.com\"." + } + }, + "domainOUPath": { + "type": "string", + "metadata": { + "description": "Required. The ADDS OU path - ex \"OU=HCI,DC=contoso,DC=com\"." + } + }, + "hvciProtection": { + "type": "bool", + "nullable": true, + "metadata": { + "description": "Optional. The Hypervisor-protected Code Integrity setting." + } + }, + "drtmProtection": { + "type": "bool", + "nullable": true, + "metadata": { + "description": "Optional. The hardware-dependent Secure Boot setting." + } + }, + "driftControlEnforced": { + "type": "bool", + "nullable": true, + "metadata": { + "description": "Optional. When set to true, the security baseline is re-applied regularly." + } + }, + "credentialGuardEnforced": { + "type": "bool", + "nullable": true, + "metadata": { + "description": "Optional. Enables the Credential Guard." + } + }, + "smbSigningEnforced": { + "type": "bool", + "nullable": true, + "metadata": { + "description": "Optional. When set to true, the SMB default instance requires sign in for the client and server services." + } + }, + "smbClusterEncryption": { + "type": "bool", + "nullable": true, + "metadata": { + "description": "Optional. When set to true, cluster east-west traffic is encrypted." + } + }, + "sideChannelMitigationEnforced": { + "type": "bool", + "nullable": true, + "metadata": { + "description": "Optional. When set to true, all the side channel mitigations are enabled." + } + }, + "bitlockerBootVolume": { + "type": "bool", + "nullable": true, + "metadata": { + "description": "Optional. When set to true, BitLocker XTS_AES 256-bit encryption is enabled for all data-at-rest on the OS volume of your Azure Stack HCI cluster. This setting is TPM-hardware dependent." + } + }, + "bitlockerDataVolumes": { + "type": "bool", + "nullable": true, + "metadata": { + "description": "Optional. When set to true, BitLocker XTS-AES 256-bit encryption is enabled for all data-at-rest on your Azure Stack HCI cluster shared volumes." + } + }, + "wdacEnforced": { + "type": "bool", + "nullable": true, + "metadata": { + "description": "Optional. Limits the applications and the code that you can run on your Azure Stack HCI cluster." + } + }, + "streamingDataClient": { + "type": "bool", + "nullable": true, + "metadata": { + "description": "Optional. The metrics data for deploying a HCI cluster." + } + }, + "isEuropeanUnionLocation": { + "type": "bool", + "nullable": true, + "metadata": { + "description": "Optional. The location data for deploying a HCI cluster." + } + }, + "episodicDataUpload": { + "type": "bool", + "nullable": true, + "metadata": { + "description": "Optional. The diagnostic data for deploying a HCI cluster." + } + }, + "storageConfigurationMode": { + "type": "string", + "allowedValues": [ + "Express", + "InfraOnly", + "KeepStorage" + ], + "nullable": true, + "metadata": { + "description": "Optional. The storage volume configuration mode. See documentation for details." + } + }, + "subnetMask": { + "type": "string", + "metadata": { + "description": "Required. The subnet mask pf the Management Network for the HCI cluster - ex: 255.255.252.0." + } + }, + "defaultGateway": { + "type": "string", + "metadata": { + "description": "Required. The default gateway of the Management Network. Example: 192.168.0.1." + } + }, + "startingIPAddress": { + "type": "string", + "metadata": { + "description": "Required. The starting IP address for the Infrastructure Network IP pool. There must be at least 6 IPs between startingIPAddress and endingIPAddress and this pool should be not include the node IPs." + } + }, + "endingIPAddress": { + "type": "string", + "metadata": { + "description": "Required. The ending IP address for the Infrastructure Network IP pool. There must be at least 6 IPs between startingIPAddress and endingIPAddress and this pool should be not include the node IPs." + } + }, + "dnsServers": { + "type": "array", + "items": { + "type": "string" + }, + "metadata": { + "description": "Required. The DNS servers accessible from the Management Network for the HCI cluster." + } + }, + "networkIntents": { + "type": "array", + "metadata": { + "description": "Required. An array of Network ATC Network Intent objects that define the Compute, Management, and Storage network configuration for the cluster." + } + }, + "storageConnectivitySwitchless": { + "type": "bool", + "metadata": { + "description": "Required. Specify whether the Storage Network connectivity is switched or switchless." + } + }, + "enableStorageAutoIp": { + "type": "bool", + "nullable": true, + "metadata": { + "description": "Optional. Enable storage auto IP assignment. This should be true for most deployments except when deploying a three-node switchless cluster, in which case storage IPs should be configured before deployment and this value set to false." + } + }, + "storageNetworks": { + "type": "array", + "metadata": { + "description": "Required. An array of JSON objects that define the storage network configuration for the cluster. Each object should contain the adapterName, VLAN properties, and (optionally) IP configurations." + } + }, + "customLocationName": { + "type": "string", + "metadata": { + "description": "Required. The name of the Custom Location associated with the Arc Resource Bridge for this cluster. This value should reflect the physical location and identifier of the HCI cluster. Example: cl-hci-den-clu01." + } + }, + "clusterWitnessStorageAccountName": { + "type": "string", + "metadata": { + "description": "Required. The name of the storage account to be used as the witness for the HCI Windows Failover Cluster." + } + }, + "keyVaultName": { + "type": "string", + "metadata": { + "description": "Required. The name of the key vault to be used for storing secrets for the HCI cluster. This currently needs to be unique per HCI cluster." + } + }, + "cloudId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. If using a shared key vault or non-legacy secret naming, pass the properties.cloudId guid from the pre-created HCI cluster resource." + } + } + } + }, + "roleAssignmentType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The name (as GUID) of the role assignment. If not provided, a GUID will be generated." + } + }, + "roleDefinitionIdOrName": { + "type": "string", + "metadata": { + "description": "Required. The role to assign. You can provide either the display name of the role definition, the role definition GUID, or its fully qualified ID in the following format: '/providers/Microsoft.Authorization/roleDefinitions/c2f4ef07-c644-48eb-af81-4b1b4947fb11'." + } + }, + "principalId": { + "type": "string", + "metadata": { + "description": "Required. The principal ID of the principal (user/group/identity) to assign the role to." + } + }, + "principalType": { + "type": "string", + "allowedValues": [ + "Device", + "ForeignGroup", + "Group", + "ServicePrincipal", + "User" + ], + "nullable": true, + "metadata": { + "description": "Optional. The principal type of the assigned principal ID." + } + }, + "description": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The description of the role assignment." + } + }, + "condition": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The conditions on the role assignment. This limits the resources it can be assigned to. e.g.: @Resource[Microsoft.Storage/storageAccounts/blobServices/containers:ContainerName] StringEqualsIgnoreCase \"foo_storage_container\"." + } + }, + "conditionVersion": { + "type": "string", + "allowedValues": [ + "2.0" + ], + "nullable": true, + "metadata": { + "description": "Optional. Version of the condition." + } + }, + "delegatedManagedIdentityResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The Resource Id of the delegated managed identity resource." + } + } + }, + "metadata": { + "description": "An AVM-aligned type for a role assignment.", + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.5.1" + } + } + } + }, + "parameters": { + "name": { + "type": "string", + "minLength": 4, + "maxLength": 15, + "metadata": { + "description": "Required. The name of the Azure Stack HCI cluster - this must be a valid Active Directory computer name and will be the name of your cluster in Azure." + } + }, + "location": { + "type": "string", + "defaultValue": "[resourceGroup().location]", + "metadata": { + "description": "Optional. Location for all resources." + } + }, + "tags": { + "type": "object", + "nullable": true, + "metadata": { + "description": "Optional. Tags of the resource." + } + }, + "deploymentOperations": { + "type": "array", + "items": { + "type": "string" + }, + "defaultValue": [ + "Validate", + "Deploy" + ], + "allowedValues": [ + "Deploy", + "Validate" + ], + "metadata": { + "description": "Optional. The cluster deployment operations to execute. Defaults to \"[Validate, Deploy]\"." + } + }, + "enableTelemetry": { + "type": "bool", + "defaultValue": true, + "metadata": { + "description": "Optional. Enable/Disable usage telemetry for module." + } + }, + "deploymentSettings": { + "$ref": "#/definitions/deploymentSettingsType", + "nullable": true, + "metadata": { + "description": "Optional. The deployment settings of the cluster." + } + }, + "roleAssignments": { + "type": "array", + "items": { + "$ref": "#/definitions/roleAssignmentType" + }, + "nullable": true, + "metadata": { + "description": "Optional. Array of role assignments to create." + } + }, + "useSharedKeyVault": { + "type": "bool", + "defaultValue": true, + "metadata": { + "description": "Optional. Specify whether to use the shared key vault for the HCI cluster." + } + } + }, + "variables": { + "copy": [ + { + "name": "formattedRoleAssignments", + "count": "[length(coalesce(parameters('roleAssignments'), createArray()))]", + "input": "[union(coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')], createObject('roleDefinitionId', coalesce(tryGet(variables('builtInRoleNames'), coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName), if(contains(coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName, '/providers/Microsoft.Authorization/roleDefinitions/'), coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName, subscriptionResourceId('Microsoft.Authorization/roleDefinitions', coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName)))))]" + } + ], + "builtInRoleNames": { + "Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'b24988ac-6180-42a0-ab88-20f7382dd24c')]", + "Owner": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '8e3af657-a8ff-443c-a75c-2fe8c4bcb635')]", + "Reader": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'acdd72a7-3385-48ef-bd42-f606fba81ae7')]", + "Role Based Access Control Administrator": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'f58310d9-a9f6-439a-9e8d-f62e7b41a168')]", + "User Access Administrator": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '18d7d88d-d35e-4fb5-a5c3-7773c20a72d9')]", + "Azure Stack HCI Administrator": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'bda0d508-adf1-4af0-9c28-88919fc3ae06')]", + "Windows Admin Center Administrator Login": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'a6333a3e-0164-44c3-b281-7a577aff287f')]" + }, + "sortedDeploymentOperations": "[if(not(empty(parameters('deploymentOperations'))), sort(parameters('deploymentOperations'), lambda('a', 'b', greater(lambdaVariables('a'), lambdaVariables('b')))), createArray())]" + }, + "resources": { + "avmTelemetry": { + "condition": "[parameters('enableTelemetry')]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2023-07-01", + "name": "[take(format('46d3xbcp.res.azurestackhci-cluster.{0}.{1}', replace('-..--..-', '.', '-'), substring(uniqueString(deployment().name, parameters('location')), 0, 4)), 64)]", + "properties": { + "mode": "Incremental", + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "0.0.0", + "resources": [], + "outputs": { + "telemetry": { + "type": "String", + "value": "For more information, see https://aka.ms/avm/TelemetryInfo" + } + } + } + } + }, + "cluster": { + "type": "Microsoft.AzureStackHCI/clusters", + "apiVersion": "2024-04-01", + "name": "[parameters('name')]", + "identity": { + "type": "SystemAssigned" + }, + "location": "[parameters('location')]", + "properties": {}, + "tags": "[parameters('tags')]" + }, + "cluster_roleAssignments": { + "copy": { + "name": "cluster_roleAssignments", + "count": "[length(coalesce(variables('formattedRoleAssignments'), createArray()))]" + }, + "type": "Microsoft.Authorization/roleAssignments", + "apiVersion": "2022-04-01", + "scope": "[format('Microsoft.AzureStackHCI/clusters/{0}', parameters('name'))]", + "name": "[coalesce(tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'name'), guid(resourceId('Microsoft.AzureStackHCI/clusters', parameters('name')), coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].principalId, coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].roleDefinitionId))]", + "properties": { + "roleDefinitionId": "[coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].roleDefinitionId]", + "principalId": "[coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].principalId]", + "description": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'description')]", + "principalType": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'principalType')]", + "condition": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'condition')]", + "conditionVersion": "[if(not(empty(tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'condition'))), coalesce(tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'conditionVersion'), '2.0'), null())]", + "delegatedManagedIdentityResourceId": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'delegatedManagedIdentityResourceId')]" + }, + "dependsOn": [ + "cluster" + ] + }, + "deploymentSetting": { + "copy": { + "name": "deploymentSetting", + "count": "[length(variables('sortedDeploymentOperations'))]", + "mode": "serial", + "batchSize": 1 + }, + "condition": "[and(not(empty(variables('sortedDeploymentOperations')[copyIndex()])), not(empty(parameters('deploymentSettings'))))]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2022-09-01", + "name": "[format('deploymentSettings-{0}', variables('sortedDeploymentOperations')[copyIndex()])]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "cloudId": "[if(parameters('useSharedKeyVault'), createObject('value', reference('cluster').cloudId), createObject('value', null()))]", + "clusterName": { + "value": "[parameters('name')]" + }, + "deploymentMode": { + "value": "[variables('sortedDeploymentOperations')[copyIndex()]]" + }, + "clusterNodeNames": { + "value": "[parameters('deploymentSettings').clusterNodeNames]" + }, + "clusterWitnessStorageAccountName": { + "value": "[parameters('deploymentSettings').clusterWitnessStorageAccountName]" + }, + "customLocationName": { + "value": "[parameters('deploymentSettings').customLocationName]" + }, + "defaultGateway": { + "value": "[parameters('deploymentSettings').defaultGateway]" + }, + "deploymentPrefix": { + "value": "[parameters('deploymentSettings').deploymentPrefix]" + }, + "dnsServers": { + "value": "[parameters('deploymentSettings').dnsServers]" + }, + "domainFqdn": { + "value": "[parameters('deploymentSettings').domainFqdn]" + }, + "domainOUPath": { + "value": "[parameters('deploymentSettings').domainOUPath]" + }, + "endingIPAddress": { + "value": "[parameters('deploymentSettings').endingIPAddress]" + }, + "keyVaultName": { + "value": "[parameters('deploymentSettings').keyVaultName]" + }, + "networkIntents": { + "value": "[parameters('deploymentSettings').networkIntents]" + }, + "startingIPAddress": { + "value": "[parameters('deploymentSettings').startingIPAddress]" + }, + "storageConnectivitySwitchless": { + "value": "[parameters('deploymentSettings').storageConnectivitySwitchless]" + }, + "storageNetworks": { + "value": "[parameters('deploymentSettings').storageNetworks]" + }, + "subnetMask": { + "value": "[parameters('deploymentSettings').subnetMask]" + }, + "bitlockerBootVolume": { + "value": "[tryGet(parameters('deploymentSettings'), 'bitlockerBootVolume')]" + }, + "bitlockerDataVolumes": { + "value": "[tryGet(parameters('deploymentSettings'), 'bitlockerDataVolumes')]" + }, + "credentialGuardEnforced": { + "value": "[tryGet(parameters('deploymentSettings'), 'credentialGuardEnforced')]" + }, + "driftControlEnforced": { + "value": "[tryGet(parameters('deploymentSettings'), 'driftControlEnforced')]" + }, + "drtmProtection": { + "value": "[tryGet(parameters('deploymentSettings'), 'drtmProtection')]" + }, + "enableStorageAutoIp": { + "value": "[tryGet(parameters('deploymentSettings'), 'enableStorageAutoIp')]" + }, + "episodicDataUpload": { + "value": "[tryGet(parameters('deploymentSettings'), 'episodicDataUpload')]" + }, + "hvciProtection": { + "value": "[tryGet(parameters('deploymentSettings'), 'hvciProtection')]" + }, + "isEuropeanUnionLocation": { + "value": "[tryGet(parameters('deploymentSettings'), 'isRFEuropeanUnionLocation')]" + }, + "sideChannelMitigationEnforced": { + "value": "[tryGet(parameters('deploymentSettings'), 'sideChannelMitigationEnforced')]" + }, + "smbClusterEncryption": { + "value": "[tryGet(parameters('deploymentSettings'), 'smbClusterEncryption')]" + }, + "smbSigningEnforced": { + "value": "[tryGet(parameters('deploymentSettings'), 'smbSigningEnforced')]" + }, + "storageConfigurationMode": { + "value": "[tryGet(parameters('deploymentSettings'), 'storageConfigurationMode')]" + }, + "streamingDataClient": { + "value": "[tryGet(parameters('deploymentSettings'), 'streamingDataClient')]" + }, + "wdacEnforced": { + "value": "[tryGet(parameters('deploymentSettings'), 'wdacEnforced')]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "languageVersion": "2.0", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.33.93.31351", + "templateHash": "7982158538530558379" + }, + "name": "Azure Stack HCI Cluster Deployment Settings", + "description": "This module deploys an Azure Stack HCI Cluster Deployment Settings resource." + }, + "parameters": { + "name": { + "type": "string", + "defaultValue": "default", + "allowedValues": [ + "default" + ], + "metadata": { + "description": "Optional. The name of the deployment settings." + } + }, + "clusterName": { + "type": "string", + "minLength": 4, + "maxLength": 15, + "metadata": { + "description": "Conditional. The name of the Azure Stack HCI cluster - this must be a valid Active Directory computer name and will be the name of your cluster in Azure. Required if the template is used in a standalone deployment." + } + }, + "deploymentMode": { + "type": "string", + "allowedValues": [ + "Validate", + "Deploy" + ], + "metadata": { + "description": "Required. First must pass with this parameter set to Validate prior running with it set to Deploy. If either Validation or Deployment phases fail, fix the issue, then resubmit the template with the same deploymentMode to retry." + } + }, + "deploymentPrefix": { + "type": "string", + "minLength": 4, + "maxLength": 8, + "metadata": { + "description": "Required. The prefix for the resource for the deployment. This value is used in key vault and storage account names in this template, as well as for the deploymentSettings.properties.deploymentConfiguration.scaleUnits.deploymentData.namingPrefix property which requires regex pattern: ^[a-zA-Z0-9-]{1,8}$." + } + }, + "clusterNodeNames": { + "type": "array", + "metadata": { + "description": "Required. Names of the cluster node Arc Machine resources. These are the name of the Arc Machine resources created when the new HCI nodes were Arc initialized. Example: [hci-node-1, hci-node-2]." + } + }, + "domainFqdn": { + "type": "string", + "metadata": { + "description": "Required. The domain name of the Active Directory Domain Services. Example: \"contoso.com\"." + } + }, + "domainOUPath": { + "type": "string", + "metadata": { + "description": "Required. The ADDS OU path - ex \"OU=HCI,DC=contoso,DC=com\"." + } + }, + "hvciProtection": { + "type": "bool", + "defaultValue": true, + "metadata": { + "description": "Optional. The Hypervisor-protected Code Integrity setting." + } + }, + "drtmProtection": { + "type": "bool", + "defaultValue": true, + "metadata": { + "description": "Optional. The hardware-dependent Secure Boot setting." + } + }, + "driftControlEnforced": { + "type": "bool", + "defaultValue": false, + "metadata": { + "description": "Optional. When set to true, the security baseline is re-applied regularly." + } + }, + "credentialGuardEnforced": { + "type": "bool", + "defaultValue": false, + "metadata": { + "description": "Optional. Enables the Credential Guard." + } + }, + "smbSigningEnforced": { + "type": "bool", + "defaultValue": false, + "metadata": { + "description": "Optional. When set to true, the SMB default instance requires sign in for the client and server services." + } + }, + "smbClusterEncryption": { + "type": "bool", + "defaultValue": false, + "metadata": { + "description": "Optional. When set to true, cluster east-west traffic is encrypted." + } + }, + "sideChannelMitigationEnforced": { + "type": "bool", + "defaultValue": false, + "metadata": { + "description": "Optional. When set to true, all the side channel mitigations are enabled." + } + }, + "bitlockerBootVolume": { + "type": "bool", + "defaultValue": false, + "metadata": { + "description": "Optional. When set to true, BitLocker XTS_AES 256-bit encryption is enabled for all data-at-rest on the OS volume of your Azure Stack HCI cluster. This setting is TPM-hardware dependent." + } + }, + "bitlockerDataVolumes": { + "type": "bool", + "defaultValue": false, + "metadata": { + "description": "Optional. When set to true, BitLocker XTS-AES 256-bit encryption is enabled for all data-at-rest on your Azure Stack HCI cluster shared volumes." + } + }, + "wdacEnforced": { + "type": "bool", + "defaultValue": true, + "metadata": { + "description": "Optional. Limits the applications and the code that you can run on your Azure Stack HCI cluster." + } + }, + "streamingDataClient": { + "type": "bool", + "defaultValue": true, + "metadata": { + "description": "Optional. The metrics data for deploying a HCI cluster." + } + }, + "isEuropeanUnionLocation": { + "type": "bool", + "defaultValue": false, + "metadata": { + "description": "Optional. The location data for deploying a HCI cluster." + } + }, + "episodicDataUpload": { + "type": "bool", + "defaultValue": true, + "metadata": { + "description": "Optional. The diagnostic data for deploying a HCI cluster." + } + }, + "storageConfigurationMode": { + "type": "string", + "defaultValue": "Express", + "allowedValues": [ + "Express", + "InfraOnly", + "KeepStorage" + ], + "metadata": { + "description": "Optional. The storage volume configuration mode. See documentation for details." + } + }, + "subnetMask": { + "type": "string", + "metadata": { + "description": "Required. The subnet mask pf the Management Network for the HCI cluster - ex: 255.255.252.0." + } + }, + "defaultGateway": { + "type": "string", + "metadata": { + "description": "Required. The default gateway of the Management Network. Example: 192.168.0.1." + } + }, + "startingIPAddress": { + "type": "string", + "metadata": { + "description": "Required. The starting IP address for the Infrastructure Network IP pool. There must be at least 6 IPs between startingIPAddress and endingIPAddress and this pool should be not include the node IPs." + } + }, + "endingIPAddress": { + "type": "string", + "metadata": { + "description": "Required. The ending IP address for the Infrastructure Network IP pool. There must be at least 6 IPs between startingIPAddress and endingIPAddress and this pool should be not include the node IPs." + } + }, + "dnsServers": { + "type": "array", + "items": { + "type": "string" + }, + "metadata": { + "description": "Required. The DNS servers accessible from the Management Network for the HCI cluster." + } + }, + "networkIntents": { + "type": "array", + "metadata": { + "description": "Required. An array of Network ATC Network Intent objects that define the Compute, Management, and Storage network configuration for the cluster." + } + }, + "storageConnectivitySwitchless": { + "type": "bool", + "metadata": { + "description": "Required. Specify whether the Storage Network connectivity is switched or switchless." + } + }, + "enableStorageAutoIp": { + "type": "bool", + "defaultValue": true, + "metadata": { + "description": "Optional. Enable storage auto IP assignment. This should be true for most deployments except when deploying a three-node switchless cluster, in which case storage IPs should be configured before deployment and this value set to false." + } + }, + "storageNetworks": { + "type": "array", + "metadata": { + "description": "Required. An array of JSON objects that define the storage network configuration for the cluster. Each object should contain the adapterName, VLAN properties, and (optionally) IP configurations." + } + }, + "customLocationName": { + "type": "string", + "metadata": { + "description": "Required. The name of the Custom Location associated with the Arc Resource Bridge for this cluster. This value should reflect the physical location and identifier of the HCI cluster. Example: cl-hci-den-clu01." + } + }, + "clusterWitnessStorageAccountName": { + "type": "string", + "metadata": { + "description": "Required. The name of the storage account to be used as the witness for the HCI Windows Failover Cluster." + } + }, + "keyVaultName": { + "type": "string", + "metadata": { + "description": "Required. The name of the key vault to be used for storing secrets for the HCI cluster. This currently needs to be unique per HCI cluster." + } + }, + "cloudId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. If using a shared key vault or non-legacy secret naming, pass the properties.cloudId guid from the pre-created HCI cluster resource." + } + } + }, + "variables": { + "copy": [ + { + "name": "arcNodeResourceIds", + "count": "[length(parameters('clusterNodeNames'))]", + "input": "[resourceId('Microsoft.HybridCompute/machines', parameters('clusterNodeNames')[copyIndex('arcNodeResourceIds')])]" + } + ] + }, + "resources": { + "cluster": { + "existing": true, + "type": "Microsoft.AzureStackHCI/clusters", + "apiVersion": "2024-04-01", + "name": "[parameters('clusterName')]" + }, + "deploymentSettings": { + "type": "Microsoft.AzureStackHCI/clusters/deploymentSettings", + "apiVersion": "2024-04-01", + "name": "[format('{0}/{1}', parameters('clusterName'), parameters('name'))]", + "properties": { + "arcNodeResourceIds": "[variables('arcNodeResourceIds')]", + "deploymentMode": "[parameters('deploymentMode')]", + "deploymentConfiguration": { + "version": "10.0.0.0", + "scaleUnits": [ + { + "deploymentData": { + "copy": [ + { + "name": "physicalNodes", + "count": "[length(variables('arcNodeResourceIds'))]", + "input": { + "name": "[reference(variables('arcNodeResourceIds')[copyIndex('physicalNodes')], '2022-12-27', 'Full').properties.displayName]", + "ipv4Address": "[filter(reference(format('{0}/providers/microsoft.azurestackhci/edgeDevices/default', variables('arcNodeResourceIds')[copyIndex('physicalNodes')]), '2024-01-01', 'Full').properties.deviceConfiguration.nicDetails, lambda('nic', not(equals(tryGet(lambdaVariables('nic'), 'defaultGateway'), null()))))[0].ip4Address]" + } + }, + { + "name": "secrets", + "count": "[length(createArray('LocalAdminCredential', 'AzureStackLCMUserCredential', 'DefaultARBApplication', 'WitnessStorageKey'))]", + "input": { + "secretName": "[if(empty(parameters('cloudId')), createArray('LocalAdminCredential', 'AzureStackLCMUserCredential', 'DefaultARBApplication', 'WitnessStorageKey')[copyIndex('secrets')], format('{0}-{1}-{2}', parameters('clusterName'), createArray('LocalAdminCredential', 'AzureStackLCMUserCredential', 'DefaultARBApplication', 'WitnessStorageKey')[copyIndex('secrets')], parameters('cloudId')))]", + "eceSecretName": "[createArray('LocalAdminCredential', 'AzureStackLCMUserCredential', 'DefaultARBApplication', 'WitnessStorageKey')[copyIndex('secrets')]]", + "secretLocation": "[if(empty(parameters('cloudId')), format('https://{0}{1}/secrets/{2}', parameters('keyVaultName'), environment().suffixes.keyvaultDns, createArray('LocalAdminCredential', 'AzureStackLCMUserCredential', 'DefaultARBApplication', 'WitnessStorageKey')[copyIndex('secrets')]), format('https://{0}{1}/secrets/{2}-{3}-{4}', parameters('keyVaultName'), environment().suffixes.keyvaultDns, parameters('clusterName'), createArray('LocalAdminCredential', 'AzureStackLCMUserCredential', 'DefaultARBApplication', 'WitnessStorageKey')[copyIndex('secrets')], parameters('cloudId')))]" + } + } + ], + "securitySettings": { + "hvciProtection": "[parameters('hvciProtection')]", + "drtmProtection": "[parameters('drtmProtection')]", + "driftControlEnforced": "[parameters('driftControlEnforced')]", + "credentialGuardEnforced": "[parameters('credentialGuardEnforced')]", + "smbSigningEnforced": "[parameters('smbSigningEnforced')]", + "smbClusterEncryption": "[parameters('smbClusterEncryption')]", + "sideChannelMitigationEnforced": "[parameters('sideChannelMitigationEnforced')]", + "bitlockerBootVolume": "[parameters('bitlockerBootVolume')]", + "bitlockerDataVolumes": "[parameters('bitlockerDataVolumes')]", + "wdacEnforced": "[parameters('wdacEnforced')]" + }, + "observability": { + "streamingDataClient": "[parameters('streamingDataClient')]", + "euLocation": "[parameters('isEuropeanUnionLocation')]", + "episodicDataUpload": "[parameters('episodicDataUpload')]" + }, + "cluster": { + "name": "[parameters('clusterName')]", + "witnessType": "Cloud", + "witnessPath": "", + "cloudAccountName": "[parameters('clusterWitnessStorageAccountName')]", + "azureServiceEndpoint": "[environment().suffixes.storage]" + }, + "storage": { + "configurationMode": "[parameters('storageConfigurationMode')]" + }, + "namingPrefix": "[parameters('deploymentPrefix')]", + "domainFqdn": "[parameters('domainFqdn')]", + "infrastructureNetwork": [ + { + "subnetMask": "[parameters('subnetMask')]", + "gateway": "[parameters('defaultGateway')]", + "ipPools": [ + { + "startingAddress": "[parameters('startingIPAddress')]", + "endingAddress": "[parameters('endingIPAddress')]" + } + ], + "dnsServers": "[parameters('dnsServers')]" + } + ], + "hostNetwork": { + "copy": [ + { + "name": "storageNetworks", + "count": "[length(parameters('storageNetworks'))]", + "input": { + "name": "[format('StorageNetwork{0}', add(copyIndex('storageNetworks'), 1))]", + "networkAdapterName": "[parameters('storageNetworks')[copyIndex('storageNetworks')].adapterName]", + "vlanId": "[parameters('storageNetworks')[copyIndex('storageNetworks')].vlan]", + "storageAdapterIPInfo": "[tryGet(parameters('storageNetworks')[copyIndex('storageNetworks')], 'storageAdapterIPInfo')]" + } + } + ], + "intents": "[parameters('networkIntents')]", + "storageConnectivitySwitchless": "[parameters('storageConnectivitySwitchless')]", + "enableStorageAutoIp": "[parameters('enableStorageAutoIp')]" + }, + "adouPath": "[parameters('domainOUPath')]", + "secretsLocation": "[format('https://{0}{1}', parameters('keyVaultName'), environment().suffixes.keyvaultDns)]", + "optionalServices": { + "customLocation": "[parameters('customLocationName')]" + } + } + } + ] + } + } + } + }, + "outputs": { + "name": { + "type": "string", + "metadata": { + "description": "The name of the cluster deployment settings." + }, + "value": "[parameters('name')]" + }, + "resourceId": { + "type": "string", + "metadata": { + "description": "The ID of the cluster deployment settings." + }, + "value": "[resourceId('Microsoft.AzureStackHCI/clusters/deploymentSettings', parameters('clusterName'), parameters('name'))]" + }, + "resourceGroupName": { + "type": "string", + "metadata": { + "description": "The resource group of the cluster deployment settings." + }, + "value": "[resourceGroup().name]" + } + } + } + }, + "dependsOn": [ + "cluster" + ] + } + }, + "outputs": { + "name": { + "type": "string", + "metadata": { + "description": "The name of the cluster." + }, + "value": "[parameters('name')]" + }, + "resourceId": { + "type": "string", + "metadata": { + "description": "The ID of the cluster." + }, + "value": "[resourceId('Microsoft.AzureStackHCI/clusters', parameters('name'))]" + }, + "resourceGroupName": { + "type": "string", + "metadata": { + "description": "The resource group of the cluster." + }, + "value": "[resourceGroup().name]" + }, + "systemAssignedMIPrincipalId": { + "type": "string", + "metadata": { + "description": "The managed identity of the cluster." + }, + "value": "[reference('cluster', '2024-04-01', 'full').identity.principalId]" + }, + "location": { + "type": "string", + "metadata": { + "description": "The location of the cluster." + }, + "value": "[reference('cluster', '2024-04-01', 'full').location]" + } + } +} \ No newline at end of file diff --git a/avm/res/azure-stack-hci/cluster/tests/e2e-template-assets/azureStackHCIClusterPreqs/ashciARBSPRoleAssignment.bicep b/avm/res/azure-stack-hci/cluster/tests/e2e-template-assets/azureStackHCIClusterPreqs/ashciARBSPRoleAssignment.bicep new file mode 100644 index 0000000000..566aa85461 --- /dev/null +++ b/avm/res/azure-stack-hci/cluster/tests/e2e-template-assets/azureStackHCIClusterPreqs/ashciARBSPRoleAssignment.bicep @@ -0,0 +1,19 @@ +targetScope = 'subscription' + +@secure() +param arbDeploymentSPObjectId string + +var ARBDeploymentRoleID = subscriptionResourceId( + 'Microsoft.Authorization/roleDefinitions', + '7b1f81f9-4196-4058-8aae-762e593270df' +) + +resource ARBServicePrincipalResourceBridgeDeploymentRolePermissions 'Microsoft.Authorization/roleAssignments@2022-04-01' = { + name: guid('ARBServicePrincipalResourceBridgeDeploymentRolePermissions', subscription().id, arbDeploymentSPObjectId) + properties: { + roleDefinitionId: ARBDeploymentRoleID + principalId: arbDeploymentSPObjectId + principalType: 'ServicePrincipal' + description: 'Created by Azure Stack HCI deployment template' + } +} diff --git a/avm/res/azure-stack-hci/cluster/tests/e2e-template-assets/azureStackHCIClusterPreqs/ashciPrereqs.bicep b/avm/res/azure-stack-hci/cluster/tests/e2e-template-assets/azureStackHCIClusterPreqs/ashciPrereqs.bicep new file mode 100644 index 0000000000..42248f3f1c --- /dev/null +++ b/avm/res/azure-stack-hci/cluster/tests/e2e-template-assets/azureStackHCIClusterPreqs/ashciPrereqs.bicep @@ -0,0 +1,346 @@ +param location string + +@description('Required. The name of the storage account to create as a cluster witness.') +param clusterWitnessStorageAccountName string + +@description('Required. The name of the storage account to be created to collect Key Vault diagnostic logs.') +param keyVaultDiagnosticStorageAccountName string + +@description('Required. The name of the Azure Key Vault to create.') +param keyVaultName string + +param softDeleteRetentionDays int = 30 + +@description('Optional. The number of days for the retention in days. A value of 0 will retain the events indefinitely.') +@minValue(0) +@maxValue(365) +param logsRetentionInDays int = 30 + +param tenantId string +@secure() +param hciResourceProviderObjectId string +param arcNodeResourceIds array +param deploymentUsername string = 'deployUser' +@secure() +param deploymentUserPassword string +param localAdminUsername string +@secure() +param localAdminPassword string +@secure() +param arbDeploymentAppId string +@secure() +param arbDeploymentSPObjectId string +@secure() +param arbDeploymentServicePrincipalSecret string +param vnetSubnetResourceId string? +param allowIPtoStorageAndKeyVault string? +param usingArcGW bool = false +param clusterName string? +param cloudId string? + +// secret names for the Azure Key Vault - these cannot be changed. +var localAdminSecretName = (empty(cloudId)) ? 'LocalAdminCredential' : '${clusterName}-LocalAdminCredential-${cloudId}' +var domainAdminSecretName = (empty(cloudId)) + ? 'AzureStackLCMUserCredential' + : '${clusterName}-AzureStackLCMUserCredential-${cloudId}' +var arbDeploymentServicePrincipalName = (empty(cloudId)) + ? 'DefaultARBApplication' + : '${clusterName}-DefaultARBApplication-${cloudId}' +var storageWitnessName = (empty(cloudId)) ? 'WitnessStorageKey' : '${clusterName}-WitnessStorageKey-${cloudId}' + +// create base64 encoded secret values to be stored in the Azure Key Vault +var deploymentUserSecretValue = base64('${deploymentUsername}:${deploymentUserPassword}') +var localAdminSecretValue = base64('${localAdminUsername}:${localAdminPassword}') +var arbDeploymentServicePrincipalValue = base64('${arbDeploymentAppId}:${arbDeploymentServicePrincipalSecret}') + +var storageAccountType = 'Standard_ZRS' + +var azureConnectedMachineResourceManagerRoleID = subscriptionResourceId( + 'Microsoft.Authorization/roleDefinitions', + 'f5819b54-e033-4d82-ac66-4fec3cbf3f4c' +) +var readerRoleID = subscriptionResourceId( + 'Microsoft.Authorization/roleDefinitions', + 'acdd72a7-3385-48ef-bd42-f606fba81ae7' +) +var azureStackHCIDeviceManagementRole = subscriptionResourceId( + 'Microsoft.Authorization/roleDefinitions', + '865ae368-6a45-4bd1-8fbf-0d5151f56fc1' +) +var keyVaultSecretUserRoleID = subscriptionResourceId( + 'Microsoft.Authorization/roleDefinitions', + '4633458b-17de-408a-b874-0445c86b69e6' +) + +module ARBDeploymentSPNSubscriptionRoleAssignmnent 'ashciARBSPRoleAssignment.bicep' = { + name: '${uniqueString(deployment().name, location)}-test-arbroleassignment' + scope: subscription() + params: { + arbDeploymentSPObjectId: arbDeploymentSPObjectId + } +} + +resource diagnosticStorageAccount 'Microsoft.Storage/storageAccounts@2023-01-01' = { + name: keyVaultDiagnosticStorageAccountName + location: location + sku: { + name: storageAccountType + } + kind: 'StorageV2' + properties: { + supportsHttpsTrafficOnly: true + allowBlobPublicAccess: false + minimumTlsVersion: 'TLS1_2' + networkAcls: { + bypass: 'AzureServices' + defaultAction: 'Deny' + ipRules: [] + virtualNetworkRules: [] + } + } + resource blobService 'blobServices' = { + name: 'default' + properties: { + deleteRetentionPolicy: { + enabled: true + days: 7 + } + containerDeleteRetentionPolicy: { + enabled: true + days: 7 + } + } + } +} + +resource witnessStorageAccount 'Microsoft.Storage/storageAccounts@2023-01-01' = { + name: clusterWitnessStorageAccountName + location: location + sku: { + name: storageAccountType + } + kind: 'StorageV2' + properties: { + supportsHttpsTrafficOnly: true + allowBlobPublicAccess: false + minimumTlsVersion: 'TLS1_2' + networkAcls: { + bypass: 'AzureServices' + defaultAction: usingArcGW ? 'Allow' : 'Deny' // we don't know the source IP when traffic is redirected through the Arc GW + ipRules: (allowIPtoStorageAndKeyVault != null) + ? [ + { + value: allowIPtoStorageAndKeyVault! + } + ] + : [] + virtualNetworkRules: (vnetSubnetResourceId != null) + ? [ + { + id: vnetSubnetResourceId! + } + ] + : [] + } + } + resource blobService 'blobServices' = { + name: 'default' + properties: { + deleteRetentionPolicy: { + enabled: true + days: 7 + } + containerDeleteRetentionPolicy: { + enabled: true + days: 7 + } + } + } +} + +resource keyVault 'Microsoft.KeyVault/vaults@2023-07-01' = { + name: keyVaultName + location: location + properties: { + enabledForDeployment: true + enabledForTemplateDeployment: true + enabledForDiskEncryption: true + enableSoftDelete: true + softDeleteRetentionInDays: softDeleteRetentionDays + enableRbacAuthorization: true + publicNetworkAccess: 'Enabled' + accessPolicies: [] + tenantId: tenantId + sku: { + name: 'standard' + family: 'A' + } + networkAcls: { + bypass: 'AzureServices' + defaultAction: 'Deny' + ipRules: (allowIPtoStorageAndKeyVault != null) + ? [ + { + value: allowIPtoStorageAndKeyVault! + } + ] + : [] + virtualNetworkRules: (vnetSubnetResourceId != null) + ? [ + { + id: vnetSubnetResourceId! + } + ] + : [] + } + } + dependsOn: [ + diagnosticStorageAccount + ] + + resource keyVaultName_domainAdminSecret 'secrets@2023-07-01' = { + name: domainAdminSecretName + properties: { + contentType: 'Secret' + value: deploymentUserSecretValue + attributes: { + enabled: true + } + } + } + + resource keyVaultName_localAdminSecret 'secrets@2023-07-01' = { + name: localAdminSecretName + properties: { + contentType: 'Secret' + value: localAdminSecretValue + attributes: { + enabled: true + } + } + } + + resource keyVaultName_arbDeploymentServicePrincipal 'secrets@2023-07-01' = { + name: arbDeploymentServicePrincipalName + properties: { + contentType: 'Secret' + value: arbDeploymentServicePrincipalValue + attributes: { + enabled: true + } + } + } + + resource keyVaultName_storageWitness 'secrets@2023-07-01' = { + name: storageWitnessName + properties: { + contentType: 'Secret' + value: base64(witnessStorageAccount.listKeys().keys[0].value) + attributes: { + enabled: true + } + } + } +} + +resource keyVaultName_Microsoft_Insights_service 'microsoft.insights/diagnosticSettings@2016-09-01' = { + name: 'service' + location: location + scope: keyVault + properties: { + storageAccountId: diagnosticStorageAccount.id + logs: [ + { + category: 'AuditEvent' + enabled: true + retentionPolicy: { + enabled: true + days: logsRetentionInDays + } + } + ] + } +} + +resource SPConnectedMachineResourceManagerRolePermissions 'Microsoft.Authorization/roleAssignments@2022-04-01' = { + name: guid( + subscription().subscriptionId, + hciResourceProviderObjectId, + 'ConnectedMachineResourceManagerRolePermissions', + resourceGroup().id + ) + scope: resourceGroup() + properties: { + roleDefinitionId: azureConnectedMachineResourceManagerRoleID + principalId: hciResourceProviderObjectId + principalType: 'ServicePrincipal' + description: 'Created by Azure Stack HCI deployment template' + } +} + +resource NodeAzureConnectedMachineResourceManagerRolePermissions 'Microsoft.Authorization/roleAssignments@2022-04-01' = [ + for hciNode in arcNodeResourceIds: { + name: guid( + subscription().subscriptionId, + hciResourceProviderObjectId, + 'azureConnectedMachineResourceManager', + hciNode, + resourceGroup().id + ) + properties: { + roleDefinitionId: azureConnectedMachineResourceManagerRoleID + principalId: reference(hciNode, '2023-10-03-preview', 'Full').identity.principalId + principalType: 'ServicePrincipal' + description: 'Created by Azure Stack HCI deployment template' + } + } +] + +resource NodeazureStackHCIDeviceManagementRole 'Microsoft.Authorization/roleAssignments@2022-04-01' = [ + for hciNode in arcNodeResourceIds: { + name: guid( + subscription().subscriptionId, + hciResourceProviderObjectId, + 'azureStackHCIDeviceManagementRole', + hciNode, + resourceGroup().id + ) + properties: { + roleDefinitionId: azureStackHCIDeviceManagementRole + principalId: reference(hciNode, '2023-10-03-preview', 'Full').identity.principalId + principalType: 'ServicePrincipal' + description: 'Created by Azure Stack HCI deployment template' + } + } +] + +resource NodereaderRoleIDPermissions 'Microsoft.Authorization/roleAssignments@2022-04-01' = [ + for hciNode in arcNodeResourceIds: { + name: guid(subscription().subscriptionId, hciResourceProviderObjectId, 'reader', hciNode, resourceGroup().id) + properties: { + roleDefinitionId: readerRoleID + principalId: reference(hciNode, '2023-10-03-preview', 'Full').identity.principalId + principalType: 'ServicePrincipal' + description: 'Created by Azure Stack HCI deployment template' + } + } +] + +resource KeyVaultSecretsUserPermissions 'Microsoft.Authorization/roleAssignments@2022-04-01' = [ + for hciNode in arcNodeResourceIds: { + name: guid( + subscription().subscriptionId, + hciResourceProviderObjectId, + 'keyVaultSecretUser', + hciNode, + resourceGroup().id + ) + scope: keyVault + properties: { + roleDefinitionId: keyVaultSecretUserRoleID + principalId: reference(hciNode, '2023-10-03-preview', 'Full').identity.principalId + principalType: 'ServicePrincipal' + description: 'Created by Azure Stack HCI deployment template' + } + } +] diff --git a/avm/res/azure-stack-hci/cluster/tests/e2e-template-assets/azureStackHCIHost/README.md b/avm/res/azure-stack-hci/cluster/tests/e2e-template-assets/azureStackHCIHost/README.md new file mode 100644 index 0000000000..af59eeeb48 --- /dev/null +++ b/avm/res/azure-stack-hci/cluster/tests/e2e-template-assets/azureStackHCIHost/README.md @@ -0,0 +1,67 @@ +# HCI Assets for deployment validation + +The templates & scripts in this folder are designed to support the testing of Azure Stack HCI scenarios using nested virtualization on a VM deployed in Azure. + +The module creates an Azure VM, then uses a series of PowerShell Managed Run Commands and Deployment Scripts to prepare the host VM and deploy the nested Azure Stack HCI VMs. To simplify the design, the host VM is also configured as an AD domain controller, DNS server, DHCP server, and RRAS router, in addition to a Hyper-v host. The deployment is zero-touch to support pipeline deployments. + +The HCI Node VMs are brought to the point of Arc initialization. Deployment of the Azure Stack HCI cluster on top of the node VMs requires additional steps, either taken through the Portal or another IaC template. + +## Capabilities + +- HCI Cluster Size: The number of Azure Stack HCI nodes is determined by the `hciNodeCount` parameter. For each node, an additional data disk is added to the host VM. The host VM must be sized appropriately to accommodate the count of nodes (RAM and CPUs), but the solution is best suited for smaller clusters (1-4 nodes) +- Spot VM: The host VM can be configured as a spot VM to reduce cost; however depending on the evication rate of the chosen spot SKU, this may be problematic--for example, if the VM is evicted during deployment of the HCI cluster. +- Switched and switchless support: the module supports switched and switchless cluster designs with the `switchlessStorageConfig` parameter +- Idempotence: The deployment has been written to be idempotent in most scenarios, so re-running after fixing an issue with a failed deployment is valid. +- Existing VNET association: the host VM can be connected to an existing VNET by passing the `vnetSubnetID` parameter--which can help with troubleshooting; otherwise, a new VNET is created. +- VHDX or ISO HCI OS source: the HCI node VMs are built with the Azure Stack HCI OS installed. The OS can be sourced from either a VHDX or an ISO file using either the `hciVHDXDownloadURL` or `hciISODownloadURL` parameters. +- Deployment with a proxy and/or Arc Gateway + +## Troubleshooting + +### Credentials + +By default, the Domain Admin account and local administrator accounts have the username `admin-hci` and the password for these accounts is set by the `localAdminPassword` parameter. If you pass a random value to this parameter, but later need the password for troubleshooting a deployment issue, here are a couple options: + +- Use a Run Command on the host VM to reset the admin password. Ex: `net user admin-hci ''` +- Use a Run Command to decrypt the exported password stored locally on the host VM. Note, this password is encrypted to the SYSTEM account, so you must use Run Command or a tool like PSExec to decrypt it. Ex: + + ```powershell + $c = Import-CliXML -Path 'C:\temp\hciHostDeployAdminCred.xml' + $c.GetNetworkCredential().password + ``` + +### Logging + +All Run Command scripts log activity to the `C:\temp\hciHostDeploy.log` path on the host VM. + +It is also possible to review Run Command output with a REST call, making sure you expand the instance view. For example: `invoke-azrest -path '/subscriptions//resourceGroups//providers/Microsoft.Compute/virtualMachines/hciHost01/runCommands/runCommand6?api-version=2024-07-01&$expand=instanceView'` + +## Using this module for a host through Arc initialization-only deployment + +To use this Bicep module, you'll need to download a copy then supply values for the required parameters. Follow these steps: + +1. Clone the repository with Git: `git clone https://github.com/Azure/bicep-registry-modules.git` +1. Navigate to this directory `avm\utilities\e2e-template-assets\templates\azure-stack-hci\modules\azureStackHCIHost` and review the parameters at the top of the `hciHostDeployment.bicep` file. Provide values as required by hardcoding them into the Bicep file or by building a parameters file. +1. Run the Bicep deployment. For example: `az deployment group create -f .\avm\utilities\e2e-template-assets\templates\azure-stack-hci\modules\azureStackHCIHost\hciHostDeployment.bicep` +1. When the deployment completes, the HCI nodes will be ready for an HCI cluster to be deployed on them. For example, you could use the Bicep module at this path `avm\res\azure-stack-hci\cluster\main.bicep` or using the Azure Portal. + +## Using this template for an end-to-end HCI cluster deployment in Azure + +To use this Bicep module, you'll need to download a copy then supply values for the required parameters. Follow these steps: + +1. Clone the repository with Git: `git clone https://github.com/Azure/bicep-registry-modules.git` +1. Pick one of the E2E test templates in `avm\res\azure-stack-hci\cluster\tests\e2e`. NOTE: these templates all deploy to 'southeastasia' - to use another location, modify the `enforcedLocation` variable. +1. Create a parameter object in PowerShell to pass to the deployment. For example: + + ```powershell + $templateParameterObjectAIRS = @{ + 'arbDeploymentServicePrincipalSecret' = (Read-Host -AsSecureString -Prompt 'arbDeploymentServicePrincipalSecret') + 'localAdminAndDeploymentUserPass' = (Read-Host -AsSecureString -Prompt 'localAdminAndDeploymentUserPass') + 'arbDeploymentSPObjectId' = '4aceef22-f89f-42e8-8d8b-5aa3fb8fb57b' + 'arbDeploymentAppId' = '6e9515aa-715d-4a18-bdfe-463e8a004ffe' + 'hciResourceProviderObjectId' = 'e44d5f5e-21c6-4b3a-b697-e1236d00b006' + 'location' = 'southeastasia' + 'deploymentPrefix' = -join ((97..122) | Get-Random -Count 5 | ForEach-Object { [char]$_ }) + } + ``` +1. Execute the deployment. Example: `new-AzSubscriptionDeployment -TemplateFile C:\Users\mbratschun\repos\bicep-registry-modules\avm\res\azure-stack-hci\cluster\tests\e2e\2nodeswitched.defaults\main.test.bicep -TemplateParameterObject $templateParameterObjectBAMI24H2 -Location southeastasia` diff --git a/avm/res/azure-stack-hci/cluster/tests/e2e-template-assets/azureStackHCIHost/hciHostDeployment.bicep b/avm/res/azure-stack-hci/cluster/tests/e2e-template-assets/azureStackHCIHost/hciHostDeployment.bicep new file mode 100644 index 0000000000..d9d9e50a83 --- /dev/null +++ b/avm/res/azure-stack-hci/cluster/tests/e2e-template-assets/azureStackHCIHost/hciHostDeployment.bicep @@ -0,0 +1,535 @@ +@description('Required. The location for all resource except HCI Arc Nodes and HCI resources') +param location string + +@description('Optional. The Azure VM size for the HCI Host VM, which must support nested virtualization and have sufficient capacity for the HCI node VMs!') +param hostVMSize string = 'Standard_E32bds_v5' + +@description('Optional. The number of Azure Stack HCI nodes to deploy.') +param hciNodeCount int = 2 + +@description('Optional. Enable configuring switchless storage.') +param switchlessStorageConfig bool = false + +@description('Optional. The download URL for the Azure Stack HCI ISO.') +param hciISODownloadURL string = 'https://azurestackreleases.download.prss.microsoft.com/dbazure/AzureStackHCI/OS-Composition/10.2408.0.3061/AZURESTACKHci23H2.25398.469.LCM.10.2408.0.3061.x64.en-us.iso' + +@description('Optional. The local admin user name.') +param localAdminUsername string = 'admin-hci' + +@description('Required. The local admin password.') +@secure() +param localAdminPassword string + +@description('Optional. The domain OU path.') +param domainOUPath string = 'OU=HCI,DC=HCI,DC=local' + +@description('Optional. The deployment username.') +param deploymentUsername string = 'deployUser' + +@description('Required. The name of the VM-managed user identity to create, used for HCI Arc onboarding.') +param userAssignedIdentityName string + +@description('Required. The name of the VNET for the HCI host Azure VM.') +param virtualNetworkName string + +@description('Required. The name of the NSG to create.') +param networkSecurityGroupName string + +@description('Required. The name of the maintenance configuration for the Azure Stack HCI Host VM and proxy server.') +param maintenanceConfigurationName string + +@description('Required. The name of the Azure VM scale set for the HCI host.') +param HCIHostVirtualMachineScaleSetName string + +@description('Required. The name of the Network Interface Card to create.') +param networkInterfaceName string + +@description('Required. The name prefix for the Disks to create.') +param diskNamePrefix string + +@description('Required. The name of the Azure VM to create.') +param virtualMachineName string + +@description('Required. The name of the Maintenance Configuration Assignment for the proxy server.') +param maintenanceConfigurationAssignmentName string + +@description('Required. The name prefix for the \'wait\' deployment scripts to create.') +param waitDeploymentScriptPrefixName string + +// =================================// +// Deploy Host VM Infrastructure // +// =================================// + +// vm managed identity used for HCI Arc onboarding +resource userAssignedIdentity 'Microsoft.ManagedIdentity/userAssignedIdentities@2023-07-31-preview' = { + location: location + name: userAssignedIdentityName +} + +// grant identity owner permissions on the resource group +resource roleAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = { + name: guid(subscription().subscriptionId, userAssignedIdentity.name, 'Owner', resourceGroup().id) + properties: { + principalId: userAssignedIdentity.properties.principalId + roleDefinitionId: resourceId('Microsoft.Authorization/roleDefinitions', '8e3af657-a8ff-443c-a75c-2fe8c4bcb635') + principalType: 'ServicePrincipal' + description: 'Role assigned used for Azure Stack HCI IaC testing pipeline - remove if identity no longer exists!' + } +} + +// grant identity contributor permissions on the subscription - needed to register resource providers +module roleAssignment_subscriptionContributor 'modules/subscriptionRoleAssignment.bicep' = { + name: '${uniqueString(deployment().name, location)}-hcihostmi-roleAssignment_subContributor' + scope: subscription() + params: { + principalId: userAssignedIdentity.properties.principalId + } +} + +// optional VNET and subnet for the HCI host Azure VM +resource vnet 'Microsoft.Network/virtualNetworks@2020-11-01' = { + name: virtualNetworkName + location: location + properties: { + addressSpace: { + addressPrefixes: ['10.0.0.0/24'] + } + subnets: [ + { + name: 'subnet01' + properties: { + addressPrefix: '10.0.0.0/24' + serviceEndpoints: [ + { + service: 'Microsoft.Storage' + locations: [location] + } + { + service: 'Microsoft.KeyVault' + locations: [location] + } + ] + } + } + ] + } +} + +// create a mintenance configuration for the Azure Stack HCI Host VM and proxy server +resource maintenanceConfig 'Microsoft.Maintenance/maintenanceConfigurations@2023-09-01-preview' = { + location: location + name: maintenanceConfigurationName ?? '' + properties: { + maintenanceScope: 'InGuestPatch' + maintenanceWindow: { + recurEvery: 'Week Sunday' + startDateTime: '2020-04-30 08:00' + duration: '02:00' + timeZone: 'UTC' + } + installPatches: { + windowsParameters: { + classificationsToInclude: ['Critical', 'Security'] + } + rebootSetting: 'IfRequired' + } + extensionProperties: { + InGuestPatchMode: 'User' + } + } +} + +resource networkSecurityGroup 'Microsoft.Network/networkSecurityGroups@2020-11-01' = { + location: location + name: networkSecurityGroupName +} + +resource hciHostVMSSFlex 'Microsoft.Compute/virtualMachineScaleSets@2024-03-01' = { + name: HCIHostVirtualMachineScaleSetName + location: location + zones: ['1', '2', '3'] + properties: { + orchestrationMode: 'Flexible' + platformFaultDomainCount: 1 + } +} + +resource nic 'Microsoft.Network/networkInterfaces@2020-11-01' = { + location: location + name: networkInterfaceName + properties: { + networkSecurityGroup: { + id: networkSecurityGroup.id + } + ipConfigurations: [ + { + name: 'ipConfig01' + properties: { + subnet: { + id: vnet.properties.subnets[0].id + } + privateIPAllocationMethod: 'Dynamic' + } + } + ] + } +} + +// host VM disks +resource disks 'Microsoft.Compute/disks@2023-10-02' = [ + for diskNum in range(1, hciNodeCount): { + name: '${diskNamePrefix}${string(diskNum)}' + location: location + zones: ['1'] + sku: { + name: 'Premium_LRS' + } + properties: { + diskSizeGB: 2048 + networkAccessPolicy: 'DenyAll' + creationData: { + createOption: 'Empty' + } + } + } +] + +// Azure Stack HCI Host VM - +resource vm 'Microsoft.Compute/virtualMachines@2024-03-01' = { + location: location + name: virtualMachineName + zones: ['1'] + identity: { + type: 'UserAssigned' + userAssignedIdentities: { + '${userAssignedIdentity.id}': {} + } + } + properties: { + virtualMachineScaleSet: { + id: hciHostVMSSFlex.id + } + hardwareProfile: { + vmSize: hostVMSize + } + priority: 'Regular' + networkProfile: { + networkInterfaces: [ + { + id: nic.id + } + ] + } + storageProfile: { + imageReference: { + publisher: 'MicrosoftWindowsServer' + offer: 'WindowsServer' + sku: '2022-datacenter-g2' + version: 'latest' + } + osDisk: { + createOption: 'FromImage' + diskSizeGB: 128 + deleteOption: 'Delete' + managedDisk: { + storageAccountType: 'Premium_LRS' + } + } + dataDisks: [ + for diskNum in range(1, hciNodeCount): { + lun: diskNum + createOption: 'Attach' + caching: 'ReadOnly' + managedDisk: { + id: disks[diskNum - 1].id + } + deleteOption: 'Delete' + } + ] + //diskControllerType: 'NVMe' + } + osProfile: { + adminPassword: localAdminPassword + adminUsername: localAdminUsername + computerName: 'hciHost01' + windowsConfiguration: { + provisionVMAgent: true + enableAutomaticUpdates: true + patchSettings: { + patchMode: 'AutomaticByPlatform' + automaticByPlatformSettings: { + bypassPlatformSafetyChecksOnUserSchedule: true + } + } + } + } + securityProfile: { + uefiSettings: { + secureBootEnabled: true + vTpmEnabled: true + } + securityType: 'TrustedLaunch' + } + licenseType: 'Windows_Server' + } +} + +resource maintenanceAssignment_hciHost 'Microsoft.Maintenance/configurationAssignments@2023-04-01' = { + location: location + name: maintenanceConfigurationAssignmentName + properties: { + maintenanceConfigurationId: maintenanceConfig.id + } + scope: vm +} + +// ====================// +// Install Host Roles // +// ====================// + +// installs roles and features required for Azure Stack HCI Host VM +resource runCommand1 'Microsoft.Compute/virtualMachines/runCommands@2024-03-01' = { + parent: vm + location: location + name: 'runCommand1' + properties: { + source: { + script: loadTextContent('./scripts/hciHostStage1.ps1') + } + treatFailureAsDeploymentFailure: true + } +} + +// schedules a reboot of the VM +resource runCommand2 'Microsoft.Compute/virtualMachines/runCommands@2024-03-01' = { + parent: vm + location: location + name: 'runCommand2' + properties: { + source: { + script: loadTextContent('./scripts/hciHostStage2.ps1') + } + treatFailureAsDeploymentFailure: true + } + dependsOn: [runCommand1] +} + +// initiates a wait for the VM to reboot +resource wait1 'Microsoft.Resources/deploymentScripts@2023-08-01' = { + location: location + kind: 'AzurePowerShell' + name: '${waitDeploymentScriptPrefixName}-wait1' + properties: { + azPowerShellVersion: '3.0' + scriptContent: 'Start-Sleep -Seconds 90' + retentionInterval: 'PT6H' + } + dependsOn: [runCommand2] +} + +// ======================// +// Configure Host Roles // +// ======================// + +// initializes and mounts data disks, downloads HCI VHDX, configures the Azure Stack HCI Host VM with AD, routing, DNS, DHCP +resource runCommand3 'Microsoft.Compute/virtualMachines/runCommands@2024-03-01' = { + parent: vm + location: location + name: 'runCommand3' + properties: { + source: { + script: loadTextContent('./scripts/hciHostStage3.ps1') + } + parameters: [ + { + name: 'hciVHDXDownloadURL' + value: '' + } + { + name: 'hciISODownloadURL' + value: hciISODownloadURL + } + { + name: 'hciNodeCount' + value: string(hciNodeCount) + } + ] + treatFailureAsDeploymentFailure: true + } + dependsOn: [wait1] +} + +// schedules a reboot of the VM +resource runCommand4 'Microsoft.Compute/virtualMachines/runCommands@2024-03-01' = { + parent: vm + location: location + name: 'runCommand4' + properties: { + source: { + script: loadTextContent('./scripts/hciHostStage4.ps1') + } + treatFailureAsDeploymentFailure: true + } + dependsOn: [runCommand3] +} + +// initiates a wait for the VM to reboot - extra time for AD initialization +resource wait2 'Microsoft.Resources/deploymentScripts@2023-08-01' = { + location: location + kind: 'AzurePowerShell' + name: '${waitDeploymentScriptPrefixName}-wait2' + properties: { + azPowerShellVersion: '3.0' + scriptContent: 'Start-Sleep -Seconds 300 #enough time for AD start-up' + retentionInterval: 'PT6H' + } + dependsOn: [ + runCommand4 + ] +} + +// ===========================// +// Create HCI Node Guest VMs // +// ===========================// + +// creates hyper-v resources, configures NAT, builds and preps the Azure Stack HCI node VMs +resource runCommand5 'Microsoft.Compute/virtualMachines/runCommands@2024-03-01' = { + parent: vm + location: location + name: 'runCommand5' + properties: { + source: { + script: loadTextContent('./scripts/hciHostStage5.ps1') + } + parameters: [ + { + name: 'adminUsername' + value: localAdminUsername + } + { + name: 'hciNodeCount' + value: string(hciNodeCount) + } + { + name: 'switchlessStorageConfig' + value: switchlessStorageConfig ? 'switchless' : 'switched' + } + ] + protectedParameters: [ + { + name: 'adminPw' + value: localAdminPassword + } + ] + treatFailureAsDeploymentFailure: true + } + dependsOn: [wait2] +} + +// ================================================// +// Initialize Arc on HCI Node VMs and AD for HCI // +// ==============================================// + +// prepares AD for ASHCI onboarding, initiates Arc onboarding of HCI node VMs +resource runCommand6 'Microsoft.Compute/virtualMachines/runCommands@2024-03-01' = { + parent: vm + location: location + name: 'runCommand6' + properties: { + source: { + script: loadTextContent('./scripts/hciHostStage6.ps1') + } + parameters: [ + { + name: 'location' + value: location + } + { + name: 'resourceGroupName' + value: resourceGroup().name + } + { + name: 'subscriptionId' + value: subscription().subscriptionId + } + { + name: 'tenantId' + value: tenant().tenantId + } + { + name: 'accountName' + value: userAssignedIdentity.properties.principalId + } + { + name: 'adminUsername' + value: localAdminUsername + } + { + name: 'arcGatewayId' + value: '' + } + { + name: 'deploymentUsername' + value: deploymentUsername + } + { + name: 'domainOUPath' + value: domainOUPath + } + { + name: 'proxyBypassString' + value: '' + } + { + name: 'proxyServerEndpoint' + value: '' + } + { + name: 'userAssignedManagedIdentityClientId' + value: userAssignedIdentity.properties.clientId + } + ] + protectedParameters: [ + { + name: 'adminPw' + value: localAdminPassword + } + ] + treatFailureAsDeploymentFailure: true + } + dependsOn: [runCommand5] +} + +// waits for HCI extensions to be in succeeded state +resource runCommand7 'Microsoft.Compute/virtualMachines/runCommands@2024-03-01' = { + parent: vm + location: location + name: 'runCommand7' + properties: { + source: { + script: loadTextContent('./scripts/hciHostStage7.ps1') + } + parameters: [ + { + name: 'hciNodeCount' + value: string(hciNodeCount) + } + { + name: 'resourceGroupName' + value: resourceGroup().name + } + { + name: 'subscriptionId' + value: subscription().subscriptionId + } + { + name: 'userAssignedManagedIdentityClientId' + value: userAssignedIdentity.properties.clientId + } + ] + treatFailureAsDeploymentFailure: true + } + dependsOn: [runCommand6] +} + +output vnetSubnetResourceId string = vnet.properties.subnets[0].id diff --git a/avm/res/azure-stack-hci/cluster/tests/e2e-template-assets/azureStackHCIHost/modules/subscriptionRoleAssignment.bicep b/avm/res/azure-stack-hci/cluster/tests/e2e-template-assets/azureStackHCIHost/modules/subscriptionRoleAssignment.bicep new file mode 100644 index 0000000000..1493a632c5 --- /dev/null +++ b/avm/res/azure-stack-hci/cluster/tests/e2e-template-assets/azureStackHCIHost/modules/subscriptionRoleAssignment.bicep @@ -0,0 +1,16 @@ +targetScope = 'subscription' + +param principalId string + +// assign principalId the contributor role on the subscription +resource subscriptionRoleAssignment 'Microsoft.Authorization/roleAssignments@2020-04-01-preview' = { + name: guid(principalId, subscription().id, 'Contributor') + properties: { + principalId: principalId + principalType: 'ServicePrincipal' + roleDefinitionId: subscriptionResourceId( + 'Microsoft.Authorization/roleDefinitions', + 'b24988ac-6180-42a0-ab88-20f7382dd24c' + ) + } +} diff --git a/avm/res/azure-stack-hci/cluster/tests/e2e-template-assets/azureStackHCIHost/scripts/hciHostStage1.ps1 b/avm/res/azure-stack-hci/cluster/tests/e2e-template-assets/azureStackHCIHost/scripts/hciHostStage1.ps1 new file mode 100644 index 0000000000..d62fea3953 --- /dev/null +++ b/avm/res/azure-stack-hci/cluster/tests/e2e-template-assets/azureStackHCIHost/scripts/hciHostStage1.ps1 @@ -0,0 +1,53 @@ +Function log { + Param ( + [string]$message, + [string]$logPath = 'C:\temp\hciHostDeploy-1.log' + ) + + If (!(Test-Path -Path C:\temp)) { + New-Item -Path C:\temp -ItemType Directory + } + + Write-Host $message + Add-Content -Path $logPath -Value "$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss') [hciHostStage1] - $message" +} + +$ErrorActionPreference = 'Stop' + +# prep host - install hyper-v, AD, DHCP, RRAS +log 'Installing required features and roles...' +$features = @('rsat-hyper-v-tools', 'rsat-clustering', 'rsat-adds', 'rsat-dns-server', 'RSAT-RemoteAccess-Mgmt', 'Routing', 'AD-Domain-Services', 'DHCP') +$missingFeatures = Get-WindowsFeature -Name $features | Where-Object { $_.Installed -eq $false } + +ForEach ($missingFeature in $missingFeatures) { + log "Installing $($missingFeature.Name)..." + Add-WindowsFeature -Name $missingFeature.Name -IncludeAllSubFeature -IncludeManagementTools + + If ($?) { + log "Successfully installed $($missingFeature.Name)" + } Else { + log "Failed to install $($missingFeature.Name)" + } + If (Test-Path -Path C:\Windows\winsxs\pending.xml) { + log 'Reboot required, exiting...' + } +} + +log 'Enabling Hyper-V...' +Enable-WindowsOptionalFeature -Online -FeatureName 'microsoft-hyper-v-online' -All -NoRestart + +# create temp directory +log 'Creating temp directory...' +If (!(Test-Path -Path C:\temp)) { + New-Item -Path C:\temp -ItemType Directory +} + +# create reboot status file +If (Test-Path -Path 'C:\temp\Reboot1Completed.status') { + log 'Reboot has already been completed, skipping...' +} ElseIf (Test-Path -Path 'C:\temp\Reboot1Initiated.status') { + log 'Reboot has already been initiated, skipping...' +} Else { + log 'Reboot required, creating status file...' + Set-Content -Path 'C:\temp\Reboot1Required.status' -Value 'Reboot 1 Required' +} diff --git a/avm/res/azure-stack-hci/cluster/tests/e2e-template-assets/azureStackHCIHost/scripts/hciHostStage2.ps1 b/avm/res/azure-stack-hci/cluster/tests/e2e-template-assets/azureStackHCIHost/scripts/hciHostStage2.ps1 new file mode 100644 index 0000000000..cf78579414 --- /dev/null +++ b/avm/res/azure-stack-hci/cluster/tests/e2e-template-assets/azureStackHCIHost/scripts/hciHostStage2.ps1 @@ -0,0 +1,40 @@ +Function log { + Param ( + [string]$message, + [string]$logPath = 'C:\temp\hciHostDeploy-2.log' + ) + + If (!(Test-Path -Path C:\temp)) { + New-Item -Path C:\temp -ItemType Directory + } + + Write-Host $message + Add-Content -Path $logPath -Value "$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss') [hciHostStage2] - $message" +} + +$ErrorActionPreference = 'Stop' + +# check for reboot status file, reboot if needed +If (Test-Path -Path 'C:\temp\Reboot1Required.status') { + log 'Reboot 1 is required' + + Remove-Item 'C:\temp\Reboot1Required.status' + Set-Content -Path 'C:\temp\Reboot1Initiated.status' -Value 'Reboot 1 Initiated' + + # use scheduled task to reboot the machine, ensuring the runCommand exits gracefully + $action = New-ScheduledTaskAction -Execute 'shutdown.exe' -Argument '-r -f -t 0' + $trigger = New-ScheduledTaskTrigger -Once -At (Get-Date).AddMinutes(2) + $principal = New-ScheduledTaskPrincipal -UserId 'NT AUTHORITY\SYSTEM' -LogonType ServiceAccount + $task = New-ScheduledTask -Action $action -Description 'Reboot 1' -Trigger $trigger -Principal $principal + Register-ScheduledTask -TaskName 'Reboot1' -InputObject $task +} ElseIf (Test-Path -Path 'C:\temp\Reboot1Initiated.status') { + log 'Reboot 1 has been initiated and now completed' + + Remove-Item 'C:\temp\Reboot1Initiated.status' + Set-Content -Path 'C:\temp\Reboot1Completed.status' -Value 'Reboot 1 Completed' + + +} ElseIf (Test-Path -Path 'C:\temp\Reboot1Completed.status') { + log 'Reboot 1 has been completed' + +} diff --git a/avm/res/azure-stack-hci/cluster/tests/e2e-template-assets/azureStackHCIHost/scripts/hciHostStage3.ps1 b/avm/res/azure-stack-hci/cluster/tests/e2e-template-assets/azureStackHCIHost/scripts/hciHostStage3.ps1 new file mode 100644 index 0000000000..e10b790976 --- /dev/null +++ b/avm/res/azure-stack-hci/cluster/tests/e2e-template-assets/azureStackHCIHost/scripts/hciHostStage3.ps1 @@ -0,0 +1,333 @@ +[CmdletBinding()] +param ( + [Parameter()] + [string] + $hciVHDXDownloadURL, + + [Parameter()] + [string] + $hciISODownloadURL, + + [Parameter()] + [ValidateRange(1, 16)] + [int] + $hciNodeCount +) +Function log { + Param ( + [string]$message, + [string]$logPath = 'C:\temp\hciHostDeploy-3.log' + ) + + If (!(Test-Path -Path C:\temp)) { + New-Item -Path C:\temp -ItemType Directory + } + + Write-Host $message + Add-Content -Path $logPath -Value "$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss') [hciHostStage3] - $message" +} + +Function Test-ADConnection { + try { + If ((Get-Service -Name 'ADWS' -ErrorAction SilentlyContinue).Status -ne 'Running') { return $false } + $env:ADPS_LoadDefaultDrive = 0 + Import-Module -Name ActiveDirectory -ErrorAction Stop + [bool](Get-ADDomainController -Server $env:COMPUTERNAME -ErrorAction SilentlyContinue) + } catch { + $false + } +} + +# THANKS https://github.com/bfrankMS/CreateHypervVms/blob/master/Scenario-AzStackHCI/CreateVhdxFromIso.ps1 +Function New-VHDXFromISO { + # Parameter help description + param( + [Parameter(ParameterSetName = 'SRC', Mandatory = $true, ValueFromPipeline = $true)] + [Alias('ISO')] + [string] + [ValidateNotNullOrEmpty()] + [ValidateScript({ Test-Path $(Resolve-Path $_) })] + $IsoPath, + + [Parameter(ParameterSetName = 'SRC')] + [Alias('VHD')] + [string] + [ValidateNotNullOrEmpty()] + $VhdxPath, + + [Parameter(ParameterSetName = 'SRC')] + [Alias('Size')] + [UInt64] + [ValidateNotNullOrEmpty()] + [ValidateRange(512MB, 64TB)] + $SizeBytes = 120GB, + + [Parameter(ParameterSetName = 'SRC')] + [Alias('Index')] + [UInt64] + [ValidateNotNullOrEmpty()] + [ValidateRange(1, 10)] + $ImageIndex = 1 #defaults to azure stack hci on 2405 image + ) + + $BCDBoot = 'bcdboot.exe' + $VHDFormat = 'VHDX' + $TempDirectory = $env:Temp + + function + Write-ActionInfo { + # Function to make the Write-Host output a bit prettier. + [CmdletBinding()] + param( + [Parameter(Mandatory = $true, ValueFromPipeline = $true)] + [string] + [ValidateNotNullOrEmpty()] + $text + ) + Write-Host "INFO : $($text)" -ForegroundColor White + } + + function + Start-Executable { + <# + .SYNOPSIS + Runs an external executable file, and validates the error level. + + .PARAMETER Executable + The path to the executable to run and monitor. + + .PARAMETER Arguments + An array of arguments to pass to the executable when it's executed. + + .PARAMETER SuccessfulErrorCode + The error code that means the executable ran successfully. + The default value is 0. + #> + + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string] + [ValidateNotNullOrEmpty()] + $Executable, + + [Parameter(Mandatory = $true)] + [string[]] + [ValidateNotNullOrEmpty()] + $Arguments, + + [Parameter()] + [int] + [ValidateNotNullOrEmpty()] + $SuccessfulErrorCode = 0 + + ) + + Write-ActionInfo "Running $Executable $Arguments" + $ret = Start-Process ` + -FilePath $Executable ` + -ArgumentList $Arguments ` + -NoNewWindow ` + -Wait ` + -RedirectStandardOutput "$($TempDirectory)\$($scriptName)\$($sessionKey)\$($Executable)-StandardOutput.txt" ` + -RedirectStandardError "$($TempDirectory)\$($scriptName)\$($sessionKey)\$($Executable)-StandardError.txt" ` + -PassThru + + Write-ActionInfo "Return code was $($ret.ExitCode)." + + if ($ret.ExitCode -ne $SuccessfulErrorCode) { + throw "$Executable failed with code $($ret.ExitCode)!" + } + } + + Write-ActionInfo 'Creating virtual hard disk...' + $newVhd = New-VHD -Path $VhdxPath -SizeBytes $SizeBytes -Dynamic + + Write-ActionInfo "Mounting $VHDFormat..." + $disk = $newVhd | Mount-VHD -Passthru | Get-Disk + + + # UEFI : 3 partitions : efi - msr - windows + <#https://learn.microsoft.com/de-de/windows/win32/api/winioctl/ns-winioctl-partition_information_gpt +PARTITION_BASIC_DATA_GUID - ebd0a0a2-b9e5-4433-87c0-68b6b72699c7 +PARTITION_SYSTEM_GUID - c12a7328-f81f-11d2-ba4b-00a0c93ec93b # EFI +PARTITION_MSFT_RESERVED_GUID - e3c9e316-0b5c-4db8-817d-f92df00215ae # MSR +PARTITION_MSFT_RECOVERY_GUID - de94bba4-06d1-4d40-a16a-bfd50179d6ac # Recovery partition (Windows) we are not using this +#> + + try { + Write-ActionInfo 'Initializing disk...' + Initialize-Disk -Number $disk.Number -PartitionStyle GPT + + Write-ActionInfo 'Creating EFI system partition...' + $systemPartition = New-Partition -DiskNumber $disk.Number -Size 200MB -GptType '{ebd0a0a2-b9e5-4433-87c0-68b6b72699c7}' + + Write-ActionInfo 'Formatting EFI system volume...' + $null = Format-Volume -Partition $systemPartition -FileSystem FAT32 -Force -Confirm:$false + + Write-ActionInfo 'Setting EFI system partition PARTITION_SYSTEM_GUID...' + $systemPartition | Set-Partition -GptType '{c12a7328-f81f-11d2-ba4b-00a0c93ec93b}' + $systemPartition | Add-PartitionAccessPath -AssignDriveLetter + + # Create the reserved partition + Write-ActionInfo 'Creating MSR partition...' + $null = New-Partition -DiskNumber $disk.Number -Size 128MB -GptType '{e3c9e316-0b5c-4db8-817d-f92df00215ae}' -Verbose + + # Create the Windows partition + Write-ActionInfo 'Creating windows partition...' + $windowsPartition = New-Partition -DiskNumber $disk.Number -UseMaximumSize -GptType '{ebd0a0a2-b9e5-4433-87c0-68b6b72699c7}' + + Write-ActionInfo 'Formatting windows volume...' + $windowsVolume = Format-Volume -Partition $windowsPartition -FileSystem NTFS -Force -Confirm:$false + + # Assign drive letter to Windows partition. This is required for bcdboot + $windowsPartition | Add-PartitionAccessPath -AssignDriveLetter + $windowsDrive = $(Get-Partition -Volume $windowsVolume).AccessPaths[0].substring(0, 2) + Write-ActionInfo "Windows path ($windowsDrive) has been assigned." + + # Refresh access paths (we have now formatted the volume) + $systemPartition = $systemPartition | Get-Partition + $systemDrive = $systemPartition.AccessPaths[0].trimend('\').replace('\?', '??') + Write-ActionInfo "System volume location: $systemDrive" + + # Mount .iso to get the install.wim + $beforeMount = (Get-Volume).DriveLetter -split ' ' + $null = Mount-DiskImage -StorageType ISO -ImagePath $IsoPath + $afterMount = (Get-Volume).DriveLetter -split ' ' + $setuppath = (Compare-Object $beforeMount $afterMount -PassThru ) + Write-ActionInfo "Mounted .iso to $($setuppath):" + + Write-ActionInfo "Applying image from .iso $("$setuppath"+':\sources\install.wim') to $VHDFormat. This could take a while..." + + Expand-WindowsImage -ApplyPath $windowsDrive -ImagePath "$setuppath`:\sources\install.wim" -Index $ImageIndex #-LogPath "$($logFolder)\DismLogs.log" | Out-Null + Write-ActionInfo 'Image was applied successfully. ' + + Write-ActionInfo 'Making image bootable...' + $bcdBootArgs = @( + "$($windowsDrive)\Windows", # Path to the \Windows on the VHD + "/s $systemDrive", # Specifies the volume letter of the drive to create the \BOOT folder on. + '/v' # Enabled verbose logging. + ) + $bcdBootArgs += '/f UEFI' # Specifies the firmware type of the target system partition + + Start-Executable -Executable $BCDBoot -Arguments $bcdBootArgs + + Write-ActionInfo 'Drive is bootable. Cleaning up...' + + # Remove system partition access path, if necessary + $systemPartition | Remove-PartitionAccessPath -AccessPath $systemPartition.AccessPaths[0] + } finally { + Write-ActionInfo "Dismounting $VHDFormat..." + Dismount-VHD -Path $VhdxPath + + #ejecting .iso - releasing drive letter. + Write-ActionInfo 'Dismounting .iso...' + Dismount-DiskImage -ImagePath $IsoPath + } +} + +$ErrorActionPreference = 'Stop' + +# download HCI VHDX or ISO +If (!(Test-Path -Path 'c:\ISOs')) { + log 'Creating c:\ISOs directory...' + mkdir c:\ISOs +} Else { + log 'ISOs directory already exists, skipping...' +} + +If ($hciVHDXDownloadURL) { + log 'Downloading HCI VHDX...' + If (! (Test-Path c:\ISOs\hci_os.vhdx)) { + [System.Net.WebClient]::new().DownloadFile($hciVHDXDownloadURL, 'c:\isos\hci_os.vhdx') + } Else { + log 'HCI VHDX already exists, skipping download...' + } +} ElseIf ($hciISODownloadURL) { + log 'Downloading HCI ISO...' + If (! (Test-Path c:\ISOs\hci_os.iso)) { + [System.Net.WebClient]::new().DownloadFile($hciISODownloadURL, 'c:\isos\hci_os.iso') + } Else { + log 'HCI ISO already exists, skipping download...' + } + + # convert ISO to VHDX + If (!(Test-Path 'c:\isos\hci_os.vhdx')) { + log 'Converting ISO to VHDX...' + New-VHDXFromISO -IsoPath 'c:\isos\hci_os.iso' -VhdxPath 'c:\isos\hci_os.vhdx' + } Else { + log 'HCI VHDX already exists, skipping conversion...' + + } +} Else { + log 'No download URL provided, cannot continue...' + Write-Error 'No download URL provided, cannot continue...' -ErrorAction Stop +} + +# create mount point directories on C:\ +log 'Creating mount points...' +For ($i = 0; $i -lt $hciNodeCount; $i++) { + $dirPath = "c:\diskmounts\hcinode$($i + 1)" + If (!(Test-Path -Path $dirPath)) { + mkdir $dirPath + } Else { + log "Mount point '$dirPath' already exists, skipping..." + } +} + +# format and mount disks +log 'Formatting and mounting disks...' +$count = 0 +$rawDisks = Get-Disk | Where-Object PartitionStyle -EQ 'RAW' +$rawDisks | + Initialize-Disk -PartitionStyle GPT -PassThru | + New-Partition -UseMaximumSize -AssignDriveLetter:$false | + Format-Volume -FileSystem NTFS | + Get-Partition | + Where-Object { $_.type -ne 'Reserved' } | + ForEach-Object { $count++; mountvol c:\diskMounts\HCINode$count $_.accesspaths[0] } + +log 'Copying VHDX to mount points...' +For ($i = 0; $i -lt $hciNodeCount; $i++) { + If (!(Test-Path -Path "c:\diskmounts\hcinode$($i + 1)\hci_os.vhdx")) { + Copy-Item -Path c:\isos\hci_os.vhdx -Destination "c:\diskmounts\hcinode$($i + 1)" + } Else { + log "HCI VHDX already exists at 'c:\diskmounts\hcinode$($i + 1)', skipping..." + } +} + +# install RRAS configure for routing +log 'Installing RRAS and configuring for routing...' +While (!(Test-Path -Path 'C:\Windows\System32\WindowsPowerShell\v1.0\Modules\RemoteAccess\RemoteAccess.psd1')) { + Start-Sleep -Seconds 5 + log 'Waiting for RRAS module to be available...' +} +Import-Module 'C:\Windows\System32\WindowsPowerShell\v1.0\Modules\RemoteAccess\RemoteAccess.psd1' +Install-RemoteAccess -VpnType RoutingOnly +Set-Service -Name RemoteAccess -StartupType Automatic -PassThru | Start-Service + +# install domain controller +log 'Checking whether AD is installed...' +If (!(Test-ADConnection)) { + log 'AD is not installed, installing...' + Import-Module 'C:\Windows\System32\WindowsPowerShell\v1.0\Modules\ADDSDeployment\ADDSDeployment.psd1' + + $ADRecoveryPassword = ConvertTo-SecureString -Force -AsPlainText (New-Guid).guid + Install-ADDSForest -DomainName hci.local -DomainNetbiosName hci -ForestMode Default -DomainMode Default -InstallDns:$true -SafeModeAdministratorPassword $ADRecoveryPassword -NoRebootOnCompletion:$true -Force:$true +} Else { + log 'AD is already installed, skipping...' +} + +log 'Adding DNS forwarders...' +Import-Module 'C:\Windows\System32\WindowsPowerShell\v1.0\Modules\DnsServer\DnsServer.psd1' +Add-DnsServerForwarder -IPAddress 8.8.8.8 + +# create reboot status file +If (Test-Path -Path 'C:\temp\Reboot2Completed.status') { + log 'Reboot has already been completed, skipping...' +} ElseIf (Test-Path -Path 'C:\temp\Reboot2Initiated.status') { + log 'Reboot has already been initiated, skipping...' +} Else { + log 'Reboot required, creating status file...' + Set-Content -Path 'C:\temp\Reboot2Required.status' -Value 'Reboot 2 Required' +} diff --git a/avm/res/azure-stack-hci/cluster/tests/e2e-template-assets/azureStackHCIHost/scripts/hciHostStage4.ps1 b/avm/res/azure-stack-hci/cluster/tests/e2e-template-assets/azureStackHCIHost/scripts/hciHostStage4.ps1 new file mode 100644 index 0000000000..e6420c7d14 --- /dev/null +++ b/avm/res/azure-stack-hci/cluster/tests/e2e-template-assets/azureStackHCIHost/scripts/hciHostStage4.ps1 @@ -0,0 +1,41 @@ + +Function log { + Param ( + [string]$message, + [string]$logPath = 'C:\temp\hciHostDeploy-4.log' + ) + + If (!(Test-Path -Path C:\temp)) { + New-Item -Path C:\temp -ItemType Directory + } + + Write-Host $message + Add-Content -Path $logPath -Value "$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss') [hciHostStage4] - $message" +} + +$ErrorActionPreference = 'Stop' + +# check for reboot status file, reboot if needed +If (Test-Path -Path 'C:\temp\Reboot2Required.status') { + log 'Reboot 2 is required' + + Remove-Item 'C:\temp\Reboot2Required.status' + Set-Content -Path 'C:\temp\Reboot2Initiated.status' -Value 'Reboot 2 Initiated' + + # use scheduled task to reboot the machine, ensuring the runCommand exits gracefully + $action = New-ScheduledTaskAction -Execute 'shutdown.exe' -Argument '-r -f -t 0' + $trigger = New-ScheduledTaskTrigger -Once -At (Get-Date).AddMinutes(2) + $principal = New-ScheduledTaskPrincipal -UserId 'NT AUTHORITY\SYSTEM' -LogonType ServiceAccount + $task = New-ScheduledTask -Action $action -Description 'Reboot 2' -Trigger $trigger -Principal $principal + Register-ScheduledTask -TaskName 'Reboot2' -InputObject $task + +} ElseIf (Test-Path -Path 'C:\temp\Reboot2Initiated.status') { + log 'Reboot 2 has been initiated and now completed' + + Remove-Item 'C:\temp\Reboot2Initiated.status' + Set-Content -Path 'C:\temp\Reboot2Completed.status' -Value 'Reboot 2 Completed' + +} ElseIf (Test-Path -Path 'C:\temp\Reboot2Completed.status') { + log 'Reboot 2 has been completed' + +} diff --git a/avm/res/azure-stack-hci/cluster/tests/e2e-template-assets/azureStackHCIHost/scripts/hciHostStage5.ps1 b/avm/res/azure-stack-hci/cluster/tests/e2e-template-assets/azureStackHCIHost/scripts/hciHostStage5.ps1 new file mode 100644 index 0000000000..8f8b2ec203 --- /dev/null +++ b/avm/res/azure-stack-hci/cluster/tests/e2e-template-assets/azureStackHCIHost/scripts/hciHostStage5.ps1 @@ -0,0 +1,398 @@ +[CmdletBinding()] +param ( + [Parameter()] + [string] + $adminUsername, + + [Parameter()] + [string] + $adminPw, + + [Parameter()] + [int] + $hciNodeCount, + + [Parameter()] + [string] + $switchlessStorageConfig = 'switched' +) + +Function log { + Param ( + [string]$message, + [string]$logPath = 'C:\temp\hciHostDeploy-5.log' + ) + + If (!(Test-Path -Path C:\temp)) { + New-Item -Path C:\temp -ItemType Directory + } + + Write-Host $message + Add-Content -Path $logPath -Value "$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss') [hciHostStage5] - $message" +} + +Function Test-ADConnection { + try { + If ((Get-Service -Name 'ADWS' -ErrorAction SilentlyContinue).Status -ne 'Running') { return $false } + $env:ADPS_LoadDefaultDrive = 0 + Import-Module -Name ActiveDirectory -ErrorAction Stop + [bool](Get-ADDomainController -Server $env:COMPUTERNAME -ErrorAction SilentlyContinue) + } catch { + $false + } +} + +$ErrorActionPreference = 'Stop' + +# create hyperv switches +log 'Creating Hyper-V switches...' +$existingSwitches = Get-VMSwitch + +If ($switchlessStorageConfig -eq 'switched') { + log 'Creating Hyper-V switches for switched storage configuration...' + If ($existingSwitches.Name -notcontains 'external' ) { New-VMSwitch -Name external -AllowManagementOS:$true -NetAdapterName Ethernet } + If ($existingSwitches.Name -notcontains 'hciNodeCompInternal' ) { New-VMSwitch -Name hciNodeCompInternal -SwitchType Internal -EnableIov $true } + If ($existingSwitches.Name -notcontains 'hciNodeMgmtInternal' ) { New-VMSwitch -Name hciNodeMgmtInternal -SwitchType Internal -EnableIov $true } + If ($existingSwitches.Name -notcontains 'hciNodeStoragePrivate' ) { New-VMSwitch -Name hciNodeStoragePrivate -SwitchType Private -EnableIov $true } +} ElseIf ($switchlessStorageConfig -eq 'switchless') { + If ($hciNodeCount -gt 3) { + log -message 'ERROR: Switchless storage configuration is only supported for 3 or fewer HCI nodes. Exiting script...' + Write-Error 'ERROR: Switchless storage configuration is only supported for 3 or fewer HCI nodes. Exiting script...' + exit 1 + } + + log 'Creating Hyper-V switches for switchless storage configuration...' + If ($existingSwitches.Name -notcontains 'external' ) { New-VMSwitch -Name external -AllowManagementOS:$true -NetAdapterName Ethernet } + If ($existingSwitches.Name -notcontains 'hciNodeCompInternal' ) { New-VMSwitch -Name hciNodeCompInternal -SwitchType Internal -EnableIov $true } + If ($existingSwitches.Name -notcontains 'hciNodeMgmtInternal' ) { New-VMSwitch -Name hciNodeMgmtInternal -SwitchType Internal -EnableIov $true } + If ($existingSwitches.Name -notcontains 'hciNodeStoragePrivateA' ) { New-VMSwitch -Name hciNodeStoragePrivateA -SwitchType Private -EnableIov $true } + If ($existingSwitches.Name -notcontains 'hciNodeStoragePrivateB' ) { New-VMSwitch -Name hciNodeStoragePrivateB -SwitchType Private -EnableIov $true } + If ($existingSwitches.Name -notcontains 'hciNodeStoragePrivateC' ) { New-VMSwitch -Name hciNodeStoragePrivateC -SwitchType Private -EnableIov $true } +} + +# add IPs for host +log 'Adding IPs for host...' +$existingIPs = Get-NetIPAddress +If ($existingIPs.IPAddress -notcontains '172.20.0.1') { New-NetIPAddress -InterfaceAlias 'vEthernet (hciNodeMgmtInternal)' -IPAddress 172.20.0.1 -PrefixLength 24 } +If ($existingIPs.IPAddress -notcontains '10.20.0.1') { New-NetIPAddress -InterfaceAlias 'vEthernet (hciNodeCompInternal)' -IPAddress 10.20.0.1 -PrefixLength 24 } + +# configure NAT +log 'Restarting RemoteAccess service...' +Restart-Service RemoteAccess + +log 'Configuring NAT...' + +netsh routing ip nat uninstall +netsh routing ip nat install +netsh routing ip nat set global tcptimeoutmins=1440 udptimeoutmins=1 loglevel=ERROR +netsh routing ip nat add interface name="vEthernet (external)" mode=FULL +If (!$?) { + $message = "Failed to run netsh command: ''netsh routing ip nat add interface name='vEthernet (external)' mode=FULL''." + log $message + Write-Error $message +} +netsh routing ip nat add interface name="vEthernet (hciNodeCompInternal)" mode=PRIVATE +If (!$?) { + $message = "Failed to run netsh command: ''netsh routing ip nat add interface name='vEthernet (hciNodeCompInternal)' mode=PRIVATE''." + log $message + Write-Error $message +} +netsh routing ip nat add interface name="vEthernet (hciNodeMgmtInternal)" mode=PRIVATE +If (!$?) { + $message = "Failed to run netsh command: ''netsh routing ip nat add interface name='vEthernet (hciNodeMgmtInternal)' mode=PRIVATE''." + log $message + Write-Error $message +} + +# create DHCP scopes +log 'Creating DHCP scopes...' +$existingScopes = Get-DhcpServerv4Scope +If ($existingScopes.name -notcontains 'HCIComp') { Add-DhcpServerv4Scope -StartRange 10.20.0.10 -EndRange 10.20.0.250 -Name HCIComp -State Active -SubnetMask 255.255.255.0 } +If ($existingScopes.name -notcontains 'HCIMgmt') { Add-DhcpServerv4Scope -StartRange 172.20.0.10 -EndRange 172.20.0.250 -Name HCIMgmt -State Active -SubnetMask 255.255.255.0 } + +# test DC connectivity before attempting to authorize DHCP server in AD +log 'Testing DC connectivity...' +$count = 0 +While (!(Test-ADConnection) -and $count -lt 120) { + Start-Sleep -Seconds 5 + log 'Waiting for AD Web Services to be available...' + $count++ +} + +# authorize DHCP servers in AD for DNS updates +log 'Authorizing DHCP servers in AD for DNS updates...' +try { + $existingAuthorizedServers = Get-DhcpServerInDC -ErrorAction Stop +} catch { + log 'Failed to query authorized DHCP servers in AD. Waiting 120 seconds before retrying...' + Start-Sleep -Seconds 120 + $existingAuthorizedServers = Get-DhcpServerInDC +} + +If ($existingAuthorizedServers.IPAddress -notcontains '172.20.0.1') { Add-DhcpServerInDC -DnsName "$($env:COMPUTERNAME).hci.local" -IPAddress 172.20.0.1 } +If ($existingAuthorizedServers.IPAddress -notcontains '10.20.0.1') { Add-DhcpServerInDC -DnsName "$($env:COMPUTERNAME).hci.local" -IPAddress 10.20.0.1 } + +# set router and dns options for mgmt DHCP scope +log 'Setting router and dns options for mgmt DHCP scope...' +Set-DhcpServerv4OptionValue -ScopeId 172.20.0.0 -DnsDomain hci.local -DnsServer 172.20.0.1 -Router 172.20.0.1 + +# create HCI node VMs +log 'Creating HCI node VMs...' +$existingVMs = Get-VM +For ($i = 1; $i -le $hciNodeCount; $i++) { + $hciNodeName = "hcinode$i" + $hciNodePath = "C:\diskMounts\$hciNodeName" + + If ($existingVMs.name -notcontains $hciNodeName) { New-VM -Name $hciNodeName -MemoryStartupBytes 32GB -BootDevice VHD -SwitchName hciNodeMgmtInternal -Path C:\diskMounts\ -VHDPath "$hciNodePath\hci_os.vhdx" -Generation 2 } +} + +# configure HCI node VMs +log 'Configuring HCI node VMs...' +log 'Setting VM processor count to 16 and enabling virtualization extensions...' +Get-VM | Set-VMProcessor -ExposeVirtualizationExtensions $true -Count 8 + +log 'Setting VM key protector and enabling TPM...' +Get-VM | ForEach-Object { + If (($_ | Get-VMKeyProtector).Length -eq 4) { + log "Adding key protector for VM '$($_.Name)'" + $_ | Set-VMKeyProtector -NewLocalKeyProtector + } Else { + log "Key protector already exists for VM '$($_.Name)'" + } + + If (($_ | Get-VMSecurity).TpmEnabled -eq $false) { + log "Enabling TPM for VM '$($_.Name)'" + $_ | Enable-VMTPM + } Else { + log "TPM already enabled for VM '$($_.Name)'" + } +} + + +# rename nic to mgmt +log 'Renaming first network adapter on HCI nodes...' +if (Get-VMNetworkAdapter -Name 'Network Adapter' -VMName * -ErrorAction SilentlyContinue) { Rename-VMNetworkAdapter -NewName mgmt -VMName * -Name 'Network Adapter' } +Get-VM | Get-VMNetworkAdapter -Name 'mgmt' | Set-VMNetworkAdapter -DeviceNaming On + +# add additional NICs to HCI node VMs +log 'Adding additional NICs to HCI node VMs...' +ForEach ($existingVM in (Get-VM)) { + $existingNICs = Get-VMNetworkAdapter -VM $existingVM + If ($existingNICs.name -notcontains 'comp0') { $existingVM | Add-VMNetworkAdapter -Name comp0 -SwitchName hciNodeCompInternal -DeviceNaming On } + If ($existingNICs.name -notcontains 'comp1') { $existingVM | Add-VMNetworkAdapter -Name comp1 -SwitchName hciNodeCompInternal -DeviceNaming On } + + If ($switchlessStorageConfig -eq 'switched') { + log "Adding NICs to VM '$($existingVM.name)' for switched storage configuration..." + If ($existingNICs.name -notcontains 'smb0') { $existingVM | Add-VMNetworkAdapter -Name smb0 -SwitchName hciNodeStoragePrivate -DeviceNaming On -Passthru | Set-VMNetworkAdapterVlan -Trunk -AllowedVlanIdList '711' -NativeVlanId 0 } + If ($existingNICs.name -notcontains 'smb1') { $existingVM | Add-VMNetworkAdapter -Name smb1 -SwitchName hciNodeStoragePrivate -DeviceNaming On -Passthru | Set-VMNetworkAdapterVlan -Trunk -AllowedVlanIdList '712' -NativeVlanId 0 } + } ElseIf ($switchlessStorageConfig -eq 'switchless') { + log "Adding NICs to VM '$($existingVM.name)' for switchless storage configuration..." + + switch ($existingVM.Name[-1]) { + 1 { + If ($existingNICs.name -notcontains 'smb0') { $existingVM | Add-VMNetworkAdapter -Name smb0 -SwitchName hciNodeStoragePrivateA -DeviceNaming On -Passthru | Set-VMNetworkAdapterVlan -Trunk -AllowedVlanIdList '711' -NativeVlanId 0 } + If ($existingNICs.name -notcontains 'smb1') { $existingVM | Add-VMNetworkAdapter -Name smb1 -SwitchName hciNodeStoragePrivateB -DeviceNaming On -Passthru | Set-VMNetworkAdapterVlan -Trunk -AllowedVlanIdList '711' -NativeVlanId 0 } + } + 2 { + If ($existingNICs.name -notcontains 'smb0') { $existingVM | Add-VMNetworkAdapter -Name smb0 -SwitchName hciNodeStoragePrivateA -DeviceNaming On -Passthru | Set-VMNetworkAdapterVlan -Trunk -AllowedVlanIdList '711' -NativeVlanId 0 } + If ($existingNICs.name -notcontains 'smb1') { $existingVM | Add-VMNetworkAdapter -Name smb1 -SwitchName hciNodeStoragePrivateC -DeviceNaming On -Passthru | Set-VMNetworkAdapterVlan -Trunk -AllowedVlanIdList '711' -NativeVlanId 0 } + } + 3 { + If ($existingNICs.name -notcontains 'smb0') { $existingVM | Add-VMNetworkAdapter -Name smb0 -SwitchName hciNodeStoragePrivateB -DeviceNaming On -Passthru | Set-VMNetworkAdapterVlan -Trunk -AllowedVlanIdList '711' -NativeVlanId 0 } + If ($existingNICs.name -notcontains 'smb1') { $existingVM | Add-VMNetworkAdapter -Name smb1 -SwitchName hciNodeStoragePrivateC -DeviceNaming On -Passthru | Set-VMNetworkAdapterVlan -Trunk -AllowedVlanIdList '711' -NativeVlanId 0 } + } + Default {} + } + } +} + +# add disks to HCI node VMs +log 'Adding disks to HCI node VMs...' +Foreach ($vm in (Get-VM)) { + (1..4) | ForEach-Object { + $diskPath = "C:\diskMounts\$($vm.Name)\hciNodeDisk$($_).vhdx" + If (!(Test-Path -Path $diskPath)) { + log "Creating disk: $diskPath" + New-VHD -Path $diskPath -SizeBytes 1TB -Dynamic | Out-Null + } + If ($VM.HardDrives.Path -notcontains $diskPath) { + log "Adding disk: $diskPath to VM: $($vm.Name)" + Add-VMHardDiskDrive -VMName $vm.Name -ControllerType SCSI -ControllerNumber 0 -ControllerLocation $_ -Path $diskPath | Out-Null + } + } +} + +# enable mac soofing on HCI node VMs +log 'Enabling MAC spoofing on HCI node VMs...' +Get-VM | Get-VMNetworkAdapter | Set-VMNetworkAdapter -MacAddressSpoofing On + +# define unattend.xml for HCI node VMs template +$unattendSource = @' + + + + + $hciNodeName + Organization + Owner + UTC + + + false + + + 1 + + + false + + + + + + true + + + + + + + + + true + true + true + Work + 1 + + + + powershell.exe -command "Get-NetAdapterAdvancedProperty -DisplayName 'Hyper-V Network Adapter Name' | Foreach-Object {`$_ | Get-NetAdapter | Rename-NetAdapter -NewName `$_.DisplayValue}" + 1 + false + + + powershell.exe -command "Enable-WindowsOptionalFeature -Online -FeatureName 'microsoft-hyper-v-online' -all -NoRestart" + 2 + false + + + powershell.exe -command "Remove-Item -Path 'C:\unattend.xml' -Force" + 3 + false + + + shutdown -r -f -t 0 + 4 + false + + + + Administrator + true + 1 + + $adminPw +

true</PlainText> + </Password> + </AutoLogon> + <UserAccounts> + <AdministratorPassword> + <Value>$adminPw</Value> + <PlainText>True</PlainText> + </AdministratorPassword> + <LocalAccounts> + <LocalAccount wcm:action="add"> + <Password> + <Value>$adminPw</Value> + <PlainText>true</PlainText> + </Password> + <Description>HCI Admin User</Description> + <DisplayName>$adminUsername</DisplayName> + <Group>Administrators;Power Users</Group> + <Name>$adminUsername</Name> + </LocalAccount> + </LocalAccounts> + </UserAccounts> + </component> + <component name="Microsoft-Windows-International-Core" processorArchitecture="amd64" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS" xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> + <InputLocale>en-us</InputLocale> + <SystemLocale>en-us</SystemLocale> + <UILanguage>en-us</UILanguage> + <UILanguageFallback>en-us</UILanguageFallback> + <UserLocale>en-us</UserLocale> + </component> + </settings> +</unattend> +'@ + +# inject updated sysp answer file into each HCI node disk +log 'Injecting updated sysprep answer file into each HCI node disk...' +For ($i = 1; $i -le $hciNodeCount; $i++) { + $hciNodeName = "hciNode$i" + $hciProductKey = '' + + Push-Location c:\diskMounts\$hciNodeName + + If (!(Test-Path -Path unattend_injected.status) -and (Get-VM -Name $hciNodeName).State -eq 'Off') { + log "Injecting unattend.xml into HCI node disk '$hciNodeName'..." + $mountedVolume = Mount-VHD .\hci_os.vhdx -Passthru | Get-Disk | Get-Partition | Get-Volume | Where-Object FileSystemType -EQ 'NTFS' + + $clone = $unattendSource.psobject.copy() + $clone = $ExecutionContext.InvokeCommand.ExpandString($clone) + + Set-Content -Path "$($mountedVolume.DriveLetter):\unattend.xml" -Value $clone -Force + + Dismount-VHD .\hci_os.vhdx + + Set-Content 'unattend_injected.status' -Value 'Unattend.xml injected' + } Else { + log "Unattend.xml already injected into HCI node disk '$hciNodeName'." + } + + Pop-Location +} + +# start HCI node VMs +If (Get-VM | Where-Object State -EQ 'Off') { + log 'Starting HCI node VMs...' + try { + $errorActionPreference = 'Stop' + Get-VM | Start-VM + } catch { + log "Failed to start HCI node VMs. $_" + Write-Error "Failed to start HCI node VMs. $_" + } + + #wait for vms to boot + log 'Waiting 300s for VMs to boot and apply sysprep...' + Start-Sleep -Seconds 300 +} Else { + log 'HCI node VMs are already running.' +} + +# create DHCP reservations for HCI node VMs management interfaces +log 'Checking DHCP reservations for HCI node VMs management interfaces...' +$existingReservations = Get-DhcpServerv4Reservation -ScopeId 172.20.0.0 +For ($i = 1; $i -le $hciNodeCount; $i++) { + $hciNodeName = "hciNode$i" + $hciNodeIP = "172.20.0.$(9 + $i)" + If ($existingReservations.Description -notcontains $hciNodeName) { + log "Creating DHCP reservation for HCI node '$hciNodeName' with IP '$hciNodeIP'..." + + $mgmtNIC = Get-VMNetworkAdapter -VMName $hciNodeName -Name mgmt + + If ($mgmtNIC) { + $mgmtMac = $mgmtNIC.MacAddress -split '(.{2})' -ne '' -join '-' + + log "Creating DHCP reservation for HCI node '$hciNodeName' with IP '$hciNodeIP' for MAC address '$mgmtMac'..." + Add-DhcpServerv4Reservation -ScopeId 172.20.0.0 -Name $hciNodeName -IPAddress $hciNodeIP -ClientId $mgmtMac -Description $hciNodeName + } Else { + log "Failed to create DHCP reservation for HCI node '$hciNodeName'. Could not find NIC named 'mgmt'." + Write-Error "Failed to create DHCP reservation for HCI node '$hciNodeName'. Could not find NIC named 'mgmt'." + exit 1 + } + } Else { + log "DHCP reservation for HCI node '$hciNodeName' already exists." + } +} diff --git a/avm/res/azure-stack-hci/cluster/tests/e2e-template-assets/azureStackHCIHost/scripts/hciHostStage6.ps1 b/avm/res/azure-stack-hci/cluster/tests/e2e-template-assets/azureStackHCIHost/scripts/hciHostStage6.ps1 new file mode 100644 index 0000000000..454e5ad45b --- /dev/null +++ b/avm/res/azure-stack-hci/cluster/tests/e2e-template-assets/azureStackHCIHost/scripts/hciHostStage6.ps1 @@ -0,0 +1,354 @@ +[CmdletBinding()] +param ( + [Parameter()] + [String] + $resourceGroupName, + + [Parameter()] + [String] + $subscriptionId, + + [Parameter()] + [String] + $tenantId, + + [Parameter()] + [String] + $location, + + [Parameter()] + [String] + $accountName, + + [Parameter()] + [String] + $adminUsername, + + [Parameter()] + [String] + $adminPw, + + [Parameter()] + [String] + $arcGatewayId, + + [Parameter()] + [String] + $domainOUPath = 'OU=HCI,DC=HCI,DC=local', + + [Parameter()] + [string] + $deploymentUsername, + + [Parameter()] + [string] + $proxyServerEndpoint, #http://[Proxy_Server_Address]:[Proxy_Port], + + [parameter()] + [string] + $proxyBypassString, #"localhost;127.0.0.1;*.contoso.com;node1;node2;192.168.1.*;s-cluster", + + [Parameter()] + [string] + $userAssignedManagedIdentityClientId +) + +Function log { + Param ( + [string]$message, + [string]$logPath = 'C:\temp\hciHostDeploy-6.log' + ) + + If (!(Test-Path -Path 'C:\temp')) { + New-Item -Path 'C:\temp' -ItemType Directory + } + + Write-Host $message + Add-Content -Path $logPath -Value "$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss') [hciHostStage6] - $message" +} + +$ErrorActionPreference = 'Stop' + +# export or re-import local administrator credential +# we do this to support re-run of the template. If deployed, the HCI node password will be set to the password provided in the template, but future re-runs will generate a new password. +If (!(Test-Path -Path 'C:\temp\hciHostDeployAdminCred.xml')) { + log "Exporting local '$($adminUsername)' credential (for re-use if script is re-run)..." + $adminCred = [pscredential]::new($adminUsername, (ConvertTo-SecureString -AsPlainText -Force $adminPw)) + $adminCred | Export-Clixml -Path 'C:\temp\hciHostDeployAdminCred.xml' +} Else { + log "Re-importing local '$($adminUsername)' credential..." + $adminCredOld = Import-Clixml -Path 'C:\temp\hciHostDeployAdminCred.xml' + + $newCredFileName = 'hciHostDeployAdminCred_{0}.xml' -f (Get-Date -Format 'yyyyMMddHHmmss') + log "Renaming the old credential file to '$newCredFileName' prevent overwriting..." + Rename-Item -Path 'C:\temp\hciHostDeployAdminCred.xml' -NewName $newCredFileName + + log "Exporting local '$($adminUsername)' credential (for re-use if script is re-run)..." + $adminCred = [pscredential]::new($adminUsername, (ConvertTo-SecureString -AsPlainText -Force $adminPw)) + $adminCred | Export-Clixml -Path 'C:\temp\hciHostDeployAdminCred.xml' +} + +If (!(Get-PSRepository -Name PSGallery -ErrorAction SilentlyContinue)) { Register-PSRepository -Default } +If (!(Get-PackageProvider -Name Nuget -ListAvailable -ErrorAction SilentlyContinue)) { Install-PackageProvider -Name NuGet -Confirm:$false -Force } +Set-PSRepository -Name PSGallery -InstallationPolicy Trusted + +Install-Module Az + +# get an access token for the VM MSI, which has been granted rights and will be used for the HCI Arc Initialization +log "Logging in to Azure with user-assigned managed identity '$($userAssignedManagedIdentityClientId)'..." +Login-AzAccount -Identity -Subscription $subscriptionId -AccountId $userAssignedManagedIdentityClientId + +log 'Getting access token for Azure Stack HCI Arc initialization...' +$t = Get-AzAccessToken -ResourceUrl 'https://management.azure.com' | Select-Object -ExpandProperty Token + +# pre-create AD objects +log 'Pre-creating AD objects with deployment username '$deploymentUsername'...' +$deployUserCred = [pscredential]::new($deploymentUsername, (ConvertTo-SecureString -AsPlainText -Force $adminPw)) + +Install-Module AsHciADArtifactsPreCreationTool +New-HciAdObjectsPreCreation -AzureStackLCMUserCredential $deployUserCred -AsHciOUName $domainOUPath + +## set the LCM deployUser password to the adminPw value - this aligns the password with the KeyVault during re-runs +log "Setting deploy user '$deploymentUsername's password..." +Set-AdAccountPassword -Identity $deploymentUsername -NewPassword (ConvertTo-SecureString -AsPlainText -Force $adminPw) -Reset -Confirm:$false + +# initialize arc on hci nodes +log 'Initializing Azure Arc on HCI nodes...' + +# wait for VMs to reach 'Running' state +log 'Checking that VMs are running...' +$stopwatch = [System.Diagnostics.Stopwatch]::StartNew() +while ((Get-VM | Where-Object State -NE 'Running') -and $stopwatch.Elapsed.TotalMinutes -lt 15) { + log "Waiting for HCI node VMs to reach 'Running' state. Current state: $((Get-VM) | Select-Object Name,State)..." + Start-Sleep -Seconds 30 +} + +If ($stopwatch.Elapsed.TotalMinutes -ge 15) { + log "HCI node VMs did not reach 'Running' state within 15 minutes. Exiting..." + Write-Error "HCI node VMs did not reach 'Running' state within 15 minutes. Exiting..." + Exit 1 +} + +log "Creating PSSessions to HCI nodes [$((Get-VM).Name -join ',')]..." +try { + $VMs = Get-VM + If ($adminCredOld) { + log 'Using old local administrator credential from exported CliXML...' + $localAdminCred = $adminCredOld + } Else { + log 'Using new local administrator credential from parameter input...' + $localAdminCred = $adminCred + } + $sessions = New-PSSession -VMName $VMs.Name -Credential $localAdminCred -ErrorAction Stop + + log "Created '$(($sessions | Where-Object State -EQ 'Opened').Count)' PSSessions to HCI nodes [$($VMs.Name -join ',')]." +} catch { + If ($_ -like '*The credential is invalid*') { + log 'Failed to create PSSessions with "The credential is invalid" error. This is likely due to the password not matching the local administrator password on the HCI nodes. Retrying with the older passwords...' + + $credFiles = Get-ChildItem -Path 'C:\temp\*' -Include 'hciHostDeployAdminCred*.xml' -Exclude 'hciHostDeployAdminCred.xml' + + If ($credFiles.count -eq 0) { + log 'No old credential files found. Exiting...' + Write-Error 'No old credential files found. Exiting...' + Exit 1 + } + + :retryCreds ForEach ($credFile in $credFiles) { + log "Attempting login with credential file '$($credFile.name)'..." + $localAdminCred = Import-Clixml -Path $credFile.FullName + + try { + $sessions = New-PSSession -VMName $VMs.Name -Credential $localAdminCred -ErrorAction Stop + + If (($sessions | Where-Object State -EQ 'Opened').count -eq $VMs.Count) { + log "Created '$(($sessions | Where-Object State -EQ 'Opened').Count)' PSSessions to HCI nodes [$($VMs.Name -join ',')]." + break retryCreds + } + } catch { + log "Failed to create PSSessions with credential file '$($credFile.name)'. Error: $_" + continue + } finally { + If (($sessions | Where-Object State -EQ 'Opened').count -eq $VMs.Count) { + log "Created '$(($sessions | Where-Object State -EQ 'Opened').Count)' PSSessions to HCI nodes [$($VMs.Name -join ',')]." + $adminCredOld = $localAdminCred + } + } + + } + } else { + log "Failed to create PSSessions to HCI nodes [$($VMs.Name -join ',')]. $sessions $_ Exiting..." + Write-Error "Failed to create PSSessions to HCI nodes [$($VMs.Name -join ',')]. $sessions $_ Exiting..." + Exit 1 + } +} + +# update local admin password to match the adminPw value +If ($adminCredOld) { + log 'Updating local administrator password to match the adminPw value...' + Invoke-Command -VMName (Get-VM).Name -Credential $adminCredOld { + $ErrorActionPreference = 'Stop' + + $adminPw = $args[0] + $adminUsername = $args[1] + + Write-Host "$($env:computerName):Setting local administrator password to match the adminPw value..." + $adminCred = [pscredential]::new($adminUsername, (ConvertTo-SecureString -AsPlainText -Force $adminPw)) + Set-LocalUser -Name $adminUsername -Password $adminCred.Password -Confirm:$false + } -ArgumentList $adminPw, $adminUsername +} Else { + log "Password for '$($adminUsername)' should already match the adminPw value..." +} + +# disable IPv6 on all HCI nodes +log 'Disabling IPv6 on HCI nodes...' +Invoke-Command -VMName (Get-VM).Name -Credential $adminCred { + reg add hklm\system\currentcontrolset\services\tcpip6\parameters /v DisabledComponents /t REG_DWORD /d 0xFF /f +} + +# set proxy settings if provided +if (![string]::IsNullOrEmpty($proxyServerEndpoint) -and ![string]::IsNullOrEmpty($proxyBypassString)) { + log 'Both -proxyServerEndpoiint and -proxyBypassString passed, setting proxy settings...' + log "Proxy Server Endpoint: $proxyServerEndpoint" + log "Proxy Bypass String: $proxyBypassString" + + If ($proxyBypassString -eq 'GENERATE_PROXY_BYPASS_DYNAMICALLY') { + log 'Generating proxy bypass string dynamically...' + $proxyBypassString = '127.0.0.1;localhost;172.20.0.*;*.hci.local;hcicluster;172.20.0.2;172.20.0.3;172.20.0.4;172.20.0.5;*.svc' + For ($i = 1; $i -le (Get-VM).count; $i++) { + $proxyBypassString += ";hcinode$i" + $proxyBypassString += ';172.20.0.{0}' -f (9 + $i) + } + } + + log 'Configuring proxy settings on HCI nodes...' + $proxyConfigLogs = Invoke-Command -VMName (Get-VM).Name -Credential $adminCred { + $ErrorActionPreference = 'Stop' + + $proxyServerEndpoint = $args[0] + $proxyBypassString = $args[1] + + ## install winInetProxy module + If (!(Get-PackageProvider -Name NuGet -ListAvailable -ErrorAction SilentlyContinue)) { Install-PackageProvider -Name NuGet -MinimumVersion 2.8.5.201 -Force } + If (!(Get-PSRepository -Name PSGallery -ErrorAction SilentlyContinue)) { Register-PSRepository -Default } + Set-PSRepository -Name PSGallery -InstallationPolicy Trusted + If (!(Get-InstalledModule -Name WinInetProxy -ErrorAction SilentlyContinue)) { Install-Module WinInetProxy -Force } + Set-PSRepository -Name PSGallery -InstallationPolicy Untrusted + + ## set WinInet proxy settings + Set-WinInetProxy -ProxyServer $proxyServerEndpoint -ProxyBypass $proxyBypassString -ProxySettingsPerUser 0 + + ## set winhttp proxy settings + Set-WinhttpProxy -ProxyServer $proxyServerEndpoint -BypassList $proxyBypassString + + ## set proxy environment variables + $proxyBypassStringEnv = $proxyBypassString.replace(';', ',').replace('*', '').replace('172.20.0.*', '172.20.0.0/24') + $proxyBypassStringEnv += ',.svc' + + [Environment]::SetEnvironmentVariable('HTTPS_PROXY', $proxyServerEndpoint, 'Machine') + [Environment]::SetEnvironmentVariable('HTTP_PROXY', $proxyServerEndpoint, 'Machine') + [Environment]::SetEnvironmentVariable('NO_PROXY', $proxyBypassStringEnv, 'Machine') + + Write-Output "[$($env:COMPUTERNAME)] WinInetProxy Settings: $(Get-WinhttpProxy -Advanced)" + Write-Output "[$($env:COMPUTERNAME)] WinhttpProxy Settings: $(Get-WinhttpProxy -Default)" + + Write-Output ("[{0}] Environment Variables {1}: '{2}'" -f $env:COMPUTERNAME, 'HTTPS_PROXY', [Environment]::GetEnvironmentVariable('HTTPS_PROXY', 'Machine')) + Write-Output ("[{0}] Environment Variables {1}: '{2}'" -f $env:COMPUTERNAME, 'HTTP_PROXY', [Environment]::GetEnvironmentVariable('HTTP_PROXY', 'Machine')) + Write-Output ("[{0}] Environment Variables {1}: '{2}'" -f $env:COMPUTERNAME, 'NO_PROXY', [Environment]::GetEnvironmentVariable('NO_PROXY', 'Machine')) + + } -ArgumentList $proxyServerEndpoint, $proxyBypassString + + $proxyConfigLogs | ForEach-Object { + log $_ + } +} Else { + log "Skipping proxy settings because both -proxyServerEndpoint and -proxyBypassString were not passed... (proxyServerEndpoint: '$proxyServerEndpoint', proxyBypassString:'$proxyBypassString')" +} + +## test node internet connection - required for Azure Arc initialization +$firstVM = Get-VM | Select-Object -First 1 +log "Testing node internet connection on VM '$($firstVM.Name)'..." +$testNodeInternetConnection = Invoke-Command -VMName $firstVM.Name -Credential $adminCred { + [bool](Invoke-RestMethod ipinfo.io -UseBasicParsing) +} + +If (!$testNodeInternetConnection) { + log "Node '$($firstVM.name)' does not have internet connection. Check RRAS NAT configuration. Exiting..." + Write-Error "Node '$($firstVM.name)' does not have internet connection. Check RRAS NAT configuration. Exiting..." + Exit 1 +} Else { + log "Node '$($firstVM.name)' has internet connection. Curl IPInfo: '$($testNodeInternetConnection)'" +} + +## create jobs for each node to initialize Azure Arc +log "Creating Azure Arc initialization jobs for HCI nodes [$((Get-VM).Name -join ',')]. ArcGatewayId: '$arcGatewayId', ProxyServerEndpoint: '$proxyServerEndpoint'..." +$arcInitializationJobs = Invoke-Command -VMName (Get-VM).Name -Credential $adminCred { + $ErrorActionPreference = 'Stop' + + $t = $args[0] + $subscriptionId = $args[1] + $resourceGroupName = $args[2] + $tenantId = $args[3] + $location = $args[4] + $accountName = $args[5] + $arcGatewayId = $args[6] + $proxyServerEndpoint = $args[7] + $proxyBypassString = $args[8] + + $optionalParameters = @{} + + If ($arcGatewayId) { + $optionalParameters += @{ + 'arcGatewayId' = $arcGatewayId + } + } + If ($proxyServerEndpoint) { + $optionalParameters += @{ + 'proxy' = $proxyServerEndpoint + 'proxyBypass' = $proxyBypassString + } + } + + Install-PackageProvider -Name NuGet -MinimumVersion 2.8.5.201 -Force | Out-Null + If (!(Get-PSRepository -Name PSGallery -ErrorAction SilentlyContinue)) { Register-PSRepository -Default } + Set-PSRepository -Name PSGallery -InstallationPolicy Trusted + Install-Module Az.Resources + Install-Module -Name AzsHCI.ARCinstaller # -RequiredVersion '0.2.2690.99' # hardcode for 2408 testing + Set-PSRepository -Name PSGallery -InstallationPolicy Untrusted + + #wait for bootstrap service to be reachable + $stopwatch = [System.Diagnostics.Stopwatch]::StartNew() + While (!(Test-NetConnection -ComputerName '127.0.0.1' -Port 9098 -InformationLevel Quiet) -and $stopwatch.Elapsed.TotalMinutes -lt 30) { + Write-Host 'Waiting for bootstrap service at 127.0.0.1:9098 to be reachable...' + Start-Sleep -Seconds 30 + } + If ($stopwatch.Elapsed.TotalMinutes -ge 30) { + Write-Error 'Bootstrap service at 127.0.0.1:9098 did not become reachable within 30 minutes. Exiting...' -ErrorAction Stop + } + + try { + Invoke-AzStackHciArcInitialization -SubscriptionID $subscriptionId -ResourceGroup $resourceGroupName -TenantID $tenantId -Cloud AzureCloud -AccountID $accountName -ArmAccessToken $t -Region $location -ErrorAction Stop @optionalParameters + } catch { + Write-Error $_ -ErrorAction Stop + } +} -AsJob -ArgumentList $t, $subscriptionId, $resourceGroupName, $tenantId, $location, $accountName, $arcGatewayId, $proxyServerEndpoint, $proxyBypassString + +log 'Waiting up to 30 minutes for Azure Arc initialization to complete on nodes...' + +$arcInitializationJobs | Wait-Job -Timeout 1800 + +# check for failed arc initialization jobs +log 'Checking status of Azure Arc initialization jobs...' +$arcInitializationJobs | ForEach-Object { + $job = $_ + log "[$($job.ComputerName)] Job output (Receive-Job): '$($job | Receive-Job -Keep -ErrorAction Continue | Out-String)'" + Get-Job -Id $job.Id -IncludeChildJob | Receive-Job -ErrorAction SilentlyContinue | ForEach-Object { + If ($_.Exception -or $_.state -eq 'Failed') { + log "Azure Arc initialization failed on node '$($job.Location)' with error: $($_.Exception.Message)" + Exit 1 + } Else { + log "[$($job.ComputerName)] Job output: '$($_ | ConvertTo-Json -Compress)'" + + } + } +} diff --git a/avm/res/azure-stack-hci/cluster/tests/e2e-template-assets/azureStackHCIHost/scripts/hciHostStage7.ps1 b/avm/res/azure-stack-hci/cluster/tests/e2e-template-assets/azureStackHCIHost/scripts/hciHostStage7.ps1 new file mode 100644 index 0000000000..77c0193f7b --- /dev/null +++ b/avm/res/azure-stack-hci/cluster/tests/e2e-template-assets/azureStackHCIHost/scripts/hciHostStage7.ps1 @@ -0,0 +1,207 @@ +param( + [Parameter()] + [String] + $resourceGroupName, + + [Parameter()] + [String] + $subscriptionId, + + [Parameter()] + [int] + $hciNodeCount, + + [Parameter()] + [String] + $userAssignedManagedIdentityClientId + +) +Function log { + Param ( + [string]$message, + [string]$logPath = 'C:\temp\hciHostDeploy-7.log' + ) + + If (!(Test-Path -Path C:\temp)) { + New-Item -Path C:\temp -ItemType Directory + } + + Write-Host $message + Add-Content -Path $logPath -Value "$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss') [hciHostStage7] - $message" +} + +$ErrorActionPreference = 'Stop' + +$hciNodeNames = @() +for ($i = 1; $i -le $hciNodeCount; $i++) { + $hciNodeNames += "hcinode$i" +} + +Set-PSRepository -Name PSGallery -InstallationPolicy Trusted +Install-Module -Name Az.ConnectedMachine -Force -AllowClobber -Scope CurrentUser -Repository PSGallery -ErrorAction SilentlyContinue +Set-PSRepository -Name PSGallery -InstallationPolicy Untrusted + +log "Logging in to Azure with user-assigned managed identity '$($userAssignedManagedIdentityClientId)'..." +Login-AzAccount -Identity -Subscription $subscriptionId -AccountId $userAssignedManagedIdentityClientId + +log "Waiting for HCI Arc Machines to exist in the resource group '$($resourceGroupName)'..." + +$timer = [System.Diagnostics.Stopwatch]::StartNew() +While (($arcMachines = Get-AzConnectedMachine -ResourceGroupName $resourceGroupName | Where-Object { $_.name -in ($hciNodeNames) }).Count -lt $hciNodeNames.Count -and $timer.Elapsed.TotalMinutes -lt 60) { + log "Found '$($arcMachines.Count)' HCI Arc Machines, waiting for '$($hciNodeNames.Count)' machines for up to 1 hour..." + Start-Sleep -Seconds 30 +} +If ($timer.Elapsed.TotalMinutes -gt 60) { + log 'HCI Arc Machines did not exist within the 1 hour timeout period' + Write-Error 'HCI Arc Machines did not exist within the 1 hour timeout period' -ErrorAction Stop + Exit 1 +} Else { + log "All HCI Arc Machines exist in the resource group '$($resourceGroupName)'" +} + +log 'Waiting up to two hours for HCI Arc Machine extensions to be installed...' +$timer = [System.Diagnostics.Stopwatch]::StartNew() +$allExtensionsReady = $false +while (!$allExtensionsReady -and $timer.Elapsed.TotalMinutes -lt 120) { + $allExtensionsReadyCheck = $true + foreach ($arcMachine in $arcMachines) { + $extensions = Get-AzConnectedMachineExtension -ResourceGroupName $resourceGroupName -MachineName $arcMachine.Name + if ($extensions.MachineExtensionType -notcontains 'TelemetryAndDiagnostics' -or $extensions.MachineExtensionType -notcontains 'DeviceManagementExtension' -or $extensions.MachineExtensionType -notcontains 'LcmController' -or $extensions.MachineExtensionType -notcontains 'EdgeRemoteSupport') { + log "Waiting for extensions to be installed on HCI Arc Machine '$($arcMachine.Name)'..." + + # install extensions if not already installed + log "Installing any missing extensions on HCI Arc Machine '$($arcMachine.Name)'..." + $extensionParams = @{ + ResourceGroupName = $resourceGroupName + MachineName = $arcMachine.Name + Location = $arcMachine.Location + ErrorAction = 'Continue' + } + + # Invoke-AzStackHciArcInitialization seemingly misses installing some extensions some of the time - so we'll install them here if missing + If ($extensions.MachineExtensionType -notcontains 'TelemetryAndDiagnostics') { + log "Installing TelemetryAndDiagnostics extension on HCI Arc Machine '$($arcMachine.Name)'..." + New-AzConnectedMachineExtension -Name 'AzureEdgeTelemetryAndDiagnostics' -Publisher 'Microsoft.AzureStack.Observability' -ExtensionType 'TelemetryAndDiagnostics' -NoWait @extensionParams + } + If ($extensions.MachineExtensionType -notcontains 'DeviceManagementExtension') { + log "Installing DeviceManagementExtension extension on HCI Arc Machine '$($arcMachine.Name)'..." + New-AzConnectedMachineExtension -Name 'AzureEdgeDeviceManagement' -Publisher 'Microsoft.Edge' -ExtensionType 'DeviceManagementExtension' -NoWait @extensionParams + } + If ($extensions.MachineExtensionType -notcontains 'LcmController') { + log "Installing LcmController extension on HCI Arc Machine '$($arcMachine.Name)'..." + New-AzConnectedMachineExtension -Name 'AzureEdgeLifecycleManager' -Publisher 'Microsoft.AzureStack.Orchestration' -ExtensionType 'LcmController' -NoWait @extensionParams + } + If ($extensions.MachineExtensionType -notcontains 'EdgeRemoteSupport') { + log "Installing EdgeRemoteSupport extension on HCI Arc Machine '$($arcMachine.Name)'..." + New-AzConnectedMachineExtension -Name 'AzureEdgeRemoteSupport' -Publisher 'Microsoft.AzureStack.Observability' -ExtensionType 'EdgeRemoteSupport' -NoWait @extensionParams + } + + $allExtensionsReadyCheck = $false + continue + } elseIf (($extensionState = $extensions | Where-Object MachineExtensionType -EQ 'TelemetryAndDiagnostics').ProvisioningState -ne 'Succeeded') { + log "Waiting for TelemetryAndDiagnostics extension to be installed on HCI Arc Machine '$($arcMachine.Name)'. Current state: '$($extensionState.ProvisioningState)'..." + $allExtensionsReadyCheck = $false + } elseIf (($extensionState = $extensions | Where-Object MachineExtensionType -EQ 'DeviceManagementExtension').ProvisioningState -ne 'Succeeded') { + log "Waiting for DeviceManagementExtension extension to be installed on HCI Arc Machine '$($arcMachine.Name)'. Current state: '$($extensionState.ProvisioningState)'..." + $allExtensionsReadyCheck = $false + } elseIf (($extensionState = $extensions | Where-Object MachineExtensionType -EQ 'LcmController').ProvisioningState -ne 'Succeeded') { + log "Waiting for LcmController extension to be installed on HCI Arc Machine '$($arcMachine.Name)'. Current state: '$($extensionState.ProvisioningState)'..." + $allExtensionsReadyCheck = $false + } elseIf (($extensionState = $extensions | Where-Object MachineExtensionType -EQ 'EdgeRemoteSupport').ProvisioningState -ne 'Succeeded') { + log "Waiting for EdgeRemoteSupport extension to be installed on HCI Arc Machine '$($arcMachine.Name)'. Current state: '$($extensionState.ProvisioningState)'..." + $allExtensionsReadyCheck = $false + } else { + log "All extensions are installed and ready on HCI Arc Machine '$($arcMachine.Name)'" + } + } + $allExtensionsReady = $allExtensionsReadyCheck + If (!$allExtensionsReady) { + log 'waiting 30 seconds to check extensions again...' + Start-Sleep -Seconds 30 + } +} + +If (!$allExtensionsReady) { + log 'Extensions did not install within the two hour timeout period' + Exit 1 +} Else { + log 'All extensions are installed and ready on all HCI Arc Machines' +} + +# import local administrator credential (exported in stage 6) +log "Re-importing local '$($adminUsername)' credential..." +$adminCred = Import-Clixml -Path 'C:\temp\hciHostDeployAdminCred.xml' + +# name net adapters - seems to be required on 2405 +log 'Renaming network adapters on HCI nodes...' +$vmNicLocalNamingOut = Invoke-Command -VMName (Get-VM).Name -Credential $adminCred { + $ErrorActionPreference = 'Stop' + + Get-NetAdapter | ForEach-Object { + $adapter = $_ + + try { + Write-Output "Getting Hyper-V network adapter name for '$($adapter.Name)' on VM '$($env:COMPUTERNAME)'..." + $newAdapterName = Get-NetAdapterAdvancedProperty -RegistryKeyword HyperVNetworkAdapterName -Name $adapter.Name | Select-Object -ExpandProperty DisplayValue + } catch { + Write-Output "Failed to get Hyper-V network adapter name for '$($adapter.Name)' on VM '$($env:COMPUTERNAME)'. Ensure DeviceNaming is turned on for the VM Network Adapter! $_ Exiting..." + Write-Error "Failed to get Hyper-V network adapter name for '$($adapter.Name)' on VM '$($env:COMPUTERNAME)'. Ensure DeviceNaming is turned on for the VM Network Adapter! $_ Exiting..." -ErrorAction Stop + Exit 1 + } + + If ($adapter.InterfaceAlias -ne $newAdapterName) { + Write-Output "Renaming network adapter '$($adapter.InterfaceAlias)' to '$newAdapterName' on VM '$($env:COMPUTERNAME)'..." + Rename-NetAdapter -Name $adapter.Name -NewName $newAdapterName + } Else { + Write-Output "Network adapter '$($adapter.InterfaceAlias)' is already named correctly on VM '$($env:COMPUTERNAME)'..." + } + } +} +log "VM NIC local naming output: $vmNicLocalNamingOut" + +# change dynamically assigned mgmt IP addresses to static IPs as required by validation +log 'Changing dynamically assigned mgmt IP addresses to static IPs on HCI nodes...' +$ipChangeOutput = Invoke-Command -VMName (Get-VM).Name -Credential $adminCred { + $ErrorActionPreference = 'Stop' + + $dhcpIpConfig = Get-NetIPConfiguration -InterfaceAlias 'mgmt' + $prefixLength = Get-NetIPAddress -InterfaceAlias 'mgmt' -AddressFamily IPv4 | Select-Object -ExpandProperty PrefixLength + $dnsClientConfig = Get-DnsClientServerAddress -InterfaceAlias 'mgmt' -AddressFamily IPv4 | Select-Object -ExpandProperty ServerAddresses + + try { + If (!(Get-NetIPInterface -InterfaceAlias 'mgmt' -Dhcp Enabled -ErrorAction SilentlyContinue)) { + Write-Output "[$env:computerName]DHCP is already disabled on network interface 'mgmt'..." + } Else { + Write-Output "[$env:computerName]Disabling DHCP on network interface 'mgmt'..." + Set-NetIPInterface -InterfaceAlias 'mgmt' -Dhcp Disabled + } + } catch { + Write-Output "[$env:computerName]Failed to disable DHCP on network interface 'mgmt'. Error message: $_. Exiting..." + Write-Error "[$env:computerName]Failed to disable DHCP on network interface 'mgmt'. Error message: $_. Exiting..." -ErrorAction Stop + Exit 1 + } + + try { + If (!(Get-NetIPAddress -IPAddress $dhcpIpConfig.IPv4Address.ipAddress -InterfaceAlias 'mgmt' -ErrorAction SilentlyContinue)) { + Write-Output "[$env:computerName]Setting static IP address on network interface 'mgmt'..." + New-NetIPAddress -InterfaceAlias 'mgmt' -IPAddress $dhcpIpConfig.IPv4Address.ipAddress -DefaultGateway $dhcpIpConfig.Ipv4DefaultGateway.NextHop -AddressFamily IPv4 -PrefixLength $prefixLength + } Else { + Write-Output "[$env:computerName]Static IP address already set on network interface 'mgmt'..." + } + } catch { + Write-Output "[$env:computerName]Failed to set static IP address on network interface 'mgmt'. Error message: $_. Exiting..." + Write-Error "[$env:computerName]Failed to set static IP address on network interface 'mgmt'. Error message: $_. Exiting..." -ErrorAction Stop + Exit 1 + } + + try { + Write-Output "[$env:computerName]Setting DNS server addresses on network interface 'mgmt' to '$dnsClientConfig'..." + Set-DnsClientServerAddress -InterfaceAlias 'mgmt' -ResetServerAddresses + Set-DnsClientServerAddress -InterfaceAlias 'mgmt' -ServerAddresses $dnsClientConfig + } catch { + Write-Output "[$env:computerName]Failed to set DNS server addresses on network interface 'mgmt'. Error message: $_. Exiting..." + Write-Error "[$env:computerName]Failed to set DNS server addresses on network interface 'mgmt'. Error message: $_. Exiting..." -ErrorAction Stop + Exit 1 + } +} +log "IP change output: $ipChangeOutput" diff --git a/avm/res/azure-stack-hci/cluster/tests/e2e-template-assets/azureStackHCIHost/scripts/proxyConfig.sh b/avm/res/azure-stack-hci/cluster/tests/e2e-template-assets/azureStackHCIHost/scripts/proxyConfig.sh new file mode 100644 index 0000000000..1a9948106e --- /dev/null +++ b/avm/res/azure-stack-hci/cluster/tests/e2e-template-assets/azureStackHCIHost/scripts/proxyConfig.sh @@ -0,0 +1,153 @@ +#!/bin/sh + +sudo apt update +sudo apt install squid -y + +sudo cp /etc/squid/squid.conf /etc/squid/squid.conf.bak +sudo cat <<EOF > /etc/squid/squid.conf + access_log /var/log/squid/access.log + + acl localnet src 0.0.0.1-0.255.255.255 # RFC 1122 "this" network (LAN) + acl localnet src 10.0.0.0/8 # RFC 1918 local private network (LAN) + acl localnet src 100.64.0.0/10 # RFC 6598 shared address space (CGN) + acl localnet src 169.254.0.0/16 # RFC 3927 link-local (directly plugged) machines + acl localnet src 172.16.0.0/12 # RFC 1918 local private network (LAN) + acl localnet src 192.168.0.0/16 # RFC 1918 local private network (LAN) + acl localnet src fc00::/7 # RFC 4193 local private network range + acl localnet src fe80::/10 # RFC 4291 link-local (directly plugged) machines + + acl SSL_ports port 443 + acl SSL_ports port 6443 + acl SSL_ports port 8084 + + acl Safe_ports port 80 # http + acl Safe_ports port 21 # ftp + acl Safe_ports port 443 # https + acl Safe_ports port 70 # gopher + acl Safe_ports port 210 # wais + acl Safe_ports port 1025-65535 # unregistered ports + acl Safe_ports port 280 # http-mgmt + acl Safe_ports port 488 # gss-http + acl Safe_ports port 591 # filemaker + acl Safe_ports port 777 # multiling http + acl Safe_ports port 6443 + + acl HCI_Dest_URLs dstdomain .mcr.microsoft.com + acl HCI_Dest_URLs dstdomain azurearcfork8s.azurecr.io + acl HCI_Dest_URLs dstdomain linuxgeneva-microsoft.azurecr.io + acl HCI_Dest_URLs dstdomain pipelineagent.azurecr.io + acl HCI_Dest_URLs dstdomain azurearcfork8sdev.azurecr.io + acl HCI_Dest_URLs dstdomain hybridaks.azurecr.io + acl HCI_Dest_URLs dstdomain aszk8snetworking.azurecr.io + acl HCI_Dest_URLs dstdomain hybridaksstorage.z13.web.core.windows.net + # acl HCI_Dest_URLs dstdomain .dl.delivery.mp.microsoft.com + acl HCI_Dest_URLs dstdomain .do.dsp.mp.microsoft.com + acl HCI_Dest_URLs dstdomain .prod.do.dsp.mp.microsoft.com + acl HCI_Dest_URLs dstdomain .dp.kubernetesconfiguration.azure.com + acl HCI_Dest_URLs dstdomain sts.windows.net + acl HCI_Dest_URLs dstdomain ecpacr.azurecr.io + acl HCI_Dest_URLs dstdomain pypi.org + acl HCI_Dest_URLs dstdomain files.pythonhosted.org + acl HCI_Dest_URLs dstdomain raw.githubusercontent.com + acl HCI_Dest_URLs dstdomain msk8s.api.cdp.microsoft.com + # acl HCI_Dest_URLs dstdomain msk8s.sb.tlu.dl.delivery.mp.microsoft.com + acl HCI_Dest_URLs dstdomain k8connecthelm.azureedge.net + acl HCI_Dest_URLs dstdomain kvamanagementoperator.azurecr.io + acl HCI_Dest_URLs dstdomain packages.microsoft.com + acl HCI_Dest_URLs dstdomain k8sconnectcsp.azureedge.net + acl HCI_Dest_URLs dstdomain .prod.hot.ingest.monitor.core.windows.net + acl HCI_Dest_URLs dstdomain .dp.prod.appliances.azure.com + acl HCI_Dest_URLs dstdomain download.microsoft.com + acl HCI_Dest_URLs dstdomain pas.windows.net + acl HCI_Dest_URLs dstdomain guestnotificationservice.azure.com + acl HCI_Dest_URLs dstdomain .his.arc.azure.com + acl HCI_Dest_URLs dstdomain .guestconfiguration.azure.com + acl HCI_Dest_URLs dstdomain agentserviceapi.guestconfiguration.azure.com + acl HCI_Dest_URLs dstdomain .servicebus.windows.net + acl HCI_Dest_URLs dstdomain .waconazure.com + acl HCI_Dest_URLs dstdomain .gw.arc.azure.net + acl HCI_Dest_URLs dstdomain login.microsoftonline.com + acl HCI_Dest_URLs dstdomain graph.windows.net + acl HCI_Dest_URLs dstdomain graph.microsoft.com + acl HCI_Dest_URLs dstdomain login.windows.net + acl HCI_Dest_URLs dstdomain eastus.login.microsoft.com + acl HCI_Dest_URLs dstdomain southeastasia.login.microsoft.com + acl HCI_Dest_URLs dstdomain crl3.digicert.com + acl HCI_Dest_URLs dstdomain crl4.digicert.com + acl HCI_Dest_URLs dstdomain www.powershellgallery.com + acl HCI_Dest_URLs dstdomain psg-prod-eastus.azureedge.net + acl HCI_Dest_URLs dstdomain psg-prod-southeastasia.azureedge.net + acl HCI_Dest_URLs dstdomain onegetcdn.azureedge.net + acl HCI_Dest_URLs dstdomain portal.azure.com + acl HCI_Dest_URLs dstdomain .blob.core.windows.net + acl HCI_Dest_URLs dstdomain hciarcvmscontainerregistry.azurecr.io + acl HCI_Dest_URLs dstdomain azurestackreleases.download.prss.microsoft.com + acl HCI_Dest_URLs dstdomain settings-win.data.microsoft.com + acl HCI_Dest_URLs dstdomain dp.stackhci.azure.com + acl HCI_Dest_URLs dstdomain licensing.platform.edge.azure.com + acl HCI_Dest_URLs dstdomain billing.platform.edge.azure.com + acl HCI_Dest_URLs dstdomain azurestackhci.azurefd.net + acl HCI_Dest_URLs dstdomain .prod.microsoftmetrics.com + acl HCI_Dest_URLs dstdomain dc.services.visualstudio.com +# acl HCI_Dest_URLs dstdomain qos.prod.warm.ingest.monitor.core.windows.net + acl HCI_Dest_URLs dstdomain .prod.warm.ingest.monitor.core.windows.net + acl HCI_Dest_URLs dstdomain gcs.prod.monitoring.core.windows.net + acl HCI_Dest_URLs dstdomain adhs.events.data.microsoft.com + acl HCI_Dest_URLs dstdomain v20.events.data.microsoft.com + acl HCI_Dest_URLs dstdomain aka.ms + acl HCI_Dest_URLs dstdomain redirectiontool.trafficmanager.net +# acl HCI_Dest_URLs dstdomain fe3.delivery.mp.microsoft.com +# acl HCI_Dest_URLs dstdomain tlu.dl.delivery.mp.microsoft.com + acl HCI_Dest_URLs dstdomain www.microsoft.com + acl HCI_Dest_URLs dstdomain windowsupdate.microsoft.com +# acl HCI_Dest_URLs dstdomain .download.windowsupdate.com + acl HCI_Dest_URLs dstdomain wustat.windows.com + acl HCI_Dest_URLs dstdomain ntservicepack.microsoft.com + acl HCI_Dest_URLs dstdomain go.microsoft.com + acl HCI_Dest_URLs dstdomain .delivery.mp.microsoft.com + # acl HCI_Dest_URLs dstdomain .windowsupdate.microsoft.com + acl HCI_Dest_URLs dstdomain .windowsupdate.com + acl HCI_Dest_URLs dstdomain .update.microsoft.com + acl HCI_Dest_URLs dstdomain .endpoint.security.microsoft.com + acl HCI_Dest_URLs dstdomain www.office.com + acl HCI_Dest_URLs dstdomain login.microsoft.com + acl HCI_Dest_URLs dstdomain pythonhosted.org + acl HCI_Dest_URLs dstdomain .blob.storage.azure.net + acl HCI_Dest_URLs dstdomain oneocsp.microsoft.com + acl HCI_Dest_URLs dstdomain ts-crl.ws.symantec.com + acl HCI_Dest_URLs dstdomain ts-ocsp.ws.symantec.com + acl HCI_Dest_URLs dstdomain s.symcb.com + acl HCI_Dest_URLs dstdomain ocsp.digicert.com + acl HCI_Dest_URLs dstdomain ocsp2.globalsign.com + acl HCI_Dest_URLs dstdomain hciarcvmsstorage.z13.web.core.windows.net + acl HCI_Dest_URLs dstdomain management.azure.com + acl HCI_Dest_URLs dstdomain developer.microsoft.com + acl HCI_Dest_URLs dstdomain .vault.azure.net + acl HCI_Dest_URLs dstdomain .prod.hot.ingestion.msftcloudes.com # optional metrics and telemetry + acl HCI_Dest_URLs dstdomain edgesupprd.trafficmanager.net # optional support + acl HCI_Dest_URLs dstdomain .obo.arc.azure.com # optional arc - port 8084 + acl HCI_Dest_URLs dstdomain azurewatsonanalysis-prod.core.windows.net # optional observability + + acl HCI_Dest_URLs_regex dstdom_regex azgn[a-zA-Z0-9]+?\.servicebus\.windows\.net + + # used to test internet connectivity during HCI node deployment (tests NAT configuration) + acl testURL dstdomain ipinfo.io + + http_access deny !Safe_ports + + http_access deny CONNECT !SSL_ports + + http_access allow localhost manager + http_access deny manager + + http_port 3128 + + http_access allow testURL + http_access allow HCI_Dest_URLs_regex + http_access allow HCI_Dest_URLs +EOF + +sudo systemctl restart squid +sudo systemctl enable squid + +sudo ufw allow 3128/tcp diff --git a/avm/res/azure-stack-hci/cluster/tests/e2e-template-assets/azureStackHCIHost/scripts/proxyConfigArcGW.sh b/avm/res/azure-stack-hci/cluster/tests/e2e-template-assets/azureStackHCIHost/scripts/proxyConfigArcGW.sh new file mode 100644 index 0000000000..1e69299179 --- /dev/null +++ b/avm/res/azure-stack-hci/cluster/tests/e2e-template-assets/azureStackHCIHost/scripts/proxyConfigArcGW.sh @@ -0,0 +1,171 @@ +#!/bin/sh + +sudo apt update +sudo apt install squid -y + +sudo cp /etc/squid/squid.conf /etc/squid/squid.conf.bak +sudo cat <<EOF > /etc/squid/squid.conf + access_log /var/log/squid/access.log + + acl localnet src 0.0.0.1-0.255.255.255 # RFC 1122 "this" network (LAN) + acl localnet src 10.0.0.0/8 # RFC 1918 local private network (LAN) + acl localnet src 100.64.0.0/10 # RFC 6598 shared address space (CGN) + acl localnet src 169.254.0.0/16 # RFC 3927 link-local (directly plugged) machines + acl localnet src 172.16.0.0/12 # RFC 1918 local private network (LAN) + acl localnet src 192.168.0.0/16 # RFC 1918 local private network (LAN) + acl localnet src fc00::/7 # RFC 4193 local private network range + acl localnet src fe80::/10 # RFC 4291 link-local (directly plugged) machines + + acl SSL_ports port 443 + acl SSL_ports port 6443 + acl SSL_ports port 8084 + + acl Safe_ports port 80 # http + acl Safe_ports port 21 # ftp + acl Safe_ports port 443 # https + acl Safe_ports port 70 # gopher + acl Safe_ports port 210 # wais + acl Safe_ports port 1025-65535 # unregistered ports + acl Safe_ports port 280 # http-mgmt + acl Safe_ports port 488 # gss-http + acl Safe_ports port 591 # filemaker + acl Safe_ports port 777 # multiling http + acl Safe_ports port 6443 + + acl HCI_ResourceBridgeIPs src 172.20.0.3-172.20.0.5 + + acl HCI_Dest_URLs dstdomain .mcr.microsoft.com + acl HCI_Dest_URLs dstdomain azurearcfork8s.azurecr.io + acl HCI_Dest_URLs dstdomain linuxgeneva-microsoft.azurecr.io + acl HCI_Dest_URLs dstdomain pipelineagent.azurecr.io + acl HCI_Dest_URLs dstdomain azurearcfork8sdev.azurecr.io + acl HCI_Dest_URLs dstdomain hybridaks.azurecr.io + acl HCI_Dest_URLs dstdomain aszk8snetworking.azurecr.io + acl HCI_Dest_URLs dstdomain hybridaksstorage.z13.web.core.windows.net + # acl HCI_Dest_URLs dstdomain .dl.delivery.mp.microsoft.com + acl HCI_Dest_URLs dstdomain .do.dsp.mp.microsoft.com + acl HCI_Dest_URLs dstdomain .prod.do.dsp.mp.microsoft.com + acl HCI_Dest_URLs dstdomain .dp.kubernetesconfiguration.azure.com + acl HCI_Dest_URLs dstdomain sts.windows.net + acl HCI_Dest_URLs dstdomain ecpacr.azurecr.io + acl HCI_Dest_URLs dstdomain pypi.org + acl HCI_Dest_URLs dstdomain files.pythonhosted.org + acl HCI_Dest_URLs dstdomain raw.githubusercontent.com + acl HCI_Dest_URLs dstdomain msk8s.api.cdp.microsoft.com + # acl HCI_Dest_URLs dstdomain msk8s.sb.tlu.dl.delivery.mp.microsoft.com + acl HCI_Dest_URLs dstdomain k8connecthelm.azureedge.net + acl HCI_Dest_URLs dstdomain kvamanagementoperator.azurecr.io + acl HCI_Dest_URLs dstdomain packages.microsoft.com + acl HCI_Dest_URLs dstdomain k8sconnectcsp.azureedge.net + acl HCI_Dest_URLs dstdomain .prod.hot.ingest.monitor.core.windows.net + acl HCI_Dest_URLs dstdomain .dp.prod.appliances.azure.com + acl HCI_Dest_URLs dstdomain download.microsoft.com + acl HCI_Dest_URLs dstdomain pas.windows.net + acl HCI_Dest_URLs dstdomain guestnotificationservice.azure.com + acl HCI_Dest_URLs dstdomain .his.arc.azure.com + acl HCI_Dest_URLs dstdomain .guestconfiguration.azure.com + acl HCI_Dest_URLs dstdomain agentserviceapi.guestconfiguration.azure.com + acl HCI_Dest_URLs dstdomain .servicebus.windows.net + acl HCI_Dest_URLs dstdomain .waconazure.com + acl HCI_Dest_URLs dstdomain .gw.arc.azure.net + acl HCI_Dest_URLs dstdomain login.microsoftonline.com + acl HCI_Dest_URLs dstdomain graph.windows.net + acl HCI_Dest_URLs dstdomain graph.microsoft.com + acl HCI_Dest_URLs dstdomain login.windows.net + acl HCI_Dest_URLs dstdomain eastus.login.microsoft.com + acl HCI_Dest_URLs dstdomain southeastasia.login.microsoft.com + acl HCI_Dest_URLs dstdomain crl3.digicert.com + acl HCI_Dest_URLs dstdomain crl4.digicert.com + acl HCI_Dest_URLs dstdomain www.powershellgallery.com + acl HCI_Dest_URLs dstdomain psg-prod-eastus.azureedge.net + acl HCI_Dest_URLs dstdomain psg-prod-southeastasia.azureedge.net + acl HCI_Dest_URLs dstdomain onegetcdn.azureedge.net + acl HCI_Dest_URLs dstdomain portal.azure.com + acl HCI_Dest_URLs dstdomain .blob.core.windows.net + acl HCI_Dest_URLs dstdomain hciarcvmscontainerregistry.azurecr.io + acl HCI_Dest_URLs dstdomain azurestackreleases.download.prss.microsoft.com + acl HCI_Dest_URLs dstdomain settings-win.data.microsoft.com + acl HCI_Dest_URLs dstdomain dp.stackhci.azure.com + acl HCI_Dest_URLs dstdomain licensing.platform.edge.azure.com + acl HCI_Dest_URLs dstdomain billing.platform.edge.azure.com + acl HCI_Dest_URLs dstdomain azurestackhci.azurefd.net + acl HCI_Dest_URLs dstdomain .prod.microsoftmetrics.com + acl HCI_Dest_URLs dstdomain dc.services.visualstudio.com +# acl HCI_Dest_URLs dstdomain qos.prod.warm.ingest.monitor.core.windows.net + acl HCI_Dest_URLs dstdomain .prod.warm.ingest.monitor.core.windows.net + acl HCI_Dest_URLs dstdomain gcs.prod.monitoring.core.windows.net + acl HCI_Dest_URLs dstdomain adhs.events.data.microsoft.com + acl HCI_Dest_URLs dstdomain v20.events.data.microsoft.com + acl HCI_Dest_URLs dstdomain aka.ms + acl HCI_Dest_URLs dstdomain redirectiontool.trafficmanager.net +# acl HCI_Dest_URLs dstdomain fe3.delivery.mp.microsoft.com +# acl HCI_Dest_URLs dstdomain tlu.dl.delivery.mp.microsoft.com + acl HCI_Dest_URLs dstdomain www.microsoft.com + acl HCI_Dest_URLs dstdomain windowsupdate.microsoft.com +# acl HCI_Dest_URLs dstdomain .download.windowsupdate.com + acl HCI_Dest_URLs dstdomain wustat.windows.com + acl HCI_Dest_URLs dstdomain ntservicepack.microsoft.com + acl HCI_Dest_URLs dstdomain go.microsoft.com + acl HCI_Dest_URLs dstdomain .delivery.mp.microsoft.com + # acl HCI_Dest_URLs dstdomain .windowsupdate.microsoft.com + acl HCI_Dest_URLs dstdomain .windowsupdate.com + acl HCI_Dest_URLs dstdomain .update.microsoft.com + acl HCI_Dest_URLs dstdomain .endpoint.security.microsoft.com + acl HCI_Dest_URLs dstdomain www.office.com + acl HCI_Dest_URLs dstdomain login.microsoft.com + acl HCI_Dest_URLs dstdomain pythonhosted.org + acl HCI_Dest_URLs dstdomain .blob.storage.azure.net + acl HCI_Dest_URLs dstdomain oneocsp.microsoft.com + acl HCI_Dest_URLs dstdomain ts-crl.ws.symantec.com + acl HCI_Dest_URLs dstdomain ts-ocsp.ws.symantec.com + acl HCI_Dest_URLs dstdomain s.symcb.com + acl HCI_Dest_URLs dstdomain ocsp.digicert.com + acl HCI_Dest_URLs dstdomain ocsp2.globalsign.com + acl HCI_Dest_URLs dstdomain hciarcvmsstorage.z13.web.core.windows.net + acl HCI_Dest_URLs dstdomain management.azure.com + acl HCI_Dest_URLs dstdomain developer.microsoft.com + acl HCI_Dest_URLs dstdomain .vault.azure.net + acl HCI_Dest_URLs dstdomain .prod.hot.ingestion.msftcloudes.com # optional metrics and telemetry + acl HCI_Dest_URLs dstdomain edgesupprd.trafficmanager.net # optional support + acl HCI_Dest_URLs dstdomain .obo.arc.azure.com # optional arc - port 8084 + acl HCI_Dest_URLs dstdomain azurewatsonanalysis-prod.core.windows.net # optional observability + + acl HCI_Dest_URLs_regex dstdom_regex azgn[a-zA-Z0-9]+?\.servicebus\.windows\.net + + # list of URLs that are redirected to the gateway in preview see https://learn.microsoft.com/en-us/azure-stack/hci/deploy/deployment-azure-arc-gateway-overview#arc-enabled-server-endpoints-redirected-via-the-arc-gateway-in-limited-public-preview + acl HCI_ArcGW_Redirected_URLs dstdomain login.windows.net + acl HCI_ArcGW_Redirected_URLs dstdomain pas.windows.net + acl HCI_ArcGW_Redirected_URLs dstdomain .guestconfiguration.azure.com # Extension management and guest configuration services Always + acl HCI_ArcGW_Redirected_URLs dstdomain guestnotificationservice.azure.com # Notification service for extension and connectivity scenarios Always + #acl HCI_ArcGW_Redirected_URLs dstdomain .guestnotificationservice.azure.com # Notification service for extension and connectivity scenarios Always + acl HCI_ArcGW_Redirected_URLs dstdomain .servicesbus.windows.net # Multiple HCI services require access to this endpoint Always + acl HCI_ArcGW_Redirected_URLs dstdomain .waconazure.com # For Windows Admin Center connectivity If using Windows Admin Center + acl HCI_ArcGW_Redirected_URLs dstdomain .blob.core.windows.net # Multipsleeple HCI services require access to this endpoint Always + acl HCI_ArcGW_Redirected_URLs dstdomain dc.services.visualstudio.com # Multiple HCI services require access to this endpoint Always + + acl ArcGateway dstdomain .gw.arc.azure.com + + # used to test internet connectivity during HCI node deployment (tests NAT configuration) + acl testURL dstdomain ipinfo.io + + http_access deny !Safe_ports + + http_access deny CONNECT !SSL_ports + + http_access allow localhost manager + http_access deny manager + + http_port 3128 + + http_access allow ArcGateway + http_access allow testURL + + http_access deny HCI_ArcGW_Redirected_URLs !HCI_ResourceBridgeIPs # block redirected URLs except from the ARB + http_access allow HCI_Dest_URLs_regex + http_access allow HCI_Dest_URLs +EOF + +sudo systemctl restart squid +sudo systemctl enable squid + +sudo ufw allow 3128/tcp diff --git a/avm/res/azure-stack-hci/cluster/tests/e2e/defaults/dependencies.bicep b/avm/res/azure-stack-hci/cluster/tests/e2e/defaults/dependencies.bicep new file mode 100644 index 0000000000..4aa097ed92 --- /dev/null +++ b/avm/res/azure-stack-hci/cluster/tests/e2e/defaults/dependencies.bicep @@ -0,0 +1,145 @@ +@description('Optional. The password of the LCM deployment user and local administrator accounts.') +@secure() +param deploymentUserPassword string + +@description('Required. The password of the LCM deployment user and local administrator accounts.') +@secure() +param localAdminPassword string + +@description('Required. The app ID of the service principal used for the Azure Stack HCI Resource Bridge deployment. If omitted, the deploying user must have permissions to create service principals and role assignments in Entra ID.') +@secure() +param arbDeploymentAppId string + +@description('Required. The service principal ID of the service principal used for the Azure Stack HCI Resource Bridge deployment. If omitted, the deploying user must have permissions to create service principals and role assignments in Entra ID.') +@secure() +param arbDeploymentSPObjectId string + +@description('Required. The secret of the service principal used for the Azure Stack HCI Resource Bridge deployment. If omitted, the deploying user must have permissions to create service principals and role assignments in Entra ID.') +@secure() +param arbDeploymentServicePrincipalSecret string + +@description('Required. The location to deploy the resources into.') +param location string + +@description('Required. The name of the storage account to create as a cluster witness.') +param clusterWitnessStorageAccountName string + +@description('Required. The name of the storage account to be created to collect Key Vault diagnostic logs.') +param keyVaultDiagnosticStorageAccountName string + +@description('Required. The name of the Key Vault to create.') +param keyVaultName string + +@description('Required. The service principal ID of the Azure Stack HCI Resource Provider in this tenant.') +@secure() +param hciResourceProviderObjectId string + +@description('Required. The name of the Azure Stack HCI cluster.') +param clusterName string + +@description('Required. The name of the VM-managed user identity to create, used for HCI Arc onboarding.') +param userAssignedIdentityName string + +@description('Required. The name of the maintenance configuration for the Azure Stack HCI Host VM and proxy server.') +param maintenanceConfigurationName string + +@description('Required. The name of the Azure VM scale set for the HCI host.') +param HCIHostVirtualMachineScaleSetName string + +@description('Conditional. The name of the Network Security Group ro create.') +param networkSecurityGroupName string + +@description('Required. The name of the virtual network to create. Used to connect the HCI Azure Host VM to an existing VNET in the same region.') +param virtualNetworkName string + +@description('Required. The name of the Network Interface Card to create.') +param networkInterfaceName string + +@description('Required. The name prefix for the Disks to create.') +param diskNamePrefix string + +@description('Required. The name of the Azure VM to create.') +param virtualMachineName string + +@description('Required. The name of the Maintenance Configuration Assignment for the proxy server.') +param maintenanceConfigurationAssignmentName string + +@description('Required. The name prefix for the \'wait\' deployment scripts to create.') +param waitDeploymentScriptPrefixName string + +var clusterNodeNames = ['hcinode1', 'hcinode2'] +var domainOUPath = 'OU=HCI,DC=hci,DC=local' +module hciHostDeployment '../../e2e-template-assets/azureStackHCIHost/hciHostDeployment.bicep' = { + name: '${uniqueString(deployment().name, location)}-test-hcihostdeploy' + params: { + domainOUPath: domainOUPath + hciISODownloadURL: 'https://azurestackreleases.download.prss.microsoft.com/dbazure/AzureStackHCI/OS-Composition/10.2408.0.3061/AZURESTACKHci23H2.25398.469.LCM.10.2408.0.3061.x64.en-us.iso' + hciNodeCount: length(clusterNodeNames) + hostVMSize: 'Standard_E16bds_v5' + localAdminPassword: localAdminPassword + location: location + switchlessStorageConfig: false + diskNamePrefix: diskNamePrefix + HCIHostVirtualMachineScaleSetName: HCIHostVirtualMachineScaleSetName + maintenanceConfigurationAssignmentName: maintenanceConfigurationAssignmentName + maintenanceConfigurationName: maintenanceConfigurationName + networkInterfaceName: networkInterfaceName + networkSecurityGroupName: networkSecurityGroupName + virtualNetworkName: virtualNetworkName + userAssignedIdentityName: userAssignedIdentityName + virtualMachineName: virtualMachineName + waitDeploymentScriptPrefixName: waitDeploymentScriptPrefixName + } +} + +// create the HCI cluster resource - cloudId property is needed for KeyVault secret names +resource cluster 'Microsoft.AzureStackHCI/clusters@2024-04-01' = { + name: clusterName + identity: { + type: 'SystemAssigned' + } + location: location + properties: {} +} + +module hciClusterPreqs '../../e2e-template-assets/azureStackHCIClusterPreqs/ashciPrereqs.bicep' = { + name: '${uniqueString(deployment().name, location)}-test-hciclusterreqs' + params: { + location: location + arbDeploymentAppId: arbDeploymentAppId + arbDeploymentServicePrincipalSecret: arbDeploymentServicePrincipalSecret + arbDeploymentSPObjectId: arbDeploymentSPObjectId + arcNodeResourceIds: [ + for (nodeName, index) in clusterNodeNames: resourceId('Microsoft.HybridCompute/machines', nodeName) + ] + clusterWitnessStorageAccountName: clusterWitnessStorageAccountName + keyVaultDiagnosticStorageAccountName: keyVaultDiagnosticStorageAccountName + deploymentUsername: 'deployUser' + deploymentUserPassword: deploymentUserPassword + hciResourceProviderObjectId: hciResourceProviderObjectId + keyVaultName: keyVaultName + localAdminPassword: localAdminPassword + localAdminUsername: 'admin-hci' + logsRetentionInDays: 30 + softDeleteRetentionDays: 30 + tenantId: subscription().tenantId + vnetSubnetResourceId: hciHostDeployment.outputs.vnetSubnetResourceId + clusterName: clusterName + cloudId: cluster.properties.cloudId + } +} + +@description('The name of the created cluster') +output clusterName string = cluster.name + +@description('The name of the cluster\'s nodes.') +output clusterNodeNames array = clusterNodeNames + +@description('The name of the storage account used as the cluster witness.') +output clusterWitnessStorageAccountName string = clusterWitnessStorageAccountName + +@description('The OU path for the domain.') +output domainOUPath string = domainOUPath + +@description('The name of the created Key Vault.') +output keyVaultName string = keyVaultName diff --git a/avm/res/azure-stack-hci/cluster/tests/e2e/defaults/main.test.bicep b/avm/res/azure-stack-hci/cluster/tests/e2e/defaults/main.test.bicep new file mode 100644 index 0000000000..e9f5b6771e --- /dev/null +++ b/avm/res/azure-stack-hci/cluster/tests/e2e/defaults/main.test.bicep @@ -0,0 +1,176 @@ +targetScope = 'subscription' + +metadata name = 'Deploy Azure Stack HCI Cluster in Azure with a 2 node switched configuration' +metadata description = 'This test deploys an Azure VM to host a 2 node switched Azure Stack HCI cluster, validates the cluster configuration, and then deploys the cluster.' + +@description('Optional. The name of the resource group to deploy for testing purposes.') +@maxLength(90) +param resourceGroupName string = 'dep-${namePrefix}-azure-stack-hci.cluster-${serviceShort}-rg' + +@description('Optional. A short identifier for the kind of deployment. Should be kept short to not run into resource-name length-constraints.') +param serviceShort string = 'ashc2nmin' + +@description('Optional. A token to inject into the name of each resource.') +param namePrefix string = '#_namePrefix_#' + +@description('Optional. The password of the LCM deployment user and local administrator accounts.') +@secure() +param localAdminAndDeploymentUserPass string = newGuid() + +@description('Required. The app ID of the service principal used for the Azure Stack HCI Resource Bridge deployment.') +@secure() +#disable-next-line secure-parameter-default +param arbDeploymentAppId string = '' + +@description('Required. The service principal ID of the service principal used for the Azure Stack HCI Resource Bridge deployment.') +@secure() +#disable-next-line secure-parameter-default +param arbDeploymentSPObjectId string = '' + +@description('Required. The secret of the service principal used for the Azure Stack HCI Resource Bridge deployment.') +@secure() +#disable-next-line secure-parameter-default +param arbDeploymentServicePrincipalSecret string = '' + +@description('Required. The service principal object ID of the Azure Stack HCI Resource Provider in this tenant. Can be fetched via `Get-AzADServicePrincipal -ApplicationId 1412d89f-b8a8-4111-b4fd-e82905cbd85d` after the \'Microsoft.AzureStackHCI\' provider was registered in the subscription.') +@secure() +#disable-next-line secure-parameter-default +param hciResourceProviderObjectId string = '' + +#disable-next-line no-hardcoded-location // Due to quotas and capacity challenges, this region must be used in the AVM testing subscription +var enforcedLocation = 'southeastasia' + +resource resourceGroup 'Microsoft.Resources/resourceGroups@2024-03-01' = { + name: resourceGroupName + location: enforcedLocation +} + +module nestedDependencies 'dependencies.bicep' = { + name: '${uniqueString(deployment().name, enforcedLocation)}-test-nestedDependencies-${serviceShort}' + scope: resourceGroup + params: { + clusterName: '${namePrefix}${serviceShort}001' + clusterWitnessStorageAccountName: 'dep${namePrefix}wst${serviceShort}' + keyVaultDiagnosticStorageAccountName: 'dep${namePrefix}st${serviceShort}' + keyVaultName: 'dep-${namePrefix}-kv-${serviceShort}' + userAssignedIdentityName: 'dep-${namePrefix}-msi-${serviceShort}' + maintenanceConfigurationName: 'dep-${namePrefix}-mc-${serviceShort}' + maintenanceConfigurationAssignmentName: 'dep-${namePrefix}-mca-${serviceShort}' + HCIHostVirtualMachineScaleSetName: 'dep-${namePrefix}-hvmss-${serviceShort}' + virtualNetworkName: 'dep-${namePrefix}-vnet-${serviceShort}' + networkSecurityGroupName: 'dep-${namePrefix}-nsg-${serviceShort}' + networkInterfaceName: 'dep-${namePrefix}-mice-${serviceShort}' + diskNamePrefix: 'dep-${namePrefix}-disk-${serviceShort}' + virtualMachineName: 'dep-${namePrefix}-vm-${serviceShort}' + waitDeploymentScriptPrefixName: 'dep-${namePrefix}-wds-${serviceShort}' + arbDeploymentAppId: arbDeploymentAppId + arbDeploymentServicePrincipalSecret: arbDeploymentServicePrincipalSecret + arbDeploymentSPObjectId: arbDeploymentSPObjectId + deploymentUserPassword: localAdminAndDeploymentUserPass + hciResourceProviderObjectId: hciResourceProviderObjectId + localAdminPassword: localAdminAndDeploymentUserPass + location: enforcedLocation + } +} + +module testDeployment '../../../main.bicep' = { + name: '${uniqueString(deployment().name, enforcedLocation)}-test-clustermodule-${serviceShort}' + scope: resourceGroup + params: { + name: nestedDependencies.outputs.clusterName + deploymentSettings: { + customLocationName: '${namePrefix}${serviceShort}-location' + clusterNodeNames: nestedDependencies.outputs.clusterNodeNames + clusterWitnessStorageAccountName: nestedDependencies.outputs.clusterWitnessStorageAccountName + defaultGateway: '172.20.0.1' + deploymentPrefix: 'a${take(uniqueString(namePrefix, serviceShort), 7)}' // ensure deployment prefix starts with a letter to match '^(?=.{1,8}$)([a-zA-Z])(\-?[a-zA-Z\d])*$' + dnsServers: ['172.20.0.1'] + domainFqdn: 'hci.local' + domainOUPath: nestedDependencies.outputs.domainOUPath + startingIPAddress: '172.20.0.2' + endingIPAddress: '172.20.0.7' + enableStorageAutoIp: true + keyVaultName: nestedDependencies.outputs.keyVaultName + networkIntents: [ + { + adapter: ['mgmt'] + name: 'management' + overrideAdapterProperty: true + adapterPropertyOverrides: { + jumboPacket: '9014' + networkDirect: 'Disabled' + networkDirectTechnology: 'iWARP' + } + overrideQosPolicy: false + qosPolicyOverrides: { + bandwidthPercentage_SMB: '50' + priorityValue8021Action_Cluster: '7' + priorityValue8021Action_SMB: '3' + } + overrideVirtualSwitchConfiguration: false + virtualSwitchConfigurationOverrides: { + enableIov: 'true' + loadBalancingAlgorithm: 'Dynamic' + } + trafficType: ['Management'] + } + { + adapter: ['comp0', 'comp1'] + name: 'compute' + overrideAdapterProperty: true + adapterPropertyOverrides: { + jumboPacket: '9014' + networkDirect: 'Disabled' + networkDirectTechnology: 'iWARP' + } + overrideQosPolicy: false + qosPolicyOverrides: { + bandwidthPercentage_SMB: '50' + priorityValue8021Action_Cluster: '7' + priorityValue8021Action_SMB: '3' + } + overrideVirtualSwitchConfiguration: false + virtualSwitchConfigurationOverrides: { + enableIov: 'true' + loadBalancingAlgorithm: 'Dynamic' + } + trafficType: ['Compute'] + } + { + adapter: ['smb0', 'smb1'] + name: 'storage' + overrideAdapterProperty: true + adapterPropertyOverrides: { + jumboPacket: '9014' + networkDirect: 'Disabled' + networkDirectTechnology: 'iWARP' + } + overrideQosPolicy: true + qosPolicyOverrides: { + bandwidthPercentage_SMB: '50' + priorityValue8021Action_Cluster: '7' + priorityValue8021Action_SMB: '3' + } + overrideVirtualSwitchConfiguration: false + virtualSwitchConfigurationOverrides: { + enableIov: 'true' + loadBalancingAlgorithm: 'Dynamic' + } + trafficType: ['Storage'] + } + ] + storageConnectivitySwitchless: false + storageNetworks: [ + { + adapterName: 'smb0' + vlan: '711' + } + { + adapterName: 'smb1' + vlan: '712' + } + ] + subnetMask: '255.255.255.0' + } + } +} diff --git a/avm/res/azure-stack-hci/cluster/tests/e2e/waf-aligned/dependencies.bicep b/avm/res/azure-stack-hci/cluster/tests/e2e/waf-aligned/dependencies.bicep new file mode 100644 index 0000000000..4aa097ed92 --- /dev/null +++ b/avm/res/azure-stack-hci/cluster/tests/e2e/waf-aligned/dependencies.bicep @@ -0,0 +1,145 @@ +@description('Optional. The password of the LCM deployment user and local administrator accounts.') +@secure() +param deploymentUserPassword string + +@description('Required. The password of the LCM deployment user and local administrator accounts.') +@secure() +param localAdminPassword string + +@description('Required. The app ID of the service principal used for the Azure Stack HCI Resource Bridge deployment. If omitted, the deploying user must have permissions to create service principals and role assignments in Entra ID.') +@secure() +param arbDeploymentAppId string + +@description('Required. The service principal ID of the service principal used for the Azure Stack HCI Resource Bridge deployment. If omitted, the deploying user must have permissions to create service principals and role assignments in Entra ID.') +@secure() +param arbDeploymentSPObjectId string + +@description('Required. The secret of the service principal used for the Azure Stack HCI Resource Bridge deployment. If omitted, the deploying user must have permissions to create service principals and role assignments in Entra ID.') +@secure() +param arbDeploymentServicePrincipalSecret string + +@description('Required. The location to deploy the resources into.') +param location string + +@description('Required. The name of the storage account to create as a cluster witness.') +param clusterWitnessStorageAccountName string + +@description('Required. The name of the storage account to be created to collect Key Vault diagnostic logs.') +param keyVaultDiagnosticStorageAccountName string + +@description('Required. The name of the Key Vault to create.') +param keyVaultName string + +@description('Required. The service principal ID of the Azure Stack HCI Resource Provider in this tenant.') +@secure() +param hciResourceProviderObjectId string + +@description('Required. The name of the Azure Stack HCI cluster.') +param clusterName string + +@description('Required. The name of the VM-managed user identity to create, used for HCI Arc onboarding.') +param userAssignedIdentityName string + +@description('Required. The name of the maintenance configuration for the Azure Stack HCI Host VM and proxy server.') +param maintenanceConfigurationName string + +@description('Required. The name of the Azure VM scale set for the HCI host.') +param HCIHostVirtualMachineScaleSetName string + +@description('Conditional. The name of the Network Security Group ro create.') +param networkSecurityGroupName string + +@description('Required. The name of the virtual network to create. Used to connect the HCI Azure Host VM to an existing VNET in the same region.') +param virtualNetworkName string + +@description('Required. The name of the Network Interface Card to create.') +param networkInterfaceName string + +@description('Required. The name prefix for the Disks to create.') +param diskNamePrefix string + +@description('Required. The name of the Azure VM to create.') +param virtualMachineName string + +@description('Required. The name of the Maintenance Configuration Assignment for the proxy server.') +param maintenanceConfigurationAssignmentName string + +@description('Required. The name prefix for the \'wait\' deployment scripts to create.') +param waitDeploymentScriptPrefixName string + +var clusterNodeNames = ['hcinode1', 'hcinode2'] +var domainOUPath = 'OU=HCI,DC=hci,DC=local' +module hciHostDeployment '../../e2e-template-assets/azureStackHCIHost/hciHostDeployment.bicep' = { + name: '${uniqueString(deployment().name, location)}-test-hcihostdeploy' + params: { + domainOUPath: domainOUPath + hciISODownloadURL: 'https://azurestackreleases.download.prss.microsoft.com/dbazure/AzureStackHCI/OS-Composition/10.2408.0.3061/AZURESTACKHci23H2.25398.469.LCM.10.2408.0.3061.x64.en-us.iso' + hciNodeCount: length(clusterNodeNames) + hostVMSize: 'Standard_E16bds_v5' + localAdminPassword: localAdminPassword + location: location + switchlessStorageConfig: false + diskNamePrefix: diskNamePrefix + HCIHostVirtualMachineScaleSetName: HCIHostVirtualMachineScaleSetName + maintenanceConfigurationAssignmentName: maintenanceConfigurationAssignmentName + maintenanceConfigurationName: maintenanceConfigurationName + networkInterfaceName: networkInterfaceName + networkSecurityGroupName: networkSecurityGroupName + virtualNetworkName: virtualNetworkName + userAssignedIdentityName: userAssignedIdentityName + virtualMachineName: virtualMachineName + waitDeploymentScriptPrefixName: waitDeploymentScriptPrefixName + } +} + +// create the HCI cluster resource - cloudId property is needed for KeyVault secret names +resource cluster 'Microsoft.AzureStackHCI/clusters@2024-04-01' = { + name: clusterName + identity: { + type: 'SystemAssigned' + } + location: location + properties: {} +} + +module hciClusterPreqs '../../e2e-template-assets/azureStackHCIClusterPreqs/ashciPrereqs.bicep' = { + name: '${uniqueString(deployment().name, location)}-test-hciclusterreqs' + params: { + location: location + arbDeploymentAppId: arbDeploymentAppId + arbDeploymentServicePrincipalSecret: arbDeploymentServicePrincipalSecret + arbDeploymentSPObjectId: arbDeploymentSPObjectId + arcNodeResourceIds: [ + for (nodeName, index) in clusterNodeNames: resourceId('Microsoft.HybridCompute/machines', nodeName) + ] + clusterWitnessStorageAccountName: clusterWitnessStorageAccountName + keyVaultDiagnosticStorageAccountName: keyVaultDiagnosticStorageAccountName + deploymentUsername: 'deployUser' + deploymentUserPassword: deploymentUserPassword + hciResourceProviderObjectId: hciResourceProviderObjectId + keyVaultName: keyVaultName + localAdminPassword: localAdminPassword + localAdminUsername: 'admin-hci' + logsRetentionInDays: 30 + softDeleteRetentionDays: 30 + tenantId: subscription().tenantId + vnetSubnetResourceId: hciHostDeployment.outputs.vnetSubnetResourceId + clusterName: clusterName + cloudId: cluster.properties.cloudId + } +} + +@description('The name of the created cluster') +output clusterName string = cluster.name + +@description('The name of the cluster\'s nodes.') +output clusterNodeNames array = clusterNodeNames + +@description('The name of the storage account used as the cluster witness.') +output clusterWitnessStorageAccountName string = clusterWitnessStorageAccountName + +@description('The OU path for the domain.') +output domainOUPath string = domainOUPath + +@description('The name of the created Key Vault.') +output keyVaultName string = keyVaultName diff --git a/avm/res/azure-stack-hci/cluster/tests/e2e/waf-aligned/main.test.bicep b/avm/res/azure-stack-hci/cluster/tests/e2e/waf-aligned/main.test.bicep new file mode 100644 index 0000000000..5fd4a0c051 --- /dev/null +++ b/avm/res/azure-stack-hci/cluster/tests/e2e/waf-aligned/main.test.bicep @@ -0,0 +1,187 @@ +targetScope = 'subscription' + +metadata name = 'Deploy Azure Stack HCI Cluster in Azure with a 2 node switched configuration WAF aligned' +metadata description = 'This test deploys an Azure VM to host a 2 node switched Azure Stack HCI cluster, validates the cluster configuration, and then deploys the cluster WAF aligned.' + +@description('Optional. The name of the resource group to deploy for testing purposes.') +@maxLength(90) +param resourceGroupName string = 'dep-${namePrefix}-azure-stack-hci.cluster-${serviceShort}-rg' + +@description('Optional. A short identifier for the kind of deployment. Should be kept short to not run into resource-name length-constraints.') +param serviceShort string = 'ashc2nwaf' + +@description('Optional. A token to inject into the name of each resource.') +param namePrefix string = '#_namePrefix_#' + +@description('Optional. The password of the LCM deployment user and local administrator accounts.') +@secure() +param localAdminAndDeploymentUserPass string = newGuid() + +@description('Required. The app ID of the service principal used for the Azure Stack HCI Resource Bridge deployment.') +@secure() +#disable-next-line secure-parameter-default +param arbDeploymentAppId string = '' + +@description('Required. The service principal ID of the service principal used for the Azure Stack HCI Resource Bridge deployment.') +@secure() +#disable-next-line secure-parameter-default +param arbDeploymentSPObjectId string = '' + +@description('Required. The secret of the service principal used for the Azure Stack HCI Resource Bridge deployment.') +@secure() +#disable-next-line secure-parameter-default +param arbDeploymentServicePrincipalSecret string = '' + +@description('Required. The service principal object ID of the Azure Stack HCI Resource Provider in this tenant. Can be fetched via `Get-AzADServicePrincipal -ApplicationId 1412d89f-b8a8-4111-b4fd-e82905cbd85d` after the \'Microsoft.AzureStackHCI\' provider was registered in the subscription.') +@secure() +#disable-next-line secure-parameter-default +param hciResourceProviderObjectId string = '' + +#disable-next-line no-hardcoded-location // Due to quotas and capacity challenges, this region must be used in the AVM testing subscription +var enforcedLocation = 'southeastasia' + +resource resourceGroup 'Microsoft.Resources/resourceGroups@2024-03-01' = { + name: resourceGroupName + location: enforcedLocation +} + +module nestedDependencies 'dependencies.bicep' = { + name: '${uniqueString(deployment().name, enforcedLocation)}-test-nestedDependencies-${serviceShort}' + scope: resourceGroup + params: { + clusterName: '${namePrefix}${serviceShort}001' + clusterWitnessStorageAccountName: 'dep${namePrefix}wst${serviceShort}' + keyVaultDiagnosticStorageAccountName: 'dep${namePrefix}st${serviceShort}' + keyVaultName: 'dep-${namePrefix}-kv-${serviceShort}' + userAssignedIdentityName: 'dep-${namePrefix}-msi-${serviceShort}' + maintenanceConfigurationName: 'dep-${namePrefix}-mc-${serviceShort}' + maintenanceConfigurationAssignmentName: 'dep-${namePrefix}-mca-${serviceShort}' + HCIHostVirtualMachineScaleSetName: 'dep-${namePrefix}-hvmss-${serviceShort}' + virtualNetworkName: 'dep-${namePrefix}-vnet-${serviceShort}' + networkSecurityGroupName: 'dep-${namePrefix}-nsg-${serviceShort}' + networkInterfaceName: 'dep-${namePrefix}-mice-${serviceShort}' + diskNamePrefix: 'dep-${namePrefix}-disk-${serviceShort}' + virtualMachineName: 'dep-${namePrefix}-vm-${serviceShort}' + waitDeploymentScriptPrefixName: 'dep-${namePrefix}-wds-${serviceShort}' + arbDeploymentAppId: arbDeploymentAppId + arbDeploymentServicePrincipalSecret: arbDeploymentServicePrincipalSecret + arbDeploymentSPObjectId: arbDeploymentSPObjectId + deploymentUserPassword: localAdminAndDeploymentUserPass + hciResourceProviderObjectId: hciResourceProviderObjectId + localAdminPassword: localAdminAndDeploymentUserPass + location: enforcedLocation + } +} + +module testDeployment '../../../main.bicep' = { + name: '${uniqueString(deployment().name, enforcedLocation)}-test-clustermodule-${serviceShort}' + scope: resourceGroup + params: { + name: nestedDependencies.outputs.clusterName + deploymentSettings: { + customLocationName: '${namePrefix}${serviceShort}-location' + clusterNodeNames: nestedDependencies.outputs.clusterNodeNames + clusterWitnessStorageAccountName: nestedDependencies.outputs.clusterWitnessStorageAccountName + defaultGateway: '172.20.0.1' + deploymentPrefix: 'a${take(uniqueString(namePrefix, serviceShort), 7)}' // ensure deployment prefix starts with a letter to match '^(?=.{1,8}$)([a-zA-Z])(\-?[a-zA-Z\d])*$' + dnsServers: ['172.20.0.1'] + domainFqdn: 'hci.local' + domainOUPath: nestedDependencies.outputs.domainOUPath + startingIPAddress: '172.20.0.2' + endingIPAddress: '172.20.0.7' + enableStorageAutoIp: true + keyVaultName: nestedDependencies.outputs.keyVaultName + networkIntents: [ + { + adapter: ['mgmt'] + name: 'management' + overrideAdapterProperty: true + adapterPropertyOverrides: { + jumboPacket: '9014' + networkDirect: 'Disabled' + networkDirectTechnology: 'iWARP' + } + overrideQosPolicy: false + qosPolicyOverrides: { + bandwidthPercentage_SMB: '50' + priorityValue8021Action_Cluster: '7' + priorityValue8021Action_SMB: '3' + } + overrideVirtualSwitchConfiguration: false + virtualSwitchConfigurationOverrides: { + enableIov: 'true' + loadBalancingAlgorithm: 'Dynamic' + } + trafficType: ['Management'] + } + { + adapter: ['comp0', 'comp1'] + name: 'compute' + overrideAdapterProperty: true + adapterPropertyOverrides: { + jumboPacket: '9014' + networkDirect: 'Disabled' + networkDirectTechnology: 'iWARP' + } + overrideQosPolicy: false + qosPolicyOverrides: { + bandwidthPercentage_SMB: '50' + priorityValue8021Action_Cluster: '7' + priorityValue8021Action_SMB: '3' + } + overrideVirtualSwitchConfiguration: false + virtualSwitchConfigurationOverrides: { + enableIov: 'true' + loadBalancingAlgorithm: 'Dynamic' + } + trafficType: ['Compute'] + } + { + adapter: ['smb0', 'smb1'] + name: 'storage' + overrideAdapterProperty: true + adapterPropertyOverrides: { + jumboPacket: '9014' + networkDirect: 'Disabled' + networkDirectTechnology: 'iWARP' + } + overrideQosPolicy: true + qosPolicyOverrides: { + bandwidthPercentage_SMB: '50' + priorityValue8021Action_Cluster: '7' + priorityValue8021Action_SMB: '3' + } + overrideVirtualSwitchConfiguration: false + virtualSwitchConfigurationOverrides: { + enableIov: 'true' + loadBalancingAlgorithm: 'Dynamic' + } + trafficType: ['Storage'] + } + ] + storageConnectivitySwitchless: false + storageNetworks: [ + { + adapterName: 'smb0' + vlan: '711' + } + { + adapterName: 'smb1' + vlan: '712' + } + ] + subnetMask: '255.255.255.0' + driftControlEnforced: true + smbSigningEnforced: true + smbClusterEncryption: true + sideChannelMitigationEnforced: true + bitlockerBootVolume: true + bitlockerDataVolumes: true + } + tags: { + 'hidden-title': 'This is visible in the resource name' + Environment: 'Non-Prod' + Role: 'DeploymentValidation' + } + } +} diff --git a/avm/res/azure-stack-hci/cluster/version.json b/avm/res/azure-stack-hci/cluster/version.json new file mode 100644 index 0000000000..1c884ecaa9 --- /dev/null +++ b/avm/res/azure-stack-hci/cluster/version.json @@ -0,0 +1,7 @@ +{ + "$schema": "https://aka.ms/bicep-registry-module-version-file-schema#", + "version": "0.1", + "pathFilters": [ + "./main.json" + ] +} \ No newline at end of file From 1abc62726b0ff97ed454a1bdad500a0bf55ecd6a Mon Sep 17 00:00:00 2001 From: Alexander Sehr <ASehr@hotmail.de> Date: Wed, 12 Feb 2025 16:46:03 +0100 Subject: [PATCH 2/7] fix: Shortened test resource name for HCI cluster (#4436) ## Description Shortened test resource name for HCI cluster as it runs into a length issue (16 chars vs max of 15 chars) during test deployment No version update required as it only afffects test resources. ## Pipeline Reference <!-- Insert your Pipeline Status Badge below --> | Pipeline | | -------- | | | ## Type of Change <!-- Use the checkboxes [x] on the options that are relevant. --> - [ ] Update to CI Environment or utilities (Non-module affecting changes) - [ ] Azure Verified Module updates: - [x] Bugfix containing backwards-compatible bug fixes, and I have NOT bumped the MAJOR or MINOR version in `version.json`: - [ ] Someone has opened a bug report issue, and I have included "Closes #{bug_report_issue_number}" in the PR description. - [ ] The bug was found by the module author, and no one has opened an issue to report it yet. - [ ] Feature update backwards compatible feature updates, and I have bumped the MINOR version in `version.json`. - [ ] Breaking changes and I have bumped the MAJOR version in `version.json`. - [ ] Update to documentation --- avm/res/azure-stack-hci/cluster/README.md | 12 ++++++------ .../cluster/deployment-setting/main.json | 4 ++-- avm/res/azure-stack-hci/cluster/main.json | 8 ++++---- .../cluster/tests/e2e/defaults/main.test.bicep | 4 ++-- .../cluster/tests/e2e/waf-aligned/main.test.bicep | 4 ++-- 5 files changed, 16 insertions(+), 16 deletions(-) diff --git a/avm/res/azure-stack-hci/cluster/README.md b/avm/res/azure-stack-hci/cluster/README.md index 4cb635ad3f..3576848736 100644 --- a/avm/res/azure-stack-hci/cluster/README.md +++ b/avm/res/azure-stack-hci/cluster/README.md @@ -49,7 +49,7 @@ module cluster 'br/public:avm/res/azure-stack-hci/cluster:<version>' = { deploymentSettings: { clusterNodeNames: '<clusterNodeNames>' clusterWitnessStorageAccountName: '<clusterWitnessStorageAccountName>' - customLocationName: 'ashc2nmin-location' + customLocationName: 'ashcmin-location' defaultGateway: '172.20.0.1' deploymentPrefix: '<deploymentPrefix>' dnsServers: [ @@ -181,7 +181,7 @@ module cluster 'br/public:avm/res/azure-stack-hci/cluster:<version>' = { "value": { "clusterNodeNames": "<clusterNodeNames>", "clusterWitnessStorageAccountName": "<clusterWitnessStorageAccountName>", - "customLocationName": "ashc2nmin-location", + "customLocationName": "ashcmin-location", "defaultGateway": "172.20.0.1", "deploymentPrefix": "<deploymentPrefix>", "dnsServers": [ @@ -309,7 +309,7 @@ param name = '<name>' param deploymentSettings = { clusterNodeNames: '<clusterNodeNames>' clusterWitnessStorageAccountName: '<clusterWitnessStorageAccountName>' - customLocationName: 'ashc2nmin-location' + customLocationName: 'ashcmin-location' defaultGateway: '172.20.0.1' deploymentPrefix: '<deploymentPrefix>' dnsServers: [ @@ -442,7 +442,7 @@ module cluster 'br/public:avm/res/azure-stack-hci/cluster:<version>' = { bitlockerDataVolumes: true clusterNodeNames: '<clusterNodeNames>' clusterWitnessStorageAccountName: '<clusterWitnessStorageAccountName>' - customLocationName: 'ashc2nwaf-location' + customLocationName: 'ashcwaf-location' defaultGateway: '172.20.0.1' deploymentPrefix: '<deploymentPrefix>' dnsServers: [ @@ -585,7 +585,7 @@ module cluster 'br/public:avm/res/azure-stack-hci/cluster:<version>' = { "bitlockerDataVolumes": true, "clusterNodeNames": "<clusterNodeNames>", "clusterWitnessStorageAccountName": "<clusterWitnessStorageAccountName>", - "customLocationName": "ashc2nwaf-location", + "customLocationName": "ashcwaf-location", "defaultGateway": "172.20.0.1", "deploymentPrefix": "<deploymentPrefix>", "dnsServers": [ @@ -726,7 +726,7 @@ param deploymentSettings = { bitlockerDataVolumes: true clusterNodeNames: '<clusterNodeNames>' clusterWitnessStorageAccountName: '<clusterWitnessStorageAccountName>' - customLocationName: 'ashc2nwaf-location' + customLocationName: 'ashcwaf-location' defaultGateway: '172.20.0.1' deploymentPrefix: '<deploymentPrefix>' dnsServers: [ diff --git a/avm/res/azure-stack-hci/cluster/deployment-setting/main.json b/avm/res/azure-stack-hci/cluster/deployment-setting/main.json index 2edb982bfa..de60f2e4ac 100644 --- a/avm/res/azure-stack-hci/cluster/deployment-setting/main.json +++ b/avm/res/azure-stack-hci/cluster/deployment-setting/main.json @@ -5,8 +5,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.33.93.31351", - "templateHash": "7982158538530558379" + "version": "0.33.13.18514", + "templateHash": "10232132824907554113" }, "name": "Azure Stack HCI Cluster Deployment Settings", "description": "This module deploys an Azure Stack HCI Cluster Deployment Settings resource." diff --git a/avm/res/azure-stack-hci/cluster/main.json b/avm/res/azure-stack-hci/cluster/main.json index f082101b8d..0a2481719a 100644 --- a/avm/res/azure-stack-hci/cluster/main.json +++ b/avm/res/azure-stack-hci/cluster/main.json @@ -5,8 +5,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.33.93.31351", - "templateHash": "2661952410917464461" + "version": "0.33.13.18514", + "templateHash": "2800706656796817324" }, "name": "Azure Stack HCI Cluster", "description": "This module deploys an Azure Stack HCI Cluster on the provided Arc Machines." @@ -837,8 +837,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.33.93.31351", - "templateHash": "7982158538530558379" + "version": "0.33.13.18514", + "templateHash": "10232132824907554113" }, "name": "Azure Stack HCI Cluster Deployment Settings", "description": "This module deploys an Azure Stack HCI Cluster Deployment Settings resource." diff --git a/avm/res/azure-stack-hci/cluster/tests/e2e/defaults/main.test.bicep b/avm/res/azure-stack-hci/cluster/tests/e2e/defaults/main.test.bicep index e9f5b6771e..4bd69b5dcf 100644 --- a/avm/res/azure-stack-hci/cluster/tests/e2e/defaults/main.test.bicep +++ b/avm/res/azure-stack-hci/cluster/tests/e2e/defaults/main.test.bicep @@ -8,7 +8,7 @@ metadata description = 'This test deploys an Azure VM to host a 2 node switched param resourceGroupName string = 'dep-${namePrefix}-azure-stack-hci.cluster-${serviceShort}-rg' @description('Optional. A short identifier for the kind of deployment. Should be kept short to not run into resource-name length-constraints.') -param serviceShort string = 'ashc2nmin' +param serviceShort string = 'ashcmin' @description('Optional. A token to inject into the name of each resource.') param namePrefix string = '#_namePrefix_#' @@ -49,7 +49,7 @@ module nestedDependencies 'dependencies.bicep' = { name: '${uniqueString(deployment().name, enforcedLocation)}-test-nestedDependencies-${serviceShort}' scope: resourceGroup params: { - clusterName: '${namePrefix}${serviceShort}001' + clusterName: '${namePrefix}${serviceShort}1' clusterWitnessStorageAccountName: 'dep${namePrefix}wst${serviceShort}' keyVaultDiagnosticStorageAccountName: 'dep${namePrefix}st${serviceShort}' keyVaultName: 'dep-${namePrefix}-kv-${serviceShort}' diff --git a/avm/res/azure-stack-hci/cluster/tests/e2e/waf-aligned/main.test.bicep b/avm/res/azure-stack-hci/cluster/tests/e2e/waf-aligned/main.test.bicep index 5fd4a0c051..131c3d6769 100644 --- a/avm/res/azure-stack-hci/cluster/tests/e2e/waf-aligned/main.test.bicep +++ b/avm/res/azure-stack-hci/cluster/tests/e2e/waf-aligned/main.test.bicep @@ -8,7 +8,7 @@ metadata description = 'This test deploys an Azure VM to host a 2 node switched param resourceGroupName string = 'dep-${namePrefix}-azure-stack-hci.cluster-${serviceShort}-rg' @description('Optional. A short identifier for the kind of deployment. Should be kept short to not run into resource-name length-constraints.') -param serviceShort string = 'ashc2nwaf' +param serviceShort string = 'ashcwaf' @description('Optional. A token to inject into the name of each resource.') param namePrefix string = '#_namePrefix_#' @@ -49,7 +49,7 @@ module nestedDependencies 'dependencies.bicep' = { name: '${uniqueString(deployment().name, enforcedLocation)}-test-nestedDependencies-${serviceShort}' scope: resourceGroup params: { - clusterName: '${namePrefix}${serviceShort}001' + clusterName: '${namePrefix}${serviceShort}1' clusterWitnessStorageAccountName: 'dep${namePrefix}wst${serviceShort}' keyVaultDiagnosticStorageAccountName: 'dep${namePrefix}st${serviceShort}' keyVaultName: 'dep-${namePrefix}-kv-${serviceShort}' From 8c3e3e901324e35e3e050ebacceea6d30fce9c6e Mon Sep 17 00:00:00 2001 From: Joe Linn <120408555+jlinn-microsoft@users.noreply.github.com> Date: Wed, 12 Feb 2025 15:04:38 -0800 Subject: [PATCH 3/7] feat: New module `avm/res/app/session-pool` module. (#4178) ## Description <!-- >Thank you for your contribution ! > Please include a summary of the change and which issue is fixed. > Please also include the context. > List any dependencies that are required for this change. Fixes #123 Fixes #456 Closes #123 Closes #456 --> Adds the `avm/res/app/session-pool` module. Closes Azure/Azure-Verified-Modules#1764 ## Pipeline Reference <!-- Insert your Pipeline Status Badge below --> | Pipeline | | -------- | | [![avm.res.app.session-pool](https://github.com/jlinn-microsoft/bicep-registry-modules/actions/workflows/avm.res.app.session-pool.yml/badge.svg)](https://github.com/jlinn-microsoft/bicep-registry-modules/actions/workflows/avm.res.app.session-pool.yml) | ## Type of Change <!-- Use the checkboxes [x] on the options that are relevant. --> - [ ] Update to CI Environment or utilities (Non-module affecting changes) - [x] Azure Verified Module updates: - [ ] Bugfix containing backwards-compatible bug fixes, and I have NOT bumped the MAJOR or MINOR version in `version.json`: - [ ] Someone has opened a bug report issue, and I have included "Closes #{bug_report_issue_number}" in the PR description. - [ ] The bug was found by the module author, and no one has opened an issue to report it yet. - [ ] Feature update backwards compatible feature updates, and I have bumped the MINOR version in `version.json`. - [ ] Breaking changes and I have bumped the MAJOR version in `version.json`. - [ ] Update to documentation ## Checklist - [x] I'm sure there are no other open Pull Requests for the same update/change - [x] I have run `Set-AVMModule` locally to generate the supporting module files. - [x] My corresponding pipelines / checks run clean and green without any errors or warnings <!-- Please keep up to date with the contribution guide at https://aka.ms/avm/contribute/bicep --> --------- Co-authored-by: Erika Gressi <56914614+eriqua@users.noreply.github.com> --- .github/CODEOWNERS | 1 + .github/ISSUE_TEMPLATE/avm_module_issue.yml | 1 + .../workflows/avm.res.app.session-pool.yml | 88 ++ avm/res/app/session-pool/README.md | 878 ++++++++++++++++++ avm/res/app/session-pool/main.bicep | 283 ++++++ avm/res/app/session-pool/main.json | 601 ++++++++++++ .../tests/e2e/defaults/main.test.bicep | 50 + .../tests/e2e/max/dependencies.bicep | 16 + .../tests/e2e/max/main.test.bicep | 84 ++ .../tests/e2e/waf-aligned/main.test.bicep | 54 ++ avm/res/app/session-pool/version.json | 7 + 11 files changed, 2063 insertions(+) create mode 100644 .github/workflows/avm.res.app.session-pool.yml create mode 100644 avm/res/app/session-pool/README.md create mode 100644 avm/res/app/session-pool/main.bicep create mode 100644 avm/res/app/session-pool/main.json create mode 100644 avm/res/app/session-pool/tests/e2e/defaults/main.test.bicep create mode 100644 avm/res/app/session-pool/tests/e2e/max/dependencies.bicep create mode 100644 avm/res/app/session-pool/tests/e2e/max/main.test.bicep create mode 100644 avm/res/app/session-pool/tests/e2e/waf-aligned/main.test.bicep create mode 100644 avm/res/app/session-pool/version.json diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index aafe6f8a1a..08a92eb9b6 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -43,6 +43,7 @@ /avm/res/app/container-app/ @Azure/avm-res-app-containerapp-module-owners-bicep @Azure/avm-module-reviewers-bicep /avm/res/app/job/ @Azure/avm-res-app-job-module-owners-bicep @Azure/avm-module-reviewers-bicep /avm/res/app/managed-environment/ @Azure/avm-res-app-managedenvironment-module-owners-bicep @Azure/avm-module-reviewers-bicep +/avm/res/app/session-pool/ @Azure/avm-res-app-sessionpool-module-owners-bicep @Azure/avm-module-reviewers-bicep /avm/res/app-configuration/configuration-store/ @Azure/avm-res-appconfiguration-configurationstore-module-owners-bicep @Azure/avm-module-reviewers-bicep /avm/res/automation/automation-account/ @Azure/avm-res-automation-automationaccount-module-owners-bicep @Azure/avm-module-reviewers-bicep /avm/res/azure-stack-hci/cluster/ @Azure/avm-res-azurestackhci-cluster-module-owners-bicep @Azure/avm-module-reviewers-bicep diff --git a/.github/ISSUE_TEMPLATE/avm_module_issue.yml b/.github/ISSUE_TEMPLATE/avm_module_issue.yml index ed4b8fb00d..2dd1ba254e 100644 --- a/.github/ISSUE_TEMPLATE/avm_module_issue.yml +++ b/.github/ISSUE_TEMPLATE/avm_module_issue.yml @@ -79,6 +79,7 @@ body: - "avm/res/app/container-app" - "avm/res/app/job" - "avm/res/app/managed-environment" + - "avm/res/app/session-pool" - "avm/res/automation/automation-account" - "avm/res/azure-stack-hci/cluster" - "avm/res/batch/batch-account" diff --git a/.github/workflows/avm.res.app.session-pool.yml b/.github/workflows/avm.res.app.session-pool.yml new file mode 100644 index 0000000000..4ab8010119 --- /dev/null +++ b/.github/workflows/avm.res.app.session-pool.yml @@ -0,0 +1,88 @@ +name: "avm.res.app.session-pool" + +on: + workflow_dispatch: + inputs: + staticValidation: + type: boolean + description: "Execute static validation" + required: false + default: true + deploymentValidation: + type: boolean + description: "Execute deployment validation" + required: false + default: true + removeDeployment: + type: boolean + description: "Remove deployed module" + required: false + default: true + customLocation: + type: string + description: "Default location overwrite (e.g., eastus)" + required: false + push: + branches: + - main + paths: + - ".github/actions/templates/avm-**" + - ".github/workflows/avm.template.module.yml" + - ".github/workflows/avm.res.app.session-pool.yml" + - "avm/res/app/session-pool/**" + - "utilities/pipelines/**" + - "!utilities/pipelines/platform/**" + - "!*/**/README.md" + +env: + modulePath: "avm/res/app/session-pool" + workflowPath: ".github/workflows/avm.res.app.session-pool.yml" + +concurrency: + group: ${{ github.workflow }} + +jobs: + ########################### + # Initialize pipeline # + ########################### + job_initialize_pipeline: + runs-on: ubuntu-latest + name: "Initialize pipeline" + steps: + - name: "Checkout" + uses: actions/checkout@v4 + with: + fetch-depth: 0 + - name: "Set input parameters to output variables" + id: get-workflow-param + uses: ./.github/actions/templates/avm-getWorkflowInput + with: + workflowPath: "${{ env.workflowPath}}" + - name: "Get module test file paths" + id: get-module-test-file-paths + uses: ./.github/actions/templates/avm-getModuleTestFiles + with: + modulePath: "${{ env.modulePath }}" + outputs: + workflowInput: ${{ steps.get-workflow-param.outputs.workflowInput }} + moduleTestFilePaths: ${{ steps.get-module-test-file-paths.outputs.moduleTestFilePaths }} + psRuleModuleTestFilePaths: ${{ steps.get-module-test-file-paths.outputs.psRuleModuleTestFilePaths }} + modulePath: "${{ env.modulePath }}" + + ############################## + # Call reusable workflow # + ############################## + call-workflow-passing-data: + name: "Run" + permissions: + id-token: write # For OIDC + contents: write # For release tags + needs: + - job_initialize_pipeline + uses: ./.github/workflows/avm.template.module.yml + with: + workflowInput: "${{ needs.job_initialize_pipeline.outputs.workflowInput }}" + moduleTestFilePaths: "${{ needs.job_initialize_pipeline.outputs.moduleTestFilePaths }}" + psRuleModuleTestFilePaths: "${{ needs.job_initialize_pipeline.outputs.psRuleModuleTestFilePaths }}" + modulePath: "${{ needs.job_initialize_pipeline.outputs.modulePath}}" + secrets: inherit diff --git a/avm/res/app/session-pool/README.md b/avm/res/app/session-pool/README.md new file mode 100644 index 0000000000..c4b1b1db46 --- /dev/null +++ b/avm/res/app/session-pool/README.md @@ -0,0 +1,878 @@ +# Container App Session Pool `[Microsoft.App/sessionPools]` + +This module deploys a Container App Session Pool. + +## Navigation + +- [Resource Types](#Resource-Types) +- [Usage examples](#Usage-examples) +- [Parameters](#Parameters) +- [Outputs](#Outputs) +- [Cross-referenced modules](#Cross-referenced-modules) +- [Data Collection](#Data-Collection) + +## Resource Types + +| Resource Type | API Version | +| :-- | :-- | +| `Microsoft.App/sessionPools` | [2024-10-02-preview](https://learn.microsoft.com/en-us/azure/templates/Microsoft.App/2024-10-02-preview/sessionPools) | +| `Microsoft.Authorization/locks` | [2020-05-01](https://learn.microsoft.com/en-us/azure/templates/Microsoft.Authorization/2020-05-01/locks) | +| `Microsoft.Authorization/roleAssignments` | [2022-04-01](https://learn.microsoft.com/en-us/azure/templates/Microsoft.Authorization/2022-04-01/roleAssignments) | + +## Usage examples + +The following section provides usage examples for the module, which were used to validate and deploy the module successfully. For a full reference, please review the module's test folder in its repository. + +>**Note**: Each example lists all the required parameters first, followed by the rest - each in alphabetical order. + +>**Note**: To reference the module, please use the following syntax `br/public:avm/res/app/session-pool:<version>`. + +- [Using only defaults](#example-1-using-only-defaults) +- [Using large parameter set](#example-2-using-large-parameter-set) +- [WAF-aligned](#example-3-waf-aligned) + +### Example 1: _Using only defaults_ + +This instance deploys the module with the minimum set of required parameters. + + +<details> + +<summary>via Bicep module</summary> + +```bicep +module sessionPool 'br/public:avm/res/app/session-pool:<version>' = { + name: 'sessionPoolDeployment' + params: { + // Required parameters + containerType: 'PythonLTS' + name: 'aspmin001' + // Non-required parameters + location: '<location>' + } +} +``` + +</details> +<p> + +<details> + +<summary>via JSON parameters file</summary> + +```json +{ + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#", + "contentVersion": "1.0.0.0", + "parameters": { + // Required parameters + "containerType": { + "value": "PythonLTS" + }, + "name": { + "value": "aspmin001" + }, + // Non-required parameters + "location": { + "value": "<location>" + } + } +} +``` + +</details> +<p> + +<details> + +<summary>via Bicep parameters file</summary> + +```bicep-params +using 'br/public:avm/res/app/session-pool:<version>' + +// Required parameters +param containerType = 'PythonLTS' +param name = 'aspmin001' +// Non-required parameters +param location = '<location>' +``` + +</details> +<p> + +### Example 2: _Using large parameter set_ + +This instance deploys the module with most of its features enabled. + + +<details> + +<summary>via Bicep module</summary> + +```bicep +module sessionPool 'br/public:avm/res/app/session-pool:<version>' = { + name: 'sessionPoolDeployment' + params: { + // Required parameters + containerType: 'PythonLTS' + name: 'aspmax001' + // Non-required parameters + cooldownPeriodInSeconds: 350 + location: '<location>' + lock: { + kind: 'CanNotDelete' + name: 'myCustomLockName' + } + managedIdentitySettings: [ + { + identity: '<identity>' + lifecycle: 'Main' + } + ] + maxConcurrentSessions: 6 + poolManagementType: 'Dynamic' + readySessionInstances: 1 + roleAssignments: [ + { + principalId: '<principalId>' + principalType: 'ServicePrincipal' + roleDefinitionIdOrName: 'Azure ContainerApps Session Executor' + } + ] + sessionNetworkStatus: 'EgressDisabled' + tags: { + resourceType: 'Session Pool' + } + } +} +``` + +</details> +<p> + +<details> + +<summary>via JSON parameters file</summary> + +```json +{ + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#", + "contentVersion": "1.0.0.0", + "parameters": { + // Required parameters + "containerType": { + "value": "PythonLTS" + }, + "name": { + "value": "aspmax001" + }, + // Non-required parameters + "cooldownPeriodInSeconds": { + "value": 350 + }, + "location": { + "value": "<location>" + }, + "lock": { + "value": { + "kind": "CanNotDelete", + "name": "myCustomLockName" + } + }, + "managedIdentitySettings": { + "value": [ + { + "identity": "<identity>", + "lifecycle": "Main" + } + ] + }, + "maxConcurrentSessions": { + "value": 6 + }, + "poolManagementType": { + "value": "Dynamic" + }, + "readySessionInstances": { + "value": 1 + }, + "roleAssignments": { + "value": [ + { + "principalId": "<principalId>", + "principalType": "ServicePrincipal", + "roleDefinitionIdOrName": "Azure ContainerApps Session Executor" + } + ] + }, + "sessionNetworkStatus": { + "value": "EgressDisabled" + }, + "tags": { + "value": { + "resourceType": "Session Pool" + } + } + } +} +``` + +</details> +<p> + +<details> + +<summary>via Bicep parameters file</summary> + +```bicep-params +using 'br/public:avm/res/app/session-pool:<version>' + +// Required parameters +param containerType = 'PythonLTS' +param name = 'aspmax001' +// Non-required parameters +param cooldownPeriodInSeconds = 350 +param location = '<location>' +param lock = { + kind: 'CanNotDelete' + name: 'myCustomLockName' +} +param managedIdentitySettings = [ + { + identity: '<identity>' + lifecycle: 'Main' + } +] +param maxConcurrentSessions = 6 +param poolManagementType = 'Dynamic' +param readySessionInstances = 1 +param roleAssignments = [ + { + principalId: '<principalId>' + principalType: 'ServicePrincipal' + roleDefinitionIdOrName: 'Azure ContainerApps Session Executor' + } +] +param sessionNetworkStatus = 'EgressDisabled' +param tags = { + resourceType: 'Session Pool' +} +``` + +</details> +<p> + +### Example 3: _WAF-aligned_ + +This instance deploys the module in alignment with the best-practices of the Azure Well-Architected Framework. + + +<details> + +<summary>via Bicep module</summary> + +```bicep +module sessionPool 'br/public:avm/res/app/session-pool:<version>' = { + name: 'sessionPoolDeployment' + params: { + // Required parameters + containerType: 'PythonLTS' + name: 'aspwaf001' + // Non-required parameters + location: '<location>' + sessionNetworkStatus: 'EgressDisabled' + tags: { + resourceType: 'Session Pool' + } + } +} +``` + +</details> +<p> + +<details> + +<summary>via JSON parameters file</summary> + +```json +{ + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#", + "contentVersion": "1.0.0.0", + "parameters": { + // Required parameters + "containerType": { + "value": "PythonLTS" + }, + "name": { + "value": "aspwaf001" + }, + // Non-required parameters + "location": { + "value": "<location>" + }, + "sessionNetworkStatus": { + "value": "EgressDisabled" + }, + "tags": { + "value": { + "resourceType": "Session Pool" + } + } + } +} +``` + +</details> +<p> + +<details> + +<summary>via Bicep parameters file</summary> + +```bicep-params +using 'br/public:avm/res/app/session-pool:<version>' + +// Required parameters +param containerType = 'PythonLTS' +param name = 'aspwaf001' +// Non-required parameters +param location = '<location>' +param sessionNetworkStatus = 'EgressDisabled' +param tags = { + resourceType: 'Session Pool' +} +``` + +</details> +<p> + +## Parameters + +**Required parameters** + +| Parameter | Type | Description | +| :-- | :-- | :-- | +| [`containerType`](#parameter-containertype) | string | The container type of the sessions. | +| [`name`](#parameter-name) | string | Name of the Container App Session Pool. | + +**Optional parameters** + +| Parameter | Type | Description | +| :-- | :-- | :-- | +| [`containers`](#parameter-containers) | array | Custom container definitions. Only required if containerType is CustomContainer. | +| [`cooldownPeriodInSeconds`](#parameter-cooldownperiodinseconds) | int | The cooldown period of a session in seconds. | +| [`enableTelemetry`](#parameter-enabletelemetry) | bool | Enable/Disable usage telemetry for module. | +| [`environmentId`](#parameter-environmentid) | string | Resource ID of the session pool's environment. | +| [`location`](#parameter-location) | string | Location for all Resources. | +| [`lock`](#parameter-lock) | object | The lock settings of the service. | +| [`managedIdentities`](#parameter-managedidentities) | object | The managed identity definition for this resource. | +| [`managedIdentitySettings`](#parameter-managedidentitysettings) | array | Settings for a Managed Identity that is assigned to the Session pool. | +| [`maxConcurrentSessions`](#parameter-maxconcurrentsessions) | int | The maximum count of sessions at the same time. | +| [`poolManagementType`](#parameter-poolmanagementtype) | string | The pool management type of the session pool. Defaults to Dynamic. | +| [`readySessionInstances`](#parameter-readysessioninstances) | int | The minimum count of ready session instances. | +| [`registryCredentials`](#parameter-registrycredentials) | object | Container registry credentials. Only required if containerType is CustomContainer and the container registry requires authentication. | +| [`roleAssignments`](#parameter-roleassignments) | array | Array of role assignments to create. | +| [`sessionNetworkStatus`](#parameter-sessionnetworkstatus) | string | Network status for the sessions. Defaults to EgressDisabled. | +| [`tags`](#parameter-tags) | object | Tags of the Automation Account resource. | +| [`targetIngressPort`](#parameter-targetingressport) | int | Required if containerType == 'CustomContainer'. Target port in containers for traffic from ingress. Only required if containerType is CustomContainer. | + +### Parameter: `containerType` + +The container type of the sessions. + +- Required: Yes +- Type: string +- Allowed: + ```Bicep + [ + 'CustomContainer' + 'PythonLTS' + ] + ``` + +### Parameter: `name` + +Name of the Container App Session Pool. + +- Required: Yes +- Type: string + +### Parameter: `containers` + +Custom container definitions. Only required if containerType is CustomContainer. + +- Required: No +- Type: array + +**Required parameters** + +| Parameter | Type | Description | +| :-- | :-- | :-- | +| [`image`](#parameter-containersimage) | string | Container image tag. | +| [`name`](#parameter-containersname) | string | Custom container name. | +| [`resources`](#parameter-containersresources) | object | Container resource requirements. | + +**Optional parameters** + +| Parameter | Type | Description | +| :-- | :-- | :-- | +| [`args`](#parameter-containersargs) | array | Container start command arguments. | +| [`command`](#parameter-containerscommand) | array | Container start command. | +| [`env`](#parameter-containersenv) | array | Container environment variables. | + +### Parameter: `containers.image` + +Container image tag. + +- Required: Yes +- Type: string + +### Parameter: `containers.name` + +Custom container name. + +- Required: Yes +- Type: string + +### Parameter: `containers.resources` + +Container resource requirements. + +- Required: Yes +- Type: object + +**Required parameters** + +| Parameter | Type | Description | +| :-- | :-- | :-- | +| [`cpu`](#parameter-containersresourcescpu) | string | Required CPU in cores, e.g. 0.5. | +| [`memory`](#parameter-containersresourcesmemory) | string | Required memory, e.g. "1.25Gi". | + +### Parameter: `containers.resources.cpu` + +Required CPU in cores, e.g. 0.5. + +- Required: Yes +- Type: string + +### Parameter: `containers.resources.memory` + +Required memory, e.g. "1.25Gi". + +- Required: Yes +- Type: string + +### Parameter: `containers.args` + +Container start command arguments. + +- Required: No +- Type: array + +### Parameter: `containers.command` + +Container start command. + +- Required: No +- Type: array + +### Parameter: `containers.env` + +Container environment variables. + +- Required: No +- Type: array + +**Required parameters** + +| Parameter | Type | Description | +| :-- | :-- | :-- | +| [`name`](#parameter-containersenvname) | string | Environment variable name. | + +**Optional parameters** + +| Parameter | Type | Description | +| :-- | :-- | :-- | +| [`secretRef`](#parameter-containersenvsecretref) | string | Required if value is not set. Name of the Container App secret from which to pull the environment variable value. | +| [`value`](#parameter-containersenvvalue) | string | Required if secretRef is not set. Non-secret environment variable value. | + +### Parameter: `containers.env.name` + +Environment variable name. + +- Required: Yes +- Type: string + +### Parameter: `containers.env.secretRef` + +Required if value is not set. Name of the Container App secret from which to pull the environment variable value. + +- Required: No +- Type: string + +### Parameter: `containers.env.value` + +Required if secretRef is not set. Non-secret environment variable value. + +- Required: No +- Type: string + +### Parameter: `cooldownPeriodInSeconds` + +The cooldown period of a session in seconds. + +- Required: No +- Type: int +- Default: `300` + +### Parameter: `enableTelemetry` + +Enable/Disable usage telemetry for module. + +- Required: No +- Type: bool +- Default: `True` + +### Parameter: `environmentId` + +Resource ID of the session pool's environment. + +- Required: No +- Type: string + +### Parameter: `location` + +Location for all Resources. + +- Required: No +- Type: string +- Default: `[resourceGroup().location]` + +### Parameter: `lock` + +The lock settings of the service. + +- Required: No +- Type: object + +**Optional parameters** + +| Parameter | Type | Description | +| :-- | :-- | :-- | +| [`kind`](#parameter-lockkind) | string | Specify the type of lock. | +| [`name`](#parameter-lockname) | string | Specify the name of lock. | + +### Parameter: `lock.kind` + +Specify the type of lock. + +- Required: No +- Type: string +- Allowed: + ```Bicep + [ + 'CanNotDelete' + 'None' + 'ReadOnly' + ] + ``` + +### Parameter: `lock.name` + +Specify the name of lock. + +- Required: No +- Type: string + +### Parameter: `managedIdentities` + +The managed identity definition for this resource. + +- Required: No +- Type: object + +**Optional parameters** + +| Parameter | Type | Description | +| :-- | :-- | :-- | +| [`systemAssigned`](#parameter-managedidentitiessystemassigned) | bool | Enables system assigned managed identity on the resource. | +| [`userAssignedResourceIds`](#parameter-managedidentitiesuserassignedresourceids) | array | The resource ID(s) to assign to the resource. Required if a user assigned identity is used for encryption. | + +### Parameter: `managedIdentities.systemAssigned` + +Enables system assigned managed identity on the resource. + +- Required: No +- Type: bool + +### Parameter: `managedIdentities.userAssignedResourceIds` + +The resource ID(s) to assign to the resource. Required if a user assigned identity is used for encryption. + +- Required: No +- Type: array + +### Parameter: `managedIdentitySettings` + +Settings for a Managed Identity that is assigned to the Session pool. + +- Required: No +- Type: array + +**Required parameters** + +| Parameter | Type | Description | +| :-- | :-- | :-- | +| [`identity`](#parameter-managedidentitysettingsidentity) | string | The resource ID of a user-assigned managed identity that is assigned to the Session Pool, or "system" for system-assigned identity. | +| [`lifecycle`](#parameter-managedidentitysettingslifecycle) | string | Use to select the lifecycle stages of a Session Pool during which the Managed Identity should be available. Valid values: "All", "Init", "Main", "None". | + +### Parameter: `managedIdentitySettings.identity` + +The resource ID of a user-assigned managed identity that is assigned to the Session Pool, or "system" for system-assigned identity. + +- Required: Yes +- Type: string + +### Parameter: `managedIdentitySettings.lifecycle` + +Use to select the lifecycle stages of a Session Pool during which the Managed Identity should be available. Valid values: "All", "Init", "Main", "None". + +- Required: Yes +- Type: string + +### Parameter: `maxConcurrentSessions` + +The maximum count of sessions at the same time. + +- Required: No +- Type: int +- Default: `5` + +### Parameter: `poolManagementType` + +The pool management type of the session pool. Defaults to Dynamic. + +- Required: No +- Type: string +- Default: `'Dynamic'` +- Allowed: + ```Bicep + [ + 'Dynamic' + 'Manual' + ] + ``` + +### Parameter: `readySessionInstances` + +The minimum count of ready session instances. + +- Required: No +- Type: int + +### Parameter: `registryCredentials` + +Container registry credentials. Only required if containerType is CustomContainer and the container registry requires authentication. + +- Required: No +- Type: object + +**Required parameters** + +| Parameter | Type | Description | +| :-- | :-- | :-- | +| [`server`](#parameter-registrycredentialsserver) | string | Container registry server. | +| [`username`](#parameter-registrycredentialsusername) | string | Container registry username. | + +**Optional parameters** + +| Parameter | Type | Description | +| :-- | :-- | :-- | +| [`identity`](#parameter-registrycredentialsidentity) | string | A Managed Identity to use to authenticate with Azure Container Registry. For user-assigned identities, use the full user-assigned identity Resource ID. For system-assigned identities, use "system". | +| [`passwordSecretRef`](#parameter-registrycredentialspasswordsecretref) | string | The name of the secret that contains the registry login password. Not used if identity is specified. | + +### Parameter: `registryCredentials.server` + +Container registry server. + +- Required: Yes +- Type: string + +### Parameter: `registryCredentials.username` + +Container registry username. + +- Required: Yes +- Type: string + +### Parameter: `registryCredentials.identity` + +A Managed Identity to use to authenticate with Azure Container Registry. For user-assigned identities, use the full user-assigned identity Resource ID. For system-assigned identities, use "system". + +- Required: No +- Type: string + +### Parameter: `registryCredentials.passwordSecretRef` + +The name of the secret that contains the registry login password. Not used if identity is specified. + +- Required: No +- Type: string + +### Parameter: `roleAssignments` + +Array of role assignments to create. + +- Required: No +- Type: array +- Roles configurable by name: + - `'Azure ContainerApps Session Executor'` + - `'Container Apps SessionPools Contributor'` + - `'Container Apps SessionPools Reader'` + - `'Contributor'` + - `'Owner'` + - `'Reader'` + - `'Role Based Access Control Administrator'` + - `'User Access Administrator'` + +**Required parameters** + +| Parameter | Type | Description | +| :-- | :-- | :-- | +| [`principalId`](#parameter-roleassignmentsprincipalid) | string | The principal ID of the principal (user/group/identity) to assign the role to. | +| [`roleDefinitionIdOrName`](#parameter-roleassignmentsroledefinitionidorname) | string | The role to assign. You can provide either the display name of the role definition, the role definition GUID, or its fully qualified ID in the following format: '/providers/Microsoft.Authorization/roleDefinitions/c2f4ef07-c644-48eb-af81-4b1b4947fb11'. | + +**Optional parameters** + +| Parameter | Type | Description | +| :-- | :-- | :-- | +| [`condition`](#parameter-roleassignmentscondition) | string | The conditions on the role assignment. This limits the resources it can be assigned to. e.g.: @Resource[Microsoft.Storage/storageAccounts/blobServices/containers:ContainerName] StringEqualsIgnoreCase "foo_storage_container". | +| [`conditionVersion`](#parameter-roleassignmentsconditionversion) | string | Version of the condition. | +| [`delegatedManagedIdentityResourceId`](#parameter-roleassignmentsdelegatedmanagedidentityresourceid) | string | The Resource Id of the delegated managed identity resource. | +| [`description`](#parameter-roleassignmentsdescription) | string | The description of the role assignment. | +| [`name`](#parameter-roleassignmentsname) | string | The name (as GUID) of the role assignment. If not provided, a GUID will be generated. | +| [`principalType`](#parameter-roleassignmentsprincipaltype) | string | The principal type of the assigned principal ID. | + +### Parameter: `roleAssignments.principalId` + +The principal ID of the principal (user/group/identity) to assign the role to. + +- Required: Yes +- Type: string + +### Parameter: `roleAssignments.roleDefinitionIdOrName` + +The role to assign. You can provide either the display name of the role definition, the role definition GUID, or its fully qualified ID in the following format: '/providers/Microsoft.Authorization/roleDefinitions/c2f4ef07-c644-48eb-af81-4b1b4947fb11'. + +- Required: Yes +- Type: string + +### Parameter: `roleAssignments.condition` + +The conditions on the role assignment. This limits the resources it can be assigned to. e.g.: @Resource[Microsoft.Storage/storageAccounts/blobServices/containers:ContainerName] StringEqualsIgnoreCase "foo_storage_container". + +- Required: No +- Type: string + +### Parameter: `roleAssignments.conditionVersion` + +Version of the condition. + +- Required: No +- Type: string +- Allowed: + ```Bicep + [ + '2.0' + ] + ``` + +### Parameter: `roleAssignments.delegatedManagedIdentityResourceId` + +The Resource Id of the delegated managed identity resource. + +- Required: No +- Type: string + +### Parameter: `roleAssignments.description` + +The description of the role assignment. + +- Required: No +- Type: string + +### Parameter: `roleAssignments.name` + +The name (as GUID) of the role assignment. If not provided, a GUID will be generated. + +- Required: No +- Type: string + +### Parameter: `roleAssignments.principalType` + +The principal type of the assigned principal ID. + +- Required: No +- Type: string +- Allowed: + ```Bicep + [ + 'Device' + 'ForeignGroup' + 'Group' + 'ServicePrincipal' + 'User' + ] + ``` + +### Parameter: `sessionNetworkStatus` + +Network status for the sessions. Defaults to EgressDisabled. + +- Required: No +- Type: string +- Default: `'EgressDisabled'` +- Allowed: + ```Bicep + [ + 'EgressDisabled' + 'EgressEnabled' + ] + ``` + +### Parameter: `tags` + +Tags of the Automation Account resource. + +- Required: No +- Type: object + +### Parameter: `targetIngressPort` + +Required if containerType == 'CustomContainer'. Target port in containers for traffic from ingress. Only required if containerType is CustomContainer. + +- Required: No +- Type: int + +## Outputs + +| Output | Type | Description | +| :-- | :-- | :-- | +| `managementEndpoint` | string | The management endpoint of the session pool. | +| `name` | string | The name of the session pool. | +| `resourceGroupName` | string | The name of the resource group in which the session pool was created. | +| `resourceId` | string | The resource ID of the deployed session pool. | +| `systemAssignedMIPrincipalId` | string | The principal ID of the system assigned identity. | + +## Cross-referenced modules + +This section gives you an overview of all local-referenced module files (i.e., other modules that are referenced in this module) and all remote-referenced files (i.e., Bicep modules that are referenced from a Bicep Registry or Template Specs). + +| Reference | Type | +| :-- | :-- | +| `br/public:avm/utl/types/avm-common-types:0.2.1` | Remote reference | + +## Data Collection + +The software may collect information about you and your use of the software and send it to Microsoft. Microsoft may use this information to provide services and improve our products and services. You may turn off the telemetry as described in the [repository](https://aka.ms/avm/telemetry). There are also some features in the software that may enable you and Microsoft to collect data from users of your applications. If you use these features, you must comply with applicable law, including providing appropriate notices to users of your applications together with a copy of Microsoft’s privacy statement. Our privacy statement is located at <https://go.microsoft.com/fwlink/?LinkID=824704>. You can learn more about data collection and use in the help documentation and our privacy statement. Your use of the software operates as your consent to these practices. diff --git a/avm/res/app/session-pool/main.bicep b/avm/res/app/session-pool/main.bicep new file mode 100644 index 0000000000..bd37c80d76 --- /dev/null +++ b/avm/res/app/session-pool/main.bicep @@ -0,0 +1,283 @@ +metadata name = 'Container App Session Pool' +metadata description = 'This module deploys a Container App Session Pool.' + +@description('Required. Name of the Container App Session Pool.') +param name string + +@description('Optional. Location for all Resources.') +param location string = resourceGroup().location + +@description('Required. The container type of the sessions.') +@allowed(['PythonLTS', 'CustomContainer']) +param containerType string + +@description('Optional. Custom container definitions. Only required if containerType is CustomContainer.') +param containers sessionContainerType[]? + +@description('Optional. Required if containerType == \'CustomContainer\'. Target port in containers for traffic from ingress. Only required if containerType is CustomContainer.') +param targetIngressPort int? + +@description('Optional. Container registry credentials. Only required if containerType is CustomContainer and the container registry requires authentication.') +param registryCredentials sessionRegistryCredentialsType? + +@description('Optional. The cooldown period of a session in seconds.') +param cooldownPeriodInSeconds int = 300 + +@description('Optional. The maximum count of sessions at the same time.') +param maxConcurrentSessions int = 5 + +@description('Optional. The minimum count of ready session instances.') +param readySessionInstances int? + +@description('Optional. Network status for the sessions. Defaults to EgressDisabled.') +@allowed(['EgressEnabled', 'EgressDisabled']) +param sessionNetworkStatus string = 'EgressDisabled' + +@description('Optional. The pool management type of the session pool. Defaults to Dynamic.') +@allowed(['Dynamic', 'Manual']) +param poolManagementType string = 'Dynamic' + +import { lockType } from 'br/public:avm/utl/types/avm-common-types:0.2.1' +@description('Optional. The lock settings of the service.') +param lock lockType? + +import { roleAssignmentType } from 'br/public:avm/utl/types/avm-common-types:0.2.1' +@description('Optional. Array of role assignments to create.') +param roleAssignments roleAssignmentType[]? + +import { managedIdentityAllType } from 'br/public:avm/utl/types/avm-common-types:0.2.1' +@description('Optional. The managed identity definition for this resource.') +param managedIdentities managedIdentityAllType? + +@description('Optional. Settings for a Managed Identity that is assigned to the Session pool.') +param managedIdentitySettings managedIdentitySettingType[]? + +@description('Optional. Resource ID of the session pool\'s environment.') +param environmentId string? + +@description('Optional. Tags of the Automation Account resource.') +param tags object? + +@description('Optional. Enable/Disable usage telemetry for module.') +param enableTelemetry bool = true + +var formattedUserAssignedIdentities = reduce( + map((managedIdentities.?userAssignedResourceIds ?? []), (id) => { '${id}': {} }), + {}, + (cur, next) => union(cur, next) +) // Converts the flat array to an object like { '${id1}': {}, '${id2}': {} } + +var identity = !empty(managedIdentities) + ? { + type: (managedIdentities.?systemAssigned ?? false) + ? (!empty(managedIdentities.?userAssignedResourceIds ?? {}) ? 'SystemAssigned,UserAssigned' : 'SystemAssigned') + : (!empty(managedIdentities.?userAssignedResourceIds ?? {}) ? 'UserAssigned' : 'None') + userAssignedIdentities: !empty(formattedUserAssignedIdentities) ? formattedUserAssignedIdentities : null + } + : null + +var builtInRoleNames = { + 'Azure ContainerApps Session Executor': subscriptionResourceId( + 'Microsoft.Authorization/roleDefinitions', + '0fb8eba5-a2bb-4abe-b1c1-49dfad359bb0' + ) + 'Container Apps SessionPools Contributor': subscriptionResourceId( + 'Microsoft.Authorization/roleDefinitions', + 'f7669afb-68b2-44b4-9c5f-6d2a47fddda0' + ) + 'Container Apps SessionPools Reader': subscriptionResourceId( + 'Microsoft.Authorization/roleDefinitions', + 'af61e8fc-2633-4b95-bed3-421ad6826515' + ) + Contributor: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'b24988ac-6180-42a0-ab88-20f7382dd24c') + Owner: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '8e3af657-a8ff-443c-a75c-2fe8c4bcb635') + Reader: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'acdd72a7-3385-48ef-bd42-f606fba81ae7') + 'Role Based Access Control Administrator': subscriptionResourceId( + 'Microsoft.Authorization/roleDefinitions', + 'f58310d9-a9f6-439a-9e8d-f62e7b41a168' + ) + 'User Access Administrator': subscriptionResourceId( + 'Microsoft.Authorization/roleDefinitions', + '18d7d88d-d35e-4fb5-a5c3-7773c20a72d9' + ) +} + +var formattedRoleAssignments = [ + for (roleAssignment, index) in (roleAssignments ?? []): union(roleAssignment, { + roleDefinitionId: builtInRoleNames[?roleAssignment.roleDefinitionIdOrName] ?? (contains( + roleAssignment.roleDefinitionIdOrName, + '/providers/Microsoft.Authorization/roleDefinitions/' + ) + ? roleAssignment.roleDefinitionIdOrName + : subscriptionResourceId('Microsoft.Authorization/roleDefinitions', roleAssignment.roleDefinitionIdOrName)) + }) +] + +#disable-next-line no-deployments-resources +resource avmTelemetry 'Microsoft.Resources/deployments@2024-03-01' = if (enableTelemetry) { + name: '46d3xbcp.res.app-sessionpool.${replace('-..--..-', '.', '-')}.${substring(uniqueString(deployment().name, location), 0, 4)}' + properties: { + mode: 'Incremental' + template: { + '$schema': 'https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#' + contentVersion: '#_moduleVersion_#.0' + resources: [] + outputs: { + telemetry: { + type: 'String' + value: 'For more information, see https://aka.ms/avm/TelemetryInfo' + } + } + } + } +} + +resource sessionPool 'Microsoft.App/sessionPools@2024-10-02-preview' = { + name: name + location: location + identity: identity + properties: { + containerType: containerType + environmentId: environmentId + customContainerTemplate: containerType == 'CustomContainer' + ? { + containers: containers + ingress: { + targetPort: targetIngressPort + } + registryCredentials: registryCredentials + } + : null + dynamicPoolConfiguration: { + cooldownPeriodInSeconds: cooldownPeriodInSeconds + executionType: 'Timed' + } + managedIdentitySettings: managedIdentitySettings + scaleConfiguration: { + maxConcurrentSessions: maxConcurrentSessions + readySessionInstances: readySessionInstances + } + sessionNetworkConfiguration: { + status: sessionNetworkStatus + } + poolManagementType: poolManagementType + } + tags: tags +} + +resource sessionPool_lock 'Microsoft.Authorization/locks@2020-05-01' = if (!empty(lock ?? {}) && lock.?kind != 'None') { + name: lock.?name ?? 'lock-${name}' + properties: { + level: lock.?kind ?? '' + notes: lock.?kind == 'CanNotDelete' + ? 'Cannot delete resource or child resources.' + : 'Cannot delete or modify the resource or child resources.' + } + scope: sessionPool +} + +resource sessionPool_roleAssignments 'Microsoft.Authorization/roleAssignments@2022-04-01' = [ + for (roleAssignment, index) in (formattedRoleAssignments ?? []): { + name: roleAssignment.?name ?? guid(sessionPool.id, roleAssignment.principalId, roleAssignment.roleDefinitionId) + properties: { + roleDefinitionId: roleAssignment.roleDefinitionId + principalId: roleAssignment.principalId + description: roleAssignment.?description + principalType: roleAssignment.?principalType + condition: roleAssignment.?condition + conditionVersion: !empty(roleAssignment.?condition) ? (roleAssignment.?conditionVersion ?? '2.0') : null // Must only be set if condtion is set + delegatedManagedIdentityResourceId: roleAssignment.?delegatedManagedIdentityResourceId + } + scope: sessionPool + } +] + +@description('The name of the session pool.') +output name string = sessionPool.name + +@description('The resource ID of the deployed session pool.') +output resourceId string = sessionPool.id + +@description('The name of the resource group in which the session pool was created.') +output resourceGroupName string = resourceGroup().name + +@description('The management endpoint of the session pool.') +output managementEndpoint string = sessionPool.properties.poolManagementEndpoint + +@description('The principal ID of the system assigned identity.') +output systemAssignedMIPrincipalId string = sessionPool.?identity.?principalId ?? '' + +// =============== // +// Definitions // +// =============== // + +@export() +@description('Optional. Custom container definition.') +type sessionContainerType = { + @description('Optional. Container start command arguments.') + args: string[]? + + @description('Optional. Container start command.') + command: string[]? + + @description('Optional. Container environment variables.') + env: sessionContainerEnvType[]? + + @description('Required. Container image tag.') + image: string + + @description('Required. Custom container name.') + name: string + + @description('Required. Container resource requirements.') + resources: sessionContainerResourceType +} + +@export() +@description('Optional. Environment variable definition for a container. Only used with custom containers.') +type sessionContainerEnvType = { + @description('Required. Environment variable name.') + name: string + + @description('Optional. Required if value is not set. Name of the Container App secret from which to pull the environment variable value.') + secretRef: string? + + @description('Optional. Required if secretRef is not set. Non-secret environment variable value.') + value: string? +} + +@export() +@description('Optional. Container resource requirements. Only used with custom containers.') +type sessionContainerResourceType = { + @description('Required. Required CPU in cores, e.g. 0.5.') + cpu: string + + @description('Required. Required memory, e.g. "1.25Gi".') + memory: string +} + +@export() +@description('Optional. Container registry credentials. Only used with custom containers.') +type sessionRegistryCredentialsType = { + @description('Optional. A Managed Identity to use to authenticate with Azure Container Registry. For user-assigned identities, use the full user-assigned identity Resource ID. For system-assigned identities, use "system".') + identity: string? + + @description('Optional. The name of the secret that contains the registry login password. Not used if identity is specified.') + passwordSecretRef: string? + + @description('Required. Container registry server.') + server: string + + @description('Required. Container registry username.') + username: string +} + +@export() +@description('Optional. Managed Identity settings for the session pool.') +type managedIdentitySettingType = { + @description('Required. The resource ID of a user-assigned managed identity that is assigned to the Session Pool, or "system" for system-assigned identity.') + identity: string + + @description('Required. Use to select the lifecycle stages of a Session Pool during which the Managed Identity should be available. Valid values: "All", "Init", "Main", "None".') + lifecycle: string +} diff --git a/avm/res/app/session-pool/main.json b/avm/res/app/session-pool/main.json new file mode 100644 index 0000000000..ceba343cb1 --- /dev/null +++ b/avm/res/app/session-pool/main.json @@ -0,0 +1,601 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "languageVersion": "2.0", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.31.92.45157", + "templateHash": "11243086501777108136" + }, + "name": "Container App Session Pool", + "description": "This module deploys a Container App Session Pool." + }, + "definitions": { + "sessionContainerType": { + "type": "object", + "properties": { + "args": { + "type": "array", + "items": { + "type": "string" + }, + "nullable": true, + "metadata": { + "description": "Optional. Container start command arguments." + } + }, + "command": { + "type": "array", + "items": { + "type": "string" + }, + "nullable": true, + "metadata": { + "description": "Optional. Container start command." + } + }, + "env": { + "type": "array", + "items": { + "$ref": "#/definitions/sessionContainerEnvType" + }, + "nullable": true, + "metadata": { + "description": "Optional. Container environment variables." + } + }, + "image": { + "type": "string", + "metadata": { + "description": "Required. Container image tag." + } + }, + "name": { + "type": "string", + "metadata": { + "description": "Required. Custom container name." + } + }, + "resources": { + "$ref": "#/definitions/sessionContainerResourceType", + "metadata": { + "description": "Required. Container resource requirements." + } + } + }, + "metadata": { + "__bicep_export!": true, + "description": "Optional. Custom container definition." + } + }, + "sessionContainerEnvType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "metadata": { + "description": "Required. Environment variable name." + } + }, + "secretRef": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Required if value is not set. Name of the Container App secret from which to pull the environment variable value." + } + }, + "value": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Required if secretRef is not set. Non-secret environment variable value." + } + } + }, + "metadata": { + "__bicep_export!": true, + "description": "Optional. Environment variable definition for a container. Only used with custom containers." + } + }, + "sessionContainerResourceType": { + "type": "object", + "properties": { + "cpu": { + "type": "string", + "metadata": { + "description": "Required. Required CPU in cores, e.g. 0.5." + } + }, + "memory": { + "type": "string", + "metadata": { + "description": "Required. Required memory, e.g. \"1.25Gi\"." + } + } + }, + "metadata": { + "__bicep_export!": true, + "description": "Optional. Container resource requirements. Only used with custom containers." + } + }, + "sessionRegistryCredentialsType": { + "type": "object", + "properties": { + "identity": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. A Managed Identity to use to authenticate with Azure Container Registry. For user-assigned identities, use the full user-assigned identity Resource ID. For system-assigned identities, use \"system\"." + } + }, + "passwordSecretRef": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The name of the secret that contains the registry login password. Not used if identity is specified." + } + }, + "server": { + "type": "string", + "metadata": { + "description": "Required. Container registry server." + } + }, + "username": { + "type": "string", + "metadata": { + "description": "Required. Container registry username." + } + } + }, + "metadata": { + "__bicep_export!": true, + "description": "Optional. Container registry credentials. Only used with custom containers." + } + }, + "managedIdentitySettingType": { + "type": "object", + "properties": { + "identity": { + "type": "string", + "metadata": { + "description": "Required. The resource ID of a user-assigned managed identity that is assigned to the Session Pool, or \"system\" for system-assigned identity." + } + }, + "lifecycle": { + "type": "string", + "metadata": { + "description": "Required. Use to select the lifecycle stages of a Session Pool during which the Managed Identity should be available. Valid values: \"All\", \"Init\", \"Main\", \"None\"." + } + } + }, + "metadata": { + "__bicep_export!": true, + "description": "Optional. Managed Identity settings for the session pool." + } + }, + "lockType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Specify the name of lock." + } + }, + "kind": { + "type": "string", + "allowedValues": [ + "CanNotDelete", + "None", + "ReadOnly" + ], + "nullable": true, + "metadata": { + "description": "Optional. Specify the type of lock." + } + } + }, + "metadata": { + "description": "An AVM-aligned type for a lock.", + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.2.1" + } + } + }, + "managedIdentityAllType": { + "type": "object", + "properties": { + "systemAssigned": { + "type": "bool", + "nullable": true, + "metadata": { + "description": "Optional. Enables system assigned managed identity on the resource." + } + }, + "userAssignedResourceIds": { + "type": "array", + "items": { + "type": "string" + }, + "nullable": true, + "metadata": { + "description": "Optional. The resource ID(s) to assign to the resource. Required if a user assigned identity is used for encryption." + } + } + }, + "metadata": { + "description": "An AVM-aligned type for a managed identity configuration. To be used if both a system-assigned & user-assigned identities are supported by the resource provider.", + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.2.1" + } + } + }, + "roleAssignmentType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The name (as GUID) of the role assignment. If not provided, a GUID will be generated." + } + }, + "roleDefinitionIdOrName": { + "type": "string", + "metadata": { + "description": "Required. The role to assign. You can provide either the display name of the role definition, the role definition GUID, or its fully qualified ID in the following format: '/providers/Microsoft.Authorization/roleDefinitions/c2f4ef07-c644-48eb-af81-4b1b4947fb11'." + } + }, + "principalId": { + "type": "string", + "metadata": { + "description": "Required. The principal ID of the principal (user/group/identity) to assign the role to." + } + }, + "principalType": { + "type": "string", + "allowedValues": [ + "Device", + "ForeignGroup", + "Group", + "ServicePrincipal", + "User" + ], + "nullable": true, + "metadata": { + "description": "Optional. The principal type of the assigned principal ID." + } + }, + "description": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The description of the role assignment." + } + }, + "condition": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The conditions on the role assignment. This limits the resources it can be assigned to. e.g.: @Resource[Microsoft.Storage/storageAccounts/blobServices/containers:ContainerName] StringEqualsIgnoreCase \"foo_storage_container\"." + } + }, + "conditionVersion": { + "type": "string", + "allowedValues": [ + "2.0" + ], + "nullable": true, + "metadata": { + "description": "Optional. Version of the condition." + } + }, + "delegatedManagedIdentityResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The Resource Id of the delegated managed identity resource." + } + } + }, + "metadata": { + "description": "An AVM-aligned type for a role assignment.", + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.2.1" + } + } + } + }, + "parameters": { + "name": { + "type": "string", + "metadata": { + "description": "Required. Name of the Container App Session Pool." + } + }, + "location": { + "type": "string", + "defaultValue": "[resourceGroup().location]", + "metadata": { + "description": "Optional. Location for all Resources." + } + }, + "containerType": { + "type": "string", + "allowedValues": [ + "PythonLTS", + "CustomContainer" + ], + "metadata": { + "description": "Required. The container type of the sessions." + } + }, + "containers": { + "type": "array", + "items": { + "$ref": "#/definitions/sessionContainerType" + }, + "nullable": true, + "metadata": { + "description": "Optional. Custom container definitions. Only required if containerType is CustomContainer." + } + }, + "targetIngressPort": { + "type": "int", + "nullable": true, + "metadata": { + "description": "Optional. Required if containerType == 'CustomContainer'. Target port in containers for traffic from ingress. Only required if containerType is CustomContainer." + } + }, + "registryCredentials": { + "$ref": "#/definitions/sessionRegistryCredentialsType", + "nullable": true, + "metadata": { + "description": "Optional. Container registry credentials. Only required if containerType is CustomContainer and the container registry requires authentication." + } + }, + "cooldownPeriodInSeconds": { + "type": "int", + "defaultValue": 300, + "metadata": { + "description": "Optional. The cooldown period of a session in seconds." + } + }, + "maxConcurrentSessions": { + "type": "int", + "defaultValue": 5, + "metadata": { + "description": "Optional. The maximum count of sessions at the same time." + } + }, + "readySessionInstances": { + "type": "int", + "nullable": true, + "metadata": { + "description": "Optional. The minimum count of ready session instances." + } + }, + "sessionNetworkStatus": { + "type": "string", + "defaultValue": "EgressDisabled", + "allowedValues": [ + "EgressEnabled", + "EgressDisabled" + ], + "metadata": { + "description": "Optional. Network status for the sessions. Defaults to EgressDisabled." + } + }, + "poolManagementType": { + "type": "string", + "defaultValue": "Dynamic", + "allowedValues": [ + "Dynamic", + "Manual" + ], + "metadata": { + "description": "Optional. The pool management type of the session pool. Defaults to Dynamic." + } + }, + "lock": { + "$ref": "#/definitions/lockType", + "nullable": true, + "metadata": { + "description": "Optional. The lock settings of the service." + } + }, + "roleAssignments": { + "type": "array", + "items": { + "$ref": "#/definitions/roleAssignmentType" + }, + "nullable": true, + "metadata": { + "description": "Optional. Array of role assignments to create." + } + }, + "managedIdentities": { + "$ref": "#/definitions/managedIdentityAllType", + "nullable": true, + "metadata": { + "description": "Optional. The managed identity definition for this resource." + } + }, + "managedIdentitySettings": { + "type": "array", + "items": { + "$ref": "#/definitions/managedIdentitySettingType" + }, + "nullable": true, + "metadata": { + "description": "Optional. Settings for a Managed Identity that is assigned to the Session pool." + } + }, + "environmentId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Resource ID of the session pool's environment." + } + }, + "tags": { + "type": "object", + "nullable": true, + "metadata": { + "description": "Optional. Tags of the Automation Account resource." + } + }, + "enableTelemetry": { + "type": "bool", + "defaultValue": true, + "metadata": { + "description": "Optional. Enable/Disable usage telemetry for module." + } + } + }, + "variables": { + "copy": [ + { + "name": "formattedRoleAssignments", + "count": "[length(coalesce(parameters('roleAssignments'), createArray()))]", + "input": "[union(coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')], createObject('roleDefinitionId', coalesce(tryGet(variables('builtInRoleNames'), coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName), if(contains(coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName, '/providers/Microsoft.Authorization/roleDefinitions/'), coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName, subscriptionResourceId('Microsoft.Authorization/roleDefinitions', coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName)))))]" + } + ], + "formattedUserAssignedIdentities": "[reduce(map(coalesce(tryGet(parameters('managedIdentities'), 'userAssignedResourceIds'), createArray()), lambda('id', createObject(format('{0}', lambdaVariables('id')), createObject()))), createObject(), lambda('cur', 'next', union(lambdaVariables('cur'), lambdaVariables('next'))))]", + "identity": "[if(not(empty(parameters('managedIdentities'))), createObject('type', if(coalesce(tryGet(parameters('managedIdentities'), 'systemAssigned'), false()), if(not(empty(coalesce(tryGet(parameters('managedIdentities'), 'userAssignedResourceIds'), createObject()))), 'SystemAssigned,UserAssigned', 'SystemAssigned'), if(not(empty(coalesce(tryGet(parameters('managedIdentities'), 'userAssignedResourceIds'), createObject()))), 'UserAssigned', 'None')), 'userAssignedIdentities', if(not(empty(variables('formattedUserAssignedIdentities'))), variables('formattedUserAssignedIdentities'), null())), null())]", + "builtInRoleNames": { + "Azure ContainerApps Session Executor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '0fb8eba5-a2bb-4abe-b1c1-49dfad359bb0')]", + "Container Apps SessionPools Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'f7669afb-68b2-44b4-9c5f-6d2a47fddda0')]", + "Container Apps SessionPools Reader": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'af61e8fc-2633-4b95-bed3-421ad6826515')]", + "Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'b24988ac-6180-42a0-ab88-20f7382dd24c')]", + "Owner": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '8e3af657-a8ff-443c-a75c-2fe8c4bcb635')]", + "Reader": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'acdd72a7-3385-48ef-bd42-f606fba81ae7')]", + "Role Based Access Control Administrator": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'f58310d9-a9f6-439a-9e8d-f62e7b41a168')]", + "User Access Administrator": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '18d7d88d-d35e-4fb5-a5c3-7773c20a72d9')]" + } + }, + "resources": { + "avmTelemetry": { + "condition": "[parameters('enableTelemetry')]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2024-03-01", + "name": "[format('46d3xbcp.res.app-sessionpool.{0}.{1}', replace('-..--..-', '.', '-'), substring(uniqueString(deployment().name, parameters('location')), 0, 4))]", + "properties": { + "mode": "Incremental", + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "#_moduleVersion_#.0", + "resources": [], + "outputs": { + "telemetry": { + "type": "String", + "value": "For more information, see https://aka.ms/avm/TelemetryInfo" + } + } + } + } + }, + "sessionPool": { + "type": "Microsoft.App/sessionPools", + "apiVersion": "2024-10-02-preview", + "name": "[parameters('name')]", + "location": "[parameters('location')]", + "identity": "[variables('identity')]", + "properties": { + "containerType": "[parameters('containerType')]", + "environmentId": "[parameters('environmentId')]", + "customContainerTemplate": "[if(equals(parameters('containerType'), 'CustomContainer'), createObject('containers', parameters('containers'), 'ingress', createObject('targetPort', parameters('targetIngressPort')), 'registryCredentials', parameters('registryCredentials')), null())]", + "dynamicPoolConfiguration": { + "cooldownPeriodInSeconds": "[parameters('cooldownPeriodInSeconds')]", + "executionType": "Timed" + }, + "managedIdentitySettings": "[parameters('managedIdentitySettings')]", + "scaleConfiguration": { + "maxConcurrentSessions": "[parameters('maxConcurrentSessions')]", + "readySessionInstances": "[parameters('readySessionInstances')]" + }, + "sessionNetworkConfiguration": { + "status": "[parameters('sessionNetworkStatus')]" + }, + "poolManagementType": "[parameters('poolManagementType')]" + }, + "tags": "[parameters('tags')]" + }, + "sessionPool_lock": { + "condition": "[and(not(empty(coalesce(parameters('lock'), createObject()))), not(equals(tryGet(parameters('lock'), 'kind'), 'None')))]", + "type": "Microsoft.Authorization/locks", + "apiVersion": "2020-05-01", + "scope": "[format('Microsoft.App/sessionPools/{0}', parameters('name'))]", + "name": "[coalesce(tryGet(parameters('lock'), 'name'), format('lock-{0}', parameters('name')))]", + "properties": { + "level": "[coalesce(tryGet(parameters('lock'), 'kind'), '')]", + "notes": "[if(equals(tryGet(parameters('lock'), 'kind'), 'CanNotDelete'), 'Cannot delete resource or child resources.', 'Cannot delete or modify the resource or child resources.')]" + }, + "dependsOn": [ + "sessionPool" + ] + }, + "sessionPool_roleAssignments": { + "copy": { + "name": "sessionPool_roleAssignments", + "count": "[length(coalesce(variables('formattedRoleAssignments'), createArray()))]" + }, + "type": "Microsoft.Authorization/roleAssignments", + "apiVersion": "2022-04-01", + "scope": "[format('Microsoft.App/sessionPools/{0}', parameters('name'))]", + "name": "[coalesce(tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'name'), guid(resourceId('Microsoft.App/sessionPools', parameters('name')), coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].principalId, coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].roleDefinitionId))]", + "properties": { + "roleDefinitionId": "[coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].roleDefinitionId]", + "principalId": "[coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].principalId]", + "description": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'description')]", + "principalType": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'principalType')]", + "condition": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'condition')]", + "conditionVersion": "[if(not(empty(tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'condition'))), coalesce(tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'conditionVersion'), '2.0'), null())]", + "delegatedManagedIdentityResourceId": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'delegatedManagedIdentityResourceId')]" + }, + "dependsOn": [ + "sessionPool" + ] + } + }, + "outputs": { + "name": { + "type": "string", + "metadata": { + "description": "The name of the session pool." + }, + "value": "[parameters('name')]" + }, + "resourceId": { + "type": "string", + "metadata": { + "description": "The resource ID of the deployed session pool." + }, + "value": "[resourceId('Microsoft.App/sessionPools', parameters('name'))]" + }, + "resourceGroupName": { + "type": "string", + "metadata": { + "description": "The name of the resource group in which the session pool was created." + }, + "value": "[resourceGroup().name]" + }, + "managementEndpoint": { + "type": "string", + "metadata": { + "description": "The management endpoint of the session pool." + }, + "value": "[reference('sessionPool').poolManagementEndpoint]" + }, + "systemAssignedMIPrincipalId": { + "type": "string", + "metadata": { + "description": "The principal ID of the system assigned identity." + }, + "value": "[coalesce(tryGet(tryGet(reference('sessionPool', '2024-10-02-preview', 'full'), 'identity'), 'principalId'), '')]" + } + } +} \ No newline at end of file diff --git a/avm/res/app/session-pool/tests/e2e/defaults/main.test.bicep b/avm/res/app/session-pool/tests/e2e/defaults/main.test.bicep new file mode 100644 index 0000000000..3c9d108665 --- /dev/null +++ b/avm/res/app/session-pool/tests/e2e/defaults/main.test.bicep @@ -0,0 +1,50 @@ +targetScope = 'subscription' + +metadata name = 'Using only defaults' +metadata description = 'This instance deploys the module with the minimum set of required parameters.' + +// ========== // +// Parameters // +// ========== // + +@description('Optional. The name of the resource group to deploy for testing purposes.') +@maxLength(90) +param resourceGroupName string = 'dep-${namePrefix}-app.session-pool-${serviceShort}-rg' + +@description('Optional. The location to deploy resources to.') +param resourceLocation string = deployment().location + +@description('Optional. A short identifier for the kind of deployment. Should be kept short to not run into resource-name length-constraints.') +param serviceShort string = 'aspmin' + +@description('Optional. A token to inject into the name of each resource.') +param namePrefix string = '#_namePrefix_#' + +// ============ // +// Dependencies // +// ============ // + +// ================= // +// General resources // +// ================= // +resource resourceGroup 'Microsoft.Resources/resourceGroups@2021-04-01' = { + name: resourceGroupName + location: resourceLocation +} + +// ============== // +// Test Execution // +// ============== // + +@batchSize(1) +module testDeployment '../../../main.bicep' = [ + for iteration in ['init', 'idem']: { + scope: resourceGroup + name: '${uniqueString(deployment().name, resourceLocation)}-test-${serviceShort}-${iteration}' + params: { + name: '${namePrefix}${serviceShort}001' + location: resourceLocation + containerType: 'PythonLTS' + } + } +] diff --git a/avm/res/app/session-pool/tests/e2e/max/dependencies.bicep b/avm/res/app/session-pool/tests/e2e/max/dependencies.bicep new file mode 100644 index 0000000000..a73969ae10 --- /dev/null +++ b/avm/res/app/session-pool/tests/e2e/max/dependencies.bicep @@ -0,0 +1,16 @@ +@description('Optional. The location to deploy resources to.') +param location string = resourceGroup().location + +@description('Required. The name of the Managed Identity to be created.') +param managedIdentityName string + +resource managedIdentity 'Microsoft.ManagedIdentity/userAssignedIdentities@2023-07-31-preview' = { + name: managedIdentityName + location: location +} + +@description('The resource ID of the created Managed Identity.') +output managedIdentityResourceId string = managedIdentity.id + +@description('The principal ID of the created Managed Identity.') +output managedIdentityPrincipalId string = managedIdentity.properties.principalId diff --git a/avm/res/app/session-pool/tests/e2e/max/main.test.bicep b/avm/res/app/session-pool/tests/e2e/max/main.test.bicep new file mode 100644 index 0000000000..1d5f073ab3 --- /dev/null +++ b/avm/res/app/session-pool/tests/e2e/max/main.test.bicep @@ -0,0 +1,84 @@ +targetScope = 'subscription' + +metadata name = 'Using large parameter set' +metadata description = 'This instance deploys the module with most of its features enabled.' + +// ========== // +// Parameters // +// ========== // + +@description('Optional. The name of the resource group to deploy for testing purposes.') +@maxLength(90) +param resourceGroupName string = 'dep-${namePrefix}-app.session-pool-${serviceShort}-rg' + +@description('Optional. The location to deploy resources to.') +param resourceLocation string = deployment().location + +@description('Optional. A short identifier for the kind of deployment. Should be kept short to not run into resource-name length-constraints.') +param serviceShort string = 'aspmax' + +@description('Optional. A token to inject into the name of each resource.') +param namePrefix string = '#_namePrefix_#' + +// ============ // +// Dependencies // +// ============ // + +// ================= // +// General resources // +// ================= // +resource resourceGroup 'Microsoft.Resources/resourceGroups@2021-04-01' = { + name: resourceGroupName + location: resourceLocation +} + +module nestedDependencies 'dependencies.bicep' = { + scope: resourceGroup + name: '${uniqueString(deployment().name, resourceLocation)}-nestedDependencies' + params: { + location: resourceLocation + managedIdentityName: 'dep-${namePrefix}-mi-${serviceShort}' + } +} + +// ============== // +// Test Execution // +// ============== // + +@batchSize(1) +module testDeployment '../../../main.bicep' = [ + for iteration in ['init', 'idem']: { + scope: resourceGroup + name: '${uniqueString(deployment().name, resourceLocation)}-test-${serviceShort}-${iteration}' + params: { + name: '${namePrefix}${serviceShort}001' + location: resourceLocation + containerType: 'PythonLTS' + tags: { + resourceType: 'Session Pool' + } + cooldownPeriodInSeconds: 350 + maxConcurrentSessions: 6 + readySessionInstances: 1 + sessionNetworkStatus: 'EgressDisabled' + poolManagementType: 'Dynamic' + managedIdentitySettings: [ + { + identity: nestedDependencies.outputs.managedIdentityResourceId + lifecycle: 'Main' + } + ] + lock: { + kind: 'CanNotDelete' + name: 'myCustomLockName' + } + roleAssignments: [ + { + principalId: nestedDependencies.outputs.managedIdentityPrincipalId + roleDefinitionIdOrName: 'Azure ContainerApps Session Executor' + principalType: 'ServicePrincipal' + } + ] + } + } +] diff --git a/avm/res/app/session-pool/tests/e2e/waf-aligned/main.test.bicep b/avm/res/app/session-pool/tests/e2e/waf-aligned/main.test.bicep new file mode 100644 index 0000000000..925c44814d --- /dev/null +++ b/avm/res/app/session-pool/tests/e2e/waf-aligned/main.test.bicep @@ -0,0 +1,54 @@ +targetScope = 'subscription' + +metadata name = 'WAF-aligned' +metadata description = 'This instance deploys the module in alignment with the best-practices of the Azure Well-Architected Framework.' + +// ========== // +// Parameters // +// ========== // + +@description('Optional. The name of the resource group to deploy for testing purposes.') +@maxLength(90) +param resourceGroupName string = 'dep-${namePrefix}-app.session-pool-${serviceShort}-rg' + +@description('Optional. The location to deploy resources to.') +param resourceLocation string = deployment().location + +@description('Optional. A short identifier for the kind of deployment. Should be kept short to not run into resource-name length-constraints.') +param serviceShort string = 'aspwaf' + +@description('Optional. A token to inject into the name of each resource.') +param namePrefix string = '#_namePrefix_#' + +// ============ // +// Dependencies // +// ============ // + +// ================= // +// General resources // +// ================= // +resource resourceGroup 'Microsoft.Resources/resourceGroups@2021-04-01' = { + name: resourceGroupName + location: resourceLocation +} + +// ============== // +// Test Execution // +// ============== // + +@batchSize(1) +module testDeployment '../../../main.bicep' = [ + for iteration in ['init', 'idem']: { + scope: resourceGroup + name: '${uniqueString(deployment().name, resourceLocation)}-test-${serviceShort}-${iteration}' + params: { + name: '${namePrefix}${serviceShort}001' + location: resourceLocation + containerType: 'PythonLTS' + tags: { + resourceType: 'Session Pool' + } + sessionNetworkStatus: 'EgressDisabled' + } + } +] diff --git a/avm/res/app/session-pool/version.json b/avm/res/app/session-pool/version.json new file mode 100644 index 0000000000..7fa401bdf7 --- /dev/null +++ b/avm/res/app/session-pool/version.json @@ -0,0 +1,7 @@ +{ + "$schema": "https://aka.ms/bicep-registry-module-version-file-schema#", + "version": "0.1", + "pathFilters": [ + "./main.json" + ] +} From b0c5affa1c9e3daa1232441c337dc102df7d09b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20H=C3=A9zser?= <rene@hezser.de> Date: Thu, 13 Feb 2025 09:14:56 +0100 Subject: [PATCH 4/7] fix: Accelerated Network `res/compute/gallery` (#4432) ## Description Fixes accelerated networking property for `res/compute/gallery` module, updates RP references and adds the new property allowUpdateImage. Closes #4374 references #4349 ## Pipeline Reference <!-- Insert your Pipeline Status Badge below --> | Pipeline | | -------- | | [![avm.res.compute.gallery](https://github.com/ReneHezser/bicep-registry-modules/actions/workflows/avm.res.compute.gallery.yml/badge.svg)](https://github.com/ReneHezser/bicep-registry-modules/actions/workflows/avm.res.compute.gallery.yml) | ## Type of Change - [ ] Update to CI Environment or utilities (Non-module affecting changes) - [x] Azure Verified Module updates: - [ ] Bugfix containing backwards-compatible bug fixes, and I have NOT bumped the MAJOR or MINOR version in `version.json`: - [x] Someone has opened a bug report issue, and I have included "Closes #{bug_report_issue_number}" in the PR description. - [ ] The bug was found by the module author, and no one has opened an issue to report it yet. - [x] Feature update backwards compatible feature updates, and I have bumped the MINOR version in `version.json`. - [ ] Breaking changes and I have bumped the MAJOR version in `version.json`. - [ ] Update to documentation ## Checklist - [x] I'm sure there are no other open Pull Requests for the same update/change - [x] I have run `Set-AVMModule` locally to generate the supporting module files. - [x] My corresponding pipelines / checks run clean and green without any errors or warnings --- avm/res/compute/gallery/README.md | 59 +++++++++++++- avm/res/compute/gallery/application/README.md | 2 +- .../compute/gallery/application/main.bicep | 4 +- avm/res/compute/gallery/application/main.json | 10 +-- avm/res/compute/gallery/image/README.md | 27 ++++++- avm/res/compute/gallery/image/main.bicep | 35 ++++++--- avm/res/compute/gallery/image/main.json | 34 ++++++-- avm/res/compute/gallery/main.bicep | 10 ++- avm/res/compute/gallery/main.json | 77 +++++++++++++++---- .../gallery/tests/e2e/max/main.test.bicep | 8 +- avm/res/compute/gallery/version.json | 2 +- 11 files changed, 218 insertions(+), 50 deletions(-) diff --git a/avm/res/compute/gallery/README.md b/avm/res/compute/gallery/README.md index d6a31db48f..9427470e22 100644 --- a/avm/res/compute/gallery/README.md +++ b/avm/res/compute/gallery/README.md @@ -9,6 +9,7 @@ This module deploys an Azure Compute Gallery (formerly known as Shared Image Gal - [Parameters](#Parameters) - [Outputs](#Outputs) - [Cross-referenced modules](#Cross-referenced-modules) +- [Notes](#Notes) - [Data Collection](#Data-Collection) ## Resource Types @@ -17,9 +18,9 @@ This module deploys an Azure Compute Gallery (formerly known as Shared Image Gal | :-- | :-- | | `Microsoft.Authorization/locks` | [2020-05-01](https://learn.microsoft.com/en-us/azure/templates/Microsoft.Authorization/2020-05-01/locks) | | `Microsoft.Authorization/roleAssignments` | [2022-04-01](https://learn.microsoft.com/en-us/azure/templates/Microsoft.Authorization/2022-04-01/roleAssignments) | -| `Microsoft.Compute/galleries` | [2023-07-03](https://learn.microsoft.com/en-us/azure/templates/Microsoft.Compute/2023-07-03/galleries) | -| `Microsoft.Compute/galleries/applications` | [2022-03-03](https://learn.microsoft.com/en-us/azure/templates/Microsoft.Compute/2022-03-03/galleries/applications) | -| `Microsoft.Compute/galleries/images` | [2023-07-03](https://learn.microsoft.com/en-us/azure/templates/Microsoft.Compute/2023-07-03/galleries/images) | +| `Microsoft.Compute/galleries` | [2024-03-03](https://learn.microsoft.com/en-us/azure/templates/Microsoft.Compute/2024-03-03/galleries) | +| `Microsoft.Compute/galleries/applications` | [2024-03-03](https://learn.microsoft.com/en-us/azure/templates/Microsoft.Compute/2024-03-03/galleries/applications) | +| `Microsoft.Compute/galleries/images` | [2024-03-03](https://learn.microsoft.com/en-us/azure/templates/Microsoft.Compute/2024-03-03/galleries/images) | ## Usage examples @@ -144,6 +145,7 @@ module gallery 'br/public:avm/res/compute/gallery:<version>' = { description: 'This is a test deployment.' images: [ { + allowUpdateImage: true architecture: 'x64' description: 'testDescription' endOfLife: '2033-01-01' @@ -169,6 +171,7 @@ module gallery 'br/public:avm/res/compute/gallery:<version>' = { releaseNoteUri: 'https://testReleaseNoteUri.com' } { + allowUpdateImage: false hyperVGeneration: 'V2' identifier: { offer: 'WindowsServer' @@ -215,6 +218,7 @@ module gallery 'br/public:avm/res/compute/gallery:<version>' = { } } { + diskControllerType: 'SCSI' hyperVGeneration: 'V2' identifier: { offer: '0001-com-ubuntu-minimal-focal' @@ -235,6 +239,7 @@ module gallery 'br/public:avm/res/compute/gallery:<version>' = { } } { + diskControllerType: 'SCSI, NVMe' hyperVGeneration: 'V2' identifier: { offer: '0001-com-ubuntu-minimal-focal' @@ -377,6 +382,7 @@ module gallery 'br/public:avm/res/compute/gallery:<version>' = { "images": { "value": [ { + "allowUpdateImage": true, "architecture": "x64", "description": "testDescription", "endOfLife": "2033-01-01", @@ -402,6 +408,7 @@ module gallery 'br/public:avm/res/compute/gallery:<version>' = { "releaseNoteUri": "https://testReleaseNoteUri.com" }, { + "allowUpdateImage": false, "hyperVGeneration": "V2", "identifier": { "offer": "WindowsServer", @@ -448,6 +455,7 @@ module gallery 'br/public:avm/res/compute/gallery:<version>' = { } }, { + "diskControllerType": "SCSI", "hyperVGeneration": "V2", "identifier": { "offer": "0001-com-ubuntu-minimal-focal", @@ -468,6 +476,7 @@ module gallery 'br/public:avm/res/compute/gallery:<version>' = { } }, { + "diskControllerType": "SCSI, NVMe", "hyperVGeneration": "V2", "identifier": { "offer": "0001-com-ubuntu-minimal-focal", @@ -610,6 +619,7 @@ param applications = [ param description = 'This is a test deployment.' param images = [ { + allowUpdateImage: true architecture: 'x64' description: 'testDescription' endOfLife: '2033-01-01' @@ -635,6 +645,7 @@ param images = [ releaseNoteUri: 'https://testReleaseNoteUri.com' } { + allowUpdateImage: false hyperVGeneration: 'V2' identifier: { offer: 'WindowsServer' @@ -681,6 +692,7 @@ param images = [ } } { + diskControllerType: 'SCSI' hyperVGeneration: 'V2' identifier: { offer: '0001-com-ubuntu-minimal-focal' @@ -701,6 +713,7 @@ param images = [ } } { + diskControllerType: 'SCSI, NVMe' hyperVGeneration: 'V2' identifier: { offer: '0001-com-ubuntu-minimal-focal' @@ -1296,8 +1309,10 @@ Images to create. | Parameter | Type | Description | | :-- | :-- | :-- | +| [`allowUpdateImage`](#parameter-imagesallowupdateimage) | bool | Must be set to true if the gallery image features are being updated. | | [`architecture`](#parameter-imagesarchitecture) | string | The architecture of the image. Applicable to OS disks only. | | [`description`](#parameter-imagesdescription) | string | The description of this gallery image definition resource. This property is updatable. | +| [`diskControllerType`](#parameter-imagesdiskcontrollertype) | string | The disk controllers that an OS disk supports. | | [`endOfLife`](#parameter-imagesendoflife) | string | The end of life date of the gallery image definition. This property can be used for decommissioning purposes. This property is updatable. | | [`eula`](#parameter-imageseula) | string | The Eula agreement for the gallery image definition. | | [`excludedDiskTypes`](#parameter-imagesexcludeddisktypes) | array | Describes the disallowed disk types. | @@ -1382,6 +1397,13 @@ This property allows you to specify the type of the OS that is included in the d ] ``` +### Parameter: `images.allowUpdateImage` + +Must be set to true if the gallery image features are being updated. + +- Required: No +- Type: bool + ### Parameter: `images.architecture` The architecture of the image. Applicable to OS disks only. @@ -1403,6 +1425,21 @@ The description of this gallery image definition resource. This property is upda - Required: No - Type: string +### Parameter: `images.diskControllerType` + +The disk controllers that an OS disk supports. + +- Required: No +- Type: string +- Allowed: + ```Bicep + [ + 'NVMe, SCSI' + 'SCSI' + 'SCSI, NVMe' + ] + ``` + ### Parameter: `images.endOfLife` The end of life date of the gallery image definition. This property can be used for decommissioning purposes. This property is updatable. @@ -1772,6 +1809,22 @@ This section gives you an overview of all local-referenced module files (i.e., o | :-- | :-- | | `br/public:avm/utl/types/avm-common-types:0.3.0` | Remote reference | +## Notes + +Currently it is not possible to redeploy the `image.diskControllerType` property with a value of `NVMe, SCSI`. The initial deployment is working, but other deployments will result in an error. + +```json +"details": [ + { + "code": "PropertyChangeNotAllowed", + "target": "DiskControllerTypes", + "message": "Changing property 'DiskControllerTypes' is not allowed." + } + ] +``` + +Once this bug has been resolved, the max test will be updated to deploy an image with the property value. + ## Data Collection The software may collect information about you and your use of the software and send it to Microsoft. Microsoft may use this information to provide services and improve our products and services. You may turn off the telemetry as described in the [repository](https://aka.ms/avm/telemetry). There are also some features in the software that may enable you and Microsoft to collect data from users of your applications. If you use these features, you must comply with applicable law, including providing appropriate notices to users of your applications together with a copy of Microsoft’s privacy statement. Our privacy statement is located at <https://go.microsoft.com/fwlink/?LinkID=824704>. You can learn more about data collection and use in the help documentation and our privacy statement. Your use of the software operates as your consent to these practices. diff --git a/avm/res/compute/gallery/application/README.md b/avm/res/compute/gallery/application/README.md index 9400c80cf4..80c590cb8a 100644 --- a/avm/res/compute/gallery/application/README.md +++ b/avm/res/compute/gallery/application/README.md @@ -15,7 +15,7 @@ This module deploys an Azure Compute Gallery Application. | Resource Type | API Version | | :-- | :-- | | `Microsoft.Authorization/roleAssignments` | [2022-04-01](https://learn.microsoft.com/en-us/azure/templates/Microsoft.Authorization/2022-04-01/roleAssignments) | -| `Microsoft.Compute/galleries/applications` | [2022-03-03](https://learn.microsoft.com/en-us/azure/templates/Microsoft.Compute/2022-03-03/galleries/applications) | +| `Microsoft.Compute/galleries/applications` | [2024-03-03](https://learn.microsoft.com/en-us/azure/templates/Microsoft.Compute/2024-03-03/galleries/applications) | ## Parameters diff --git a/avm/res/compute/gallery/application/main.bicep b/avm/res/compute/gallery/application/main.bicep index 65a1418585..b8cddc8517 100644 --- a/avm/res/compute/gallery/application/main.bicep +++ b/avm/res/compute/gallery/application/main.bicep @@ -72,11 +72,11 @@ var formattedRoleAssignments = [ }) ] -resource gallery 'Microsoft.Compute/galleries@2022-03-03' existing = { +resource gallery 'Microsoft.Compute/galleries@2024-03-03' existing = { name: galleryName } -resource application 'Microsoft.Compute/galleries/applications@2022-03-03' = { +resource application 'Microsoft.Compute/galleries/applications@2024-03-03' = { name: name parent: gallery location: location diff --git a/avm/res/compute/gallery/application/main.json b/avm/res/compute/gallery/application/main.json index ca08bbc5b4..389026f8c7 100644 --- a/avm/res/compute/gallery/application/main.json +++ b/avm/res/compute/gallery/application/main.json @@ -5,8 +5,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.32.4.45862", - "templateHash": "7761447372947910331" + "version": "0.33.93.31351", + "templateHash": "8421640590183520512" }, "name": "Compute Galleries Applications", "description": "This module deploys an Azure Compute Gallery Application." @@ -281,12 +281,12 @@ "gallery": { "existing": true, "type": "Microsoft.Compute/galleries", - "apiVersion": "2022-03-03", + "apiVersion": "2024-03-03", "name": "[parameters('galleryName')]" }, "application": { "type": "Microsoft.Compute/galleries/applications", - "apiVersion": "2022-03-03", + "apiVersion": "2024-03-03", "name": "[format('{0}/{1}', parameters('galleryName'), parameters('name'))]", "location": "[parameters('location')]", "tags": "[parameters('tags')]", @@ -350,7 +350,7 @@ "metadata": { "description": "The location the resource was deployed into." }, - "value": "[reference('application', '2022-03-03', 'full').location]" + "value": "[reference('application', '2024-03-03', 'full').location]" } } } \ No newline at end of file diff --git a/avm/res/compute/gallery/image/README.md b/avm/res/compute/gallery/image/README.md index 0dbff55826..a5cf55dbb0 100644 --- a/avm/res/compute/gallery/image/README.md +++ b/avm/res/compute/gallery/image/README.md @@ -13,7 +13,7 @@ This module deploys an Azure Compute Gallery Image Definition. | Resource Type | API Version | | :-- | :-- | | `Microsoft.Authorization/roleAssignments` | [2022-04-01](https://learn.microsoft.com/en-us/azure/templates/Microsoft.Authorization/2022-04-01/roleAssignments) | -| `Microsoft.Compute/galleries/images` | [2023-07-03](https://learn.microsoft.com/en-us/azure/templates/Microsoft.Compute/2023-07-03/galleries/images) | +| `Microsoft.Compute/galleries/images` | [2024-03-03](https://learn.microsoft.com/en-us/azure/templates/Microsoft.Compute/2024-03-03/galleries/images) | ## Parameters @@ -36,9 +36,11 @@ This module deploys an Azure Compute Gallery Image Definition. | Parameter | Type | Description | | :-- | :-- | :-- | +| [`allowUpdateImage`](#parameter-allowupdateimage) | bool | Must be set to true if the gallery image features are being updated. | | [`architecture`](#parameter-architecture) | string | The architecture of the image. Applicable to OS disks only. | | [`description`](#parameter-description) | string | The description of this gallery image definition resource. This property is updatable. | | [`disallowed`](#parameter-disallowed) | object | Describes the disallowed disk types. | +| [`diskControllerType`](#parameter-diskcontrollertype) | string | The disk controllers that an OS disk supports. | | [`endOfLifeDate`](#parameter-endoflifedate) | string | The end of life date of the gallery image definition. This property can be used for decommissioning purposes. This property is updatable. | | [`eula`](#parameter-eula) | string | The Eula agreement for the gallery image definition. | | [`hyperVGeneration`](#parameter-hypervgeneration) | string | The hypervisor generation of the Virtual Machine. If this value is not specified, then it is determined by the securityType parameter. If the securityType parameter is specified, then the value of hyperVGeneration will be V2, else V1. | @@ -132,6 +134,13 @@ The name of the parent Azure Shared Image Gallery. Required if the template is u - Required: Yes - Type: string +### Parameter: `allowUpdateImage` + +Must be set to true if the gallery image features are being updated. + +- Required: No +- Type: bool + ### Parameter: `architecture` The architecture of the image. Applicable to OS disks only. @@ -179,6 +188,21 @@ A list of disk types. ] ``` +### Parameter: `diskControllerType` + +The disk controllers that an OS disk supports. + +- Required: No +- Type: string +- Allowed: + ```Bicep + [ + 'NVMe, SCSI' + 'SCSI' + 'SCSI, NVMe' + ] + ``` + ### Parameter: `endOfLifeDate` The end of life date of the gallery image definition. This property can be used for decommissioning purposes. This property is updatable. @@ -213,7 +237,6 @@ Specify if the image supports accelerated networking. - Required: No - Type: bool -- Default: `True` ### Parameter: `isHibernateSupported` diff --git a/avm/res/compute/gallery/image/main.bicep b/avm/res/compute/gallery/image/main.bicep index c09179fbd7..ae277e2cf0 100644 --- a/avm/res/compute/gallery/image/main.bicep +++ b/avm/res/compute/gallery/image/main.bicep @@ -47,11 +47,14 @@ param securityType ( | 'ConfidentialVMSupported')? @sys.description('Optional. Specify if the image supports accelerated networking.') -param isAcceleratedNetworkSupported bool = true +param isAcceleratedNetworkSupported bool? @sys.description('Optional. Specifiy if the image supports hibernation.') param isHibernateSupported bool? +@sys.description('Optional. Must be set to true if the gallery image features are being updated.') +param allowUpdateImage bool? + @sys.description('Optional. The architecture of the image. Applicable to OS disks only.') param architecture ('x64' | 'Arm64')? @@ -70,6 +73,9 @@ param eula string? @sys.description('Optional. The hypervisor generation of the Virtual Machine. If this value is not specified, then it is determined by the securityType parameter. If the securityType parameter is specified, then the value of hyperVGeneration will be V2, else V1.') param hyperVGeneration ('V1' | 'V2')? +@sys.description('Optional. The disk controllers that an OS disk supports.') +param diskControllerType ('SCSI' | 'SCSI, NVMe' | 'NVMe, SCSI')? + @sys.description('Optional. Array of role assignments to create.') param roleAssignments roleAssignmentType @@ -113,16 +119,17 @@ var formattedRoleAssignments = [ }) ] -resource gallery 'Microsoft.Compute/galleries@2023-07-03' existing = { +resource gallery 'Microsoft.Compute/galleries@2024-03-03' existing = { name: galleryName } -resource image 'Microsoft.Compute/galleries/images@2023-07-03' = { +resource image 'Microsoft.Compute/galleries/images@2024-03-03' = { name: name parent: gallery location: location tags: tags properties: { + allowUpdateImage: allowUpdateImage != null ? allowUpdateImage : null architecture: architecture description: description disallowed: { @@ -131,12 +138,14 @@ resource image 'Microsoft.Compute/galleries/images@2023-07-03' = { endOfLifeDate: endOfLifeDate eula: eula features: union( - [ - { - name: 'IsAcceleratedNetworkSupported' - value: '${isAcceleratedNetworkSupported}' - } - ], + (isAcceleratedNetworkSupported != null // Accelerated network is not set by default and must not be set for unsupported skus + ? [ + { + name: 'IsAcceleratedNetworkSupported' + value: '${isAcceleratedNetworkSupported}' + } + ] + : []), (securityType != null && securityType != 'Standard' // Standard is the default and is not set ? [ { @@ -152,6 +161,14 @@ resource image 'Microsoft.Compute/galleries/images@2023-07-03' = { value: '${isHibernateSupported}' } ] + : []), + (diskControllerType != null + ? [ + { + name: 'DiskControllerTypes' + value: '${diskControllerType}' + } + ] : []) ) hyperVGeneration: hyperVGeneration ?? (!empty(securityType ?? '') ? 'V2' : 'V1') diff --git a/avm/res/compute/gallery/image/main.json b/avm/res/compute/gallery/image/main.json index 62d581baa5..647150763c 100644 --- a/avm/res/compute/gallery/image/main.json +++ b/avm/res/compute/gallery/image/main.json @@ -5,8 +5,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.32.4.45862", - "templateHash": "13858460997059168010" + "version": "0.33.93.31351", + "templateHash": "5191633407919649272" }, "name": "Compute Galleries Image Definitions", "description": "This module deploys an Azure Compute Gallery Image Definition." @@ -285,7 +285,7 @@ }, "isAcceleratedNetworkSupported": { "type": "bool", - "defaultValue": true, + "nullable": true, "metadata": { "description": "Optional. Specify if the image supports accelerated networking." } @@ -297,6 +297,13 @@ "description": "Optional. Specifiy if the image supports hibernation." } }, + "allowUpdateImage": { + "type": "bool", + "nullable": true, + "metadata": { + "description": "Optional. Must be set to true if the gallery image features are being updated." + } + }, "architecture": { "type": "string", "allowedValues": [ @@ -347,6 +354,18 @@ "description": "Optional. The hypervisor generation of the Virtual Machine. If this value is not specified, then it is determined by the securityType parameter. If the securityType parameter is specified, then the value of hyperVGeneration will be V2, else V1." } }, + "diskControllerType": { + "type": "string", + "allowedValues": [ + "NVMe, SCSI", + "SCSI", + "SCSI, NVMe" + ], + "nullable": true, + "metadata": { + "description": "Optional. The disk controllers that an OS disk supports." + } + }, "roleAssignments": { "$ref": "#/definitions/roleAssignmentType", "metadata": { @@ -383,16 +402,17 @@ "gallery": { "existing": true, "type": "Microsoft.Compute/galleries", - "apiVersion": "2023-07-03", + "apiVersion": "2024-03-03", "name": "[parameters('galleryName')]" }, "image": { "type": "Microsoft.Compute/galleries/images", - "apiVersion": "2023-07-03", + "apiVersion": "2024-03-03", "name": "[format('{0}/{1}', parameters('galleryName'), parameters('name'))]", "location": "[parameters('location')]", "tags": "[parameters('tags')]", "properties": { + "allowUpdateImage": "[if(not(equals(parameters('allowUpdateImage'), null())), parameters('allowUpdateImage'), null())]", "architecture": "[parameters('architecture')]", "description": "[parameters('description')]", "disallowed": { @@ -400,7 +420,7 @@ }, "endOfLifeDate": "[parameters('endOfLifeDate')]", "eula": "[parameters('eula')]", - "features": "[union(createArray(createObject('name', 'IsAcceleratedNetworkSupported', 'value', format('{0}', parameters('isAcceleratedNetworkSupported')))), if(and(not(equals(parameters('securityType'), null())), not(equals(parameters('securityType'), 'Standard'))), createArray(createObject('name', 'SecurityType', 'value', format('{0}', parameters('securityType')))), createArray()), if(not(equals(parameters('isHibernateSupported'), null())), createArray(createObject('name', 'IsHibernateSupported', 'value', format('{0}', parameters('isHibernateSupported')))), createArray()))]", + "features": "[union(if(not(equals(parameters('isAcceleratedNetworkSupported'), null())), createArray(createObject('name', 'IsAcceleratedNetworkSupported', 'value', format('{0}', parameters('isAcceleratedNetworkSupported')))), createArray()), if(and(not(equals(parameters('securityType'), null())), not(equals(parameters('securityType'), 'Standard'))), createArray(createObject('name', 'SecurityType', 'value', format('{0}', parameters('securityType')))), createArray()), if(not(equals(parameters('isHibernateSupported'), null())), createArray(createObject('name', 'IsHibernateSupported', 'value', format('{0}', parameters('isHibernateSupported')))), createArray()), if(not(equals(parameters('diskControllerType'), null())), createArray(createObject('name', 'DiskControllerTypes', 'value', format('{0}', parameters('diskControllerType')))), createArray()))]", "hyperVGeneration": "[coalesce(parameters('hyperVGeneration'), if(not(empty(coalesce(parameters('securityType'), ''))), 'V2', 'V1'))]", "identifier": { "publisher": "[parameters('identifier').publisher]", @@ -468,7 +488,7 @@ "metadata": { "description": "The location the resource was deployed into." }, - "value": "[reference('image', '2023-07-03', 'full').location]" + "value": "[reference('image', '2024-03-03', 'full').location]" } } } \ No newline at end of file diff --git a/avm/res/compute/gallery/main.bicep b/avm/res/compute/gallery/main.bicep index 4a85a01615..0902a56202 100644 --- a/avm/res/compute/gallery/main.bicep +++ b/avm/res/compute/gallery/main.bicep @@ -101,7 +101,7 @@ resource avmTelemetry 'Microsoft.Resources/deployments@2024-03-01' = if (enableT } } -resource gallery 'Microsoft.Compute/galleries@2023-07-03' = { +resource gallery 'Microsoft.Compute/galleries@2024-03-03' = { name: name location: location tags: tags @@ -168,6 +168,7 @@ module galleries_images 'image/main.bicep' = [ location: image.?location ?? location galleryName: gallery.name description: image.?description + allowUpdateImage: image.?allowUpdateImage osType: image.osType osState: image.osState identifier: image.identifier @@ -177,6 +178,7 @@ module galleries_images 'image/main.bicep' = [ securityType: image.?securityType isAcceleratedNetworkSupported: image.?isAcceleratedNetworkSupported isHibernateSupported: image.?isHibernateSupported + diskControllerType: image.?diskControllerType architecture: image.?architecture eula: image.?eula privacyStatementUri: image.?privacyStatementUri @@ -226,6 +228,9 @@ type imageType = { @sys.description('Optional. The description of this gallery image definition resource. This property is updatable.') description: string? + @sys.description('Optional. Must be set to true if the gallery image features are being updated.') + allowUpdateImage: bool? + @sys.description('Required. This property allows you to specify the type of the OS that is included in the disk when creating a VM from a managed image.') osType: ('Linux' | 'Windows') @@ -259,6 +264,9 @@ type imageType = { @sys.description('Optional. Specify if the image supports hibernation.') isHibernateSupported: bool? + @sys.description('Optional. The disk controllers that an OS disk supports.') + diskControllerType: ('SCSI' | 'SCSI, NVMe' | 'NVMe, SCSI')? + @sys.description('Optional. The architecture of the image. Applicable to OS disks only.') architecture: ('x64' | 'Arm64')? diff --git a/avm/res/compute/gallery/main.json b/avm/res/compute/gallery/main.json index 87eed5748c..cbcc15f39d 100644 --- a/avm/res/compute/gallery/main.json +++ b/avm/res/compute/gallery/main.json @@ -5,8 +5,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.32.4.45862", - "templateHash": "8716597573060065028" + "version": "0.33.93.31351", + "templateHash": "13276352345178978927" }, "name": "Azure Compute Galleries", "description": "This module deploys an Azure Compute Gallery (formerly known as Shared Image Gallery)." @@ -30,6 +30,13 @@ "description": "Optional. The description of this gallery image definition resource. This property is updatable." } }, + "allowUpdateImage": { + "type": "bool", + "nullable": true, + "metadata": { + "description": "Optional. Must be set to true if the gallery image features are being updated." + } + }, "osType": { "type": "string", "allowedValues": [ @@ -110,6 +117,18 @@ "description": "Optional. Specify if the image supports hibernation." } }, + "diskControllerType": { + "type": "string", + "allowedValues": [ + "NVMe, SCSI", + "SCSI", + "SCSI, NVMe" + ], + "nullable": true, + "metadata": { + "description": "Optional. The disk controllers that an OS disk supports." + } + }, "architecture": { "type": "string", "allowedValues": [ @@ -653,7 +672,7 @@ }, "gallery": { "type": "Microsoft.Compute/galleries", - "apiVersion": "2023-07-03", + "apiVersion": "2024-03-03", "name": "[parameters('name')]", "location": "[parameters('location')]", "tags": "[parameters('tags')]", @@ -757,8 +776,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.32.4.45862", - "templateHash": "7761447372947910331" + "version": "0.33.93.31351", + "templateHash": "8421640590183520512" }, "name": "Compute Galleries Applications", "description": "This module deploys an Azure Compute Gallery Application." @@ -1033,12 +1052,12 @@ "gallery": { "existing": true, "type": "Microsoft.Compute/galleries", - "apiVersion": "2022-03-03", + "apiVersion": "2024-03-03", "name": "[parameters('galleryName')]" }, "application": { "type": "Microsoft.Compute/galleries/applications", - "apiVersion": "2022-03-03", + "apiVersion": "2024-03-03", "name": "[format('{0}/{1}', parameters('galleryName'), parameters('name'))]", "location": "[parameters('location')]", "tags": "[parameters('tags')]", @@ -1102,7 +1121,7 @@ "metadata": { "description": "The location the resource was deployed into." }, - "value": "[reference('application', '2022-03-03', 'full').location]" + "value": "[reference('application', '2024-03-03', 'full').location]" } } } @@ -1137,6 +1156,9 @@ "description": { "value": "[tryGet(coalesce(parameters('images'), createArray())[copyIndex()], 'description')]" }, + "allowUpdateImage": { + "value": "[tryGet(coalesce(parameters('images'), createArray())[copyIndex()], 'allowUpdateImage')]" + }, "osType": { "value": "[coalesce(parameters('images'), createArray())[copyIndex()].osType]" }, @@ -1164,6 +1186,9 @@ "isHibernateSupported": { "value": "[tryGet(coalesce(parameters('images'), createArray())[copyIndex()], 'isHibernateSupported')]" }, + "diskControllerType": { + "value": "[tryGet(coalesce(parameters('images'), createArray())[copyIndex()], 'diskControllerType')]" + }, "architecture": { "value": "[tryGet(coalesce(parameters('images'), createArray())[copyIndex()], 'architecture')]" }, @@ -1201,8 +1226,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.32.4.45862", - "templateHash": "13858460997059168010" + "version": "0.33.93.31351", + "templateHash": "5191633407919649272" }, "name": "Compute Galleries Image Definitions", "description": "This module deploys an Azure Compute Gallery Image Definition." @@ -1481,7 +1506,7 @@ }, "isAcceleratedNetworkSupported": { "type": "bool", - "defaultValue": true, + "nullable": true, "metadata": { "description": "Optional. Specify if the image supports accelerated networking." } @@ -1493,6 +1518,13 @@ "description": "Optional. Specifiy if the image supports hibernation." } }, + "allowUpdateImage": { + "type": "bool", + "nullable": true, + "metadata": { + "description": "Optional. Must be set to true if the gallery image features are being updated." + } + }, "architecture": { "type": "string", "allowedValues": [ @@ -1543,6 +1575,18 @@ "description": "Optional. The hypervisor generation of the Virtual Machine. If this value is not specified, then it is determined by the securityType parameter. If the securityType parameter is specified, then the value of hyperVGeneration will be V2, else V1." } }, + "diskControllerType": { + "type": "string", + "allowedValues": [ + "NVMe, SCSI", + "SCSI", + "SCSI, NVMe" + ], + "nullable": true, + "metadata": { + "description": "Optional. The disk controllers that an OS disk supports." + } + }, "roleAssignments": { "$ref": "#/definitions/roleAssignmentType", "metadata": { @@ -1579,16 +1623,17 @@ "gallery": { "existing": true, "type": "Microsoft.Compute/galleries", - "apiVersion": "2023-07-03", + "apiVersion": "2024-03-03", "name": "[parameters('galleryName')]" }, "image": { "type": "Microsoft.Compute/galleries/images", - "apiVersion": "2023-07-03", + "apiVersion": "2024-03-03", "name": "[format('{0}/{1}', parameters('galleryName'), parameters('name'))]", "location": "[parameters('location')]", "tags": "[parameters('tags')]", "properties": { + "allowUpdateImage": "[if(not(equals(parameters('allowUpdateImage'), null())), parameters('allowUpdateImage'), null())]", "architecture": "[parameters('architecture')]", "description": "[parameters('description')]", "disallowed": { @@ -1596,7 +1641,7 @@ }, "endOfLifeDate": "[parameters('endOfLifeDate')]", "eula": "[parameters('eula')]", - "features": "[union(createArray(createObject('name', 'IsAcceleratedNetworkSupported', 'value', format('{0}', parameters('isAcceleratedNetworkSupported')))), if(and(not(equals(parameters('securityType'), null())), not(equals(parameters('securityType'), 'Standard'))), createArray(createObject('name', 'SecurityType', 'value', format('{0}', parameters('securityType')))), createArray()), if(not(equals(parameters('isHibernateSupported'), null())), createArray(createObject('name', 'IsHibernateSupported', 'value', format('{0}', parameters('isHibernateSupported')))), createArray()))]", + "features": "[union(if(not(equals(parameters('isAcceleratedNetworkSupported'), null())), createArray(createObject('name', 'IsAcceleratedNetworkSupported', 'value', format('{0}', parameters('isAcceleratedNetworkSupported')))), createArray()), if(and(not(equals(parameters('securityType'), null())), not(equals(parameters('securityType'), 'Standard'))), createArray(createObject('name', 'SecurityType', 'value', format('{0}', parameters('securityType')))), createArray()), if(not(equals(parameters('isHibernateSupported'), null())), createArray(createObject('name', 'IsHibernateSupported', 'value', format('{0}', parameters('isHibernateSupported')))), createArray()), if(not(equals(parameters('diskControllerType'), null())), createArray(createObject('name', 'DiskControllerTypes', 'value', format('{0}', parameters('diskControllerType')))), createArray()))]", "hyperVGeneration": "[coalesce(parameters('hyperVGeneration'), if(not(empty(coalesce(parameters('securityType'), ''))), 'V2', 'V1'))]", "identifier": { "publisher": "[parameters('identifier').publisher]", @@ -1664,7 +1709,7 @@ "metadata": { "description": "The location the resource was deployed into." }, - "value": "[reference('image', '2023-07-03', 'full').location]" + "value": "[reference('image', '2024-03-03', 'full').location]" } } } @@ -1701,7 +1746,7 @@ "metadata": { "description": "The location the resource was deployed into." }, - "value": "[reference('gallery', '2023-07-03', 'full').location]" + "value": "[reference('gallery', '2024-03-03', 'full').location]" }, "imageResourceIds": { "type": "array", diff --git a/avm/res/compute/gallery/tests/e2e/max/main.test.bicep b/avm/res/compute/gallery/tests/e2e/max/main.test.bicep index bca3fb8249..c301dea7da 100644 --- a/avm/res/compute/gallery/tests/e2e/max/main.test.bicep +++ b/avm/res/compute/gallery/tests/e2e/max/main.test.bicep @@ -91,6 +91,7 @@ module testDeployment '../../../main.bicep' = [ images: [ { name: '${namePrefix}-az-imgd-ws-001' + allowUpdateImage: true hyperVGeneration: 'V1' identifier: { publisher: 'MicrosoftWindowsServer' @@ -114,6 +115,7 @@ module testDeployment '../../../main.bicep' = [ } { name: '${namePrefix}-az-imgd-ws-002' + allowUpdateImage: false hyperVGeneration: 'V2' identifier: { publisher: 'MicrosoftWindowsServer' @@ -178,6 +180,7 @@ module testDeployment '../../../main.bicep' = [ max: 4 } isAcceleratedNetworkSupported: false + diskControllerType: 'SCSI' } { name: '${namePrefix}-az-imgd-us-005' @@ -198,6 +201,7 @@ module testDeployment '../../../main.bicep' = [ max: 4 } isAcceleratedNetworkSupported: true + diskControllerType: 'SCSI, NVMe' } { name: '${namePrefix}-az-imgd-us-006' @@ -232,6 +236,7 @@ module testDeployment '../../../main.bicep' = [ } releaseNoteUri: 'https://testReleaseNoteUri.com' isAcceleratedNetworkSupported: false + // diskControllerType: 'NVMe, SCSI' // --> needs to remain commented, as there is a bug setting the value starting with 'NVMe' again, which prevents the idem test to pass } ] roleAssignments: [ @@ -262,8 +267,5 @@ module testDeployment '../../../main.bicep' = [ Role: 'DeploymentValidation' } } - dependsOn: [ - nestedDependencies - ] } ] diff --git a/avm/res/compute/gallery/version.json b/avm/res/compute/gallery/version.json index 9a9a06e897..6b6be93891 100644 --- a/avm/res/compute/gallery/version.json +++ b/avm/res/compute/gallery/version.json @@ -1,6 +1,6 @@ { "$schema": "https://aka.ms/bicep-registry-module-version-file-schema#", - "version": "0.8", + "version": "0.9", "pathFilters": [ "./main.json" ] From 10d8c7392b64b2a9f83eac1ed8d82d4baa2e2adf Mon Sep 17 00:00:00 2001 From: Seif Bassem <38246040+sebassem@users.noreply.github.com> Date: Thu, 13 Feb 2025 12:47:15 +0200 Subject: [PATCH 5/7] fix: sub-vending, adding CI secret (#4442) ## Description Adding a custom CI secret to the sub-vending module ## Pipeline Reference <!-- Insert your Pipeline Status Badge below --> | Pipeline | | -------- | | [![avm.ptn.lz.sub-vending](https://github.com/sebassem/bicep-registry-modules/actions/workflows/avm.ptn.lz.sub-vending.yml/badge.svg?branch=avm-ptn-vending-add-secret)](https://github.com/sebassem/bicep-registry-modules/actions/workflows/avm.ptn.lz.sub-vending.yml) | ## Type of Change <!-- Use the checkboxes [x] on the options that are relevant. --> - [ ] Update to CI Environment or utilities (Non-module affecting changes) - [ ] Azure Verified Module updates: - [ X] Bugfix containing backwards-compatible bug fixes, and I have NOT bumped the MAJOR or MINOR version in `version.json`: - [ ] Someone has opened a bug report issue, and I have included "Closes #{bug_report_issue_number}" in the PR description. - [X] The bug was found by the module author, and no one has opened an issue to report it yet. - [ ] Feature update backwards compatible feature updates, and I have bumped the MINOR version in `version.json`. - [ ] Breaking changes and I have bumped the MAJOR version in `version.json`. - [ ] Update to documentation ## Checklist - [X] I'm sure there are no other open Pull Requests for the same update/change - [X] I have run `Set-AVMModule` locally to generate the supporting module files. - [X] My corresponding pipelines / checks run clean and green without any errors or warnings <!-- Please keep up to date with the contribution guide at https://aka.ms/avm/contribute/bicep --> --- avm/ptn/lz/sub-vending/main.json | 60 +++++++++---------- .../tests/e2e/defaults/main.test.bicep | 6 +- .../tests/e2e/hub-spoke/main.test.bicep | 6 +- .../tests/e2e/rbac-condition/main.test.bicep | 6 +- .../tests/e2e/vwan-spoke/main.test.bicep | 6 +- avm/ptn/network/hub-networking/main.json | 16 ++--- 6 files changed, 50 insertions(+), 50 deletions(-) diff --git a/avm/ptn/lz/sub-vending/main.json b/avm/ptn/lz/sub-vending/main.json index 2421387f0d..649e69f153 100644 --- a/avm/ptn/lz/sub-vending/main.json +++ b/avm/ptn/lz/sub-vending/main.json @@ -5,8 +5,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.33.13.18514", - "templateHash": "16856855794906870670" + "version": "0.33.93.31351", + "templateHash": "14919277706976174104" }, "name": "Sub-vending", "description": "This module deploys a subscription to accelerate deployment of landing zones. For more information on how to use it, please visit this [Wiki](https://github.com/Azure/bicep-lz-vending/wiki).", @@ -669,8 +669,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.33.13.18514", - "templateHash": "2801471703151139948" + "version": "0.33.93.31351", + "templateHash": "8425865084067531624" } }, "parameters": { @@ -880,8 +880,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.33.13.18514", - "templateHash": "250791371811352682" + "version": "0.33.93.31351", + "templateHash": "11018695082972643897" }, "name": "`/subResourcesWrapper/deploy.bicep` Parameters", "description": "This module is used by the [`bicep-lz-vending`](https://aka.ms/sub-vending/bicep) module to help orchestrate the deployment", @@ -1588,8 +1588,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.33.13.18514", - "templateHash": "12834168093418139358" + "version": "0.33.93.31351", + "templateHash": "8452628568304993719" } }, "parameters": { @@ -1649,8 +1649,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.33.13.18514", - "templateHash": "15508893729756562690" + "version": "0.33.93.31351", + "templateHash": "11019372772177629958" } }, "parameters": { @@ -1709,8 +1709,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.33.13.18514", - "templateHash": "2541836993831686925" + "version": "0.33.93.31351", + "templateHash": "8397259050503224920" } }, "parameters": { @@ -1765,8 +1765,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.33.13.18514", - "templateHash": "7264500549088500335" + "version": "0.33.93.31351", + "templateHash": "7623404265819505597" } }, "parameters": { @@ -1843,8 +1843,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.33.13.18514", - "templateHash": "9459644647794329484" + "version": "0.33.93.31351", + "templateHash": "8957892045766331539" } }, "parameters": { @@ -1898,8 +1898,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.33.13.18514", - "templateHash": "8593973730489733307" + "version": "0.33.93.31351", + "templateHash": "14513856367602857749" } }, "parameters": { @@ -2511,8 +2511,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.33.13.18514", - "templateHash": "15508893729756562690" + "version": "0.33.93.31351", + "templateHash": "11019372772177629958" } }, "parameters": { @@ -2571,8 +2571,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.33.13.18514", - "templateHash": "2541836993831686925" + "version": "0.33.93.31351", + "templateHash": "8397259050503224920" } }, "parameters": { @@ -2627,8 +2627,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.33.13.18514", - "templateHash": "7264500549088500335" + "version": "0.33.93.31351", + "templateHash": "7623404265819505597" } }, "parameters": { @@ -2705,8 +2705,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.33.13.18514", - "templateHash": "9459644647794329484" + "version": "0.33.93.31351", + "templateHash": "8957892045766331539" } }, "parameters": { @@ -2760,8 +2760,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.33.13.18514", - "templateHash": "8593973730489733307" + "version": "0.33.93.31351", + "templateHash": "14513856367602857749" } }, "parameters": { @@ -4454,8 +4454,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.33.13.18514", - "templateHash": "4537798139153123030" + "version": "0.33.93.31351", + "templateHash": "12390675321128699904" } }, "parameters": { diff --git a/avm/ptn/lz/sub-vending/tests/e2e/defaults/main.test.bicep b/avm/ptn/lz/sub-vending/tests/e2e/defaults/main.test.bicep index 2a0aba1b3b..ffc3e62a24 100644 --- a/avm/ptn/lz/sub-vending/tests/e2e/defaults/main.test.bicep +++ b/avm/ptn/lz/sub-vending/tests/e2e/defaults/main.test.bicep @@ -6,9 +6,9 @@ targetScope = 'managementGroup' @description('Optional. The location to deploy resources to.') param resourceLocation string = deployment().location -// This parameter needs to be updated with the billing account and the enrollment account of your enviornment. -@description('Optional. The subscription billing scope.') -param subscriptionBillingScope string = 'providers/Microsoft.Billing/billingAccounts/7690848/enrollmentAccounts/350580' +@description('Required. The scope of the subscription billing. This value is tenant-specific and must be stored in the CI Key Vault in a secret named \'CI-SubscriptionBillingScope\'.') +@secure() +param subscriptionBillingScope string = '' @description('Optional. A token to inject into the name of each resource.') param namePrefix string = '#_namePrefix_#' diff --git a/avm/ptn/lz/sub-vending/tests/e2e/hub-spoke/main.test.bicep b/avm/ptn/lz/sub-vending/tests/e2e/hub-spoke/main.test.bicep index e9a70825bb..e817b70b37 100644 --- a/avm/ptn/lz/sub-vending/tests/e2e/hub-spoke/main.test.bicep +++ b/avm/ptn/lz/sub-vending/tests/e2e/hub-spoke/main.test.bicep @@ -6,9 +6,9 @@ targetScope = 'managementGroup' @description('Optional. The location to deploy resources to.') param resourceLocation string = deployment().location -// This parameter needs to be updated with the billing account and the enrollment account of your enviornment. -@description('Optional. The subscription billing scope.') -param subscriptionBillingScope string = 'providers/Microsoft.Billing/billingAccounts/7690848/enrollmentAccounts/350580' +@description('Required. The scope of the subscription billing. This value is tenant-specific and must be stored in the CI Key Vault in a secret named \'CI-SubscriptionBillingScope\'.') +@secure() +param subscriptionBillingScope string = '' @description('Optional. A token to inject into the name of each resource.') param namePrefix string = '#_namePrefix_#' diff --git a/avm/ptn/lz/sub-vending/tests/e2e/rbac-condition/main.test.bicep b/avm/ptn/lz/sub-vending/tests/e2e/rbac-condition/main.test.bicep index c69578e2c9..8c897dcee0 100644 --- a/avm/ptn/lz/sub-vending/tests/e2e/rbac-condition/main.test.bicep +++ b/avm/ptn/lz/sub-vending/tests/e2e/rbac-condition/main.test.bicep @@ -6,9 +6,9 @@ targetScope = 'managementGroup' @description('Optional. The location to deploy resources to.') param resourceLocation string = deployment().location -// This parameter needs to be updated with the billing account and the enrollment account of your enviornment. -@description('Optional. The subscription billing scope.') -param subscriptionBillingScope string = 'providers/Microsoft.Billing/billingAccounts/7690848/enrollmentAccounts/350580' +@description('Required. The scope of the subscription billing. This value is tenant-specific and must be stored in the CI Key Vault in a secret named \'CI-SubscriptionBillingScope\'.') +@secure() +param subscriptionBillingScope string = '' @description('Optional. A token to inject into the name of each resource.') param namePrefix string = '#_namePrefix_#' diff --git a/avm/ptn/lz/sub-vending/tests/e2e/vwan-spoke/main.test.bicep b/avm/ptn/lz/sub-vending/tests/e2e/vwan-spoke/main.test.bicep index f4ebc8730c..48449af2f6 100644 --- a/avm/ptn/lz/sub-vending/tests/e2e/vwan-spoke/main.test.bicep +++ b/avm/ptn/lz/sub-vending/tests/e2e/vwan-spoke/main.test.bicep @@ -6,9 +6,9 @@ targetScope = 'managementGroup' @description('Optional. The location to deploy resources to.') param resourceLocation string = deployment().location -// This parameter needs to be updated with the billing account and the enrollment account of your enviornment. -@description('Optional. The subscription billing scope.') -param subscriptionBillingScope string = 'providers/Microsoft.Billing/billingAccounts/7690848/enrollmentAccounts/350580' +@description('Required. The scope of the subscription billing. This value is tenant-specific and must be stored in the CI Key Vault in a secret named \'CI-SubscriptionBillingScope\'.') +@secure() +param subscriptionBillingScope string = '' @description('Optional. A token to inject into the name of each resource.') param namePrefix string = '#_namePrefix_#' diff --git a/avm/ptn/network/hub-networking/main.json b/avm/ptn/network/hub-networking/main.json index f3c3c12f2a..124c1e8233 100644 --- a/avm/ptn/network/hub-networking/main.json +++ b/avm/ptn/network/hub-networking/main.json @@ -5,8 +5,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.32.4.45862", - "templateHash": "17389954898374222994" + "version": "0.33.93.31351", + "templateHash": "10309836000326275731" }, "name": "Hub Networking", "description": "This module is designed to simplify the creation of multi-region hub networks in Azure. It will create a number of virtual networks and subnets, and optionally peer them together in a mesh topology with routing." @@ -2340,8 +2340,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.32.4.45862", - "templateHash": "13800865708253602673" + "version": "0.33.93.31351", + "templateHash": "6971888757750761810" }, "name": "Virtual Networks", "description": "This module deploys a Virtual Network." @@ -6467,8 +6467,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.32.4.45862", - "templateHash": "17332298613317998164" + "version": "0.33.93.31351", + "templateHash": "4695558034353162860" }, "name": "Existing Virtual Network Subnets", "description": "This module retrieves an existing Virtual Network Subnet." @@ -6549,8 +6549,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.32.4.45862", - "templateHash": "13712864569972623739" + "version": "0.33.93.31351", + "templateHash": "3980815084200627301" }, "name": "Virtual Network Subnets", "description": "This module deploys a Virtual Network Subnet." From 3bdd5951311b5b5292d9d8de95f083caa1820ac6 Mon Sep 17 00:00:00 2001 From: Seif Bassem <38246040+sebassem@users.noreply.github.com> Date: Thu, 13 Feb 2025 18:46:03 +0200 Subject: [PATCH 6/7] fix: enhancements to hub-spoke module (#4443) ## Description Closes #4433 - Added options to specify bastion and route table names - Added values for bastion and firewall Skus in UDT - Added locks to route table - Updated the necessary tests ## Pipeline Reference <!-- Insert your Pipeline Status Badge below --> | Pipeline | | -------- | | [![avm.ptn.network.hub-networking](https://github.com/sebassem/bicep-registry-modules/actions/workflows/avm.ptn.network.hub-networking.yml/badge.svg?branch=hub-spoke-enhancements)](https://github.com/sebassem/bicep-registry-modules/actions/workflows/avm.ptn.network.hub-networking.yml) | ## Type of Change <!-- Use the checkboxes [x] on the options that are relevant. --> - [ ] Update to CI Environment or utilities (Non-module affecting changes) - [ ] Azure Verified Module updates: - [X] Bugfix containing backwards-compatible bug fixes, and I have NOT bumped the MAJOR or MINOR version in `version.json`: - [X] Someone has opened a bug report issue, and I have included "Closes #{bug_report_issue_number}" in the PR description. - [ ] The bug was found by the module author, and no one has opened an issue to report it yet. - [ ] Feature update backwards compatible feature updates, and I have bumped the MINOR version in `version.json`. - [ ] Breaking changes and I have bumped the MAJOR version in `version.json`. - [X] Update to documentation ## Checklist - [X] I'm sure there are no other open Pull Requests for the same update/change - [X] I have run `Set-AVMModule` locally to generate the supporting module files. - [X] My corresponding pipelines / checks run clean and green without any errors or warnings <!-- Please keep up to date with the contribution guide at https://aka.ms/avm/contribute/bicep --> --- avm/ptn/network/hub-networking/README.md | 69 ++++++++++++++++++- avm/ptn/network/hub-networking/main.bicep | 25 +++++-- avm/ptn/network/hub-networking/main.json | 56 +++++++++++++-- .../tests/e2e/max/main.test.bicep | 6 ++ 4 files changed, 146 insertions(+), 10 deletions(-) diff --git a/avm/ptn/network/hub-networking/README.md b/avm/ptn/network/hub-networking/README.md index b20fa6928b..9151a55c9b 100644 --- a/avm/ptn/network/hub-networking/README.md +++ b/avm/ptn/network/hub-networking/README.md @@ -118,8 +118,14 @@ module hubNetworking 'br/public:avm/ptn/network/hub-networking:<version>' = { name: 'hub1-waf-pip' } threatIntelMode: 'Alert' + zones: [ + 1 + 2 + 3 + ] } bastionHost: { + bastionHostName: 'bastion-hub1' disableCopyPaste: true enableFileCopy: false enableIpConnect: false @@ -298,9 +304,15 @@ module hubNetworking 'br/public:avm/ptn/network/hub-networking:<version>' = { "publicIPAddressObject": { "name": "hub1-waf-pip" }, - "threatIntelMode": "Alert" + "threatIntelMode": "Alert", + "zones": [ + 1, + 2, + 3 + ] }, "bastionHost": { + "bastionHostName": "bastion-hub1", "disableCopyPaste": true, "enableFileCopy": false, "enableIpConnect": false, @@ -480,8 +492,14 @@ param hubVirtualNetworks = { name: 'hub1-waf-pip' } threatIntelMode: 'Alert' + zones: [ + 1 + 2 + 3 + ] } bastionHost: { + bastionHostName: 'bastion-hub1' disableCopyPaste: true enableFileCopy: false enableIpConnect: false @@ -1255,6 +1273,7 @@ The hub virtual networks to create. | [`peeringSettings`](#parameter-hubvirtualnetworks>any_other_property<peeringsettings) | array | The peerings of the virtual network. | | [`roleAssignments`](#parameter-hubvirtualnetworks>any_other_property<roleassignments) | array | The role assignments to create. | | [`routes`](#parameter-hubvirtualnetworks>any_other_property<routes) | array | Routes to add to the virtual network route table. | +| [`routeTableName`](#parameter-hubvirtualnetworks>any_other_property<routetablename) | string | The name of the route table. | | [`subnets`](#parameter-hubvirtualnetworks>any_other_property<subnets) | array | The subnets of the virtual network. | | [`tags`](#parameter-hubvirtualnetworks>any_other_property<tags) | object | The tags of the virtual network. | | [`vnetEncryption`](#parameter-hubvirtualnetworks>any_other_property<vnetencryption) | bool | Enable/Disable VNet encryption. | @@ -1280,6 +1299,7 @@ The Azure Firewall config. | :-- | :-- | :-- | | [`additionalPublicIpConfigurations`](#parameter-hubvirtualnetworks>any_other_property<azurefirewallsettingsadditionalpublicipconfigurations) | array | Additional public IP configurations. | | [`applicationRuleCollections`](#parameter-hubvirtualnetworks>any_other_property<azurefirewallsettingsapplicationrulecollections) | array | Application rule collections. | +| [`azureFirewallName`](#parameter-hubvirtualnetworks>any_other_property<azurefirewallsettingsazurefirewallname) | string | The name of the Azure Firewall. | | [`azureSkuTier`](#parameter-hubvirtualnetworks>any_other_property<azurefirewallsettingsazureskutier) | string | Azure Firewall SKU. | | [`diagnosticSettings`](#parameter-hubvirtualnetworks>any_other_property<azurefirewallsettingsdiagnosticsettings) | array | Diagnostic settings. | | [`enableTelemetry`](#parameter-hubvirtualnetworks>any_other_property<azurefirewallsettingsenabletelemetry) | bool | Enable/Disable usage telemetry for module. | @@ -1313,12 +1333,27 @@ Application rule collections. - Required: No - Type: array +### Parameter: `hubVirtualNetworks.>Any_other_property<.azureFirewallSettings.azureFirewallName` + +The name of the Azure Firewall. + +- Required: No +- Type: string + ### Parameter: `hubVirtualNetworks.>Any_other_property<.azureFirewallSettings.azureSkuTier` Azure Firewall SKU. - Required: No - Type: string +- Allowed: + ```Bicep + [ + 'Basic' + 'Premium' + 'Standard' + ] + ``` ### Parameter: `hubVirtualNetworks.>Any_other_property<.azureFirewallSettings.diagnosticSettings` @@ -1708,13 +1743,22 @@ The Azure Bastion config. | Parameter | Type | Description | | :-- | :-- | :-- | +| [`bastionHostName`](#parameter-hubvirtualnetworks>any_other_property<bastionhostbastionhostname) | string | The name of the bastion host. | | [`disableCopyPaste`](#parameter-hubvirtualnetworks>any_other_property<bastionhostdisablecopypaste) | bool | Enable/Disable copy/paste functionality. | | [`enableFileCopy`](#parameter-hubvirtualnetworks>any_other_property<bastionhostenablefilecopy) | bool | Enable/Disable file copy functionality. | | [`enableIpConnect`](#parameter-hubvirtualnetworks>any_other_property<bastionhostenableipconnect) | bool | Enable/Disable IP connect functionality. | +| [`enableKerberos`](#parameter-hubvirtualnetworks>any_other_property<bastionhostenablekerberos) | bool | Enable/Disable Kerberos authentication. | | [`enableShareableLink`](#parameter-hubvirtualnetworks>any_other_property<bastionhostenableshareablelink) | bool | Enable/Disable shareable link functionality. | | [`scaleUnits`](#parameter-hubvirtualnetworks>any_other_property<bastionhostscaleunits) | int | The number of scale units for the Bastion host. Defaults to 4. | | [`skuName`](#parameter-hubvirtualnetworks>any_other_property<bastionhostskuname) | string | The SKU name of the Bastion host. Defaults to Standard. | +### Parameter: `hubVirtualNetworks.>Any_other_property<.bastionHost.bastionHostName` + +The name of the bastion host. + +- Required: No +- Type: string + ### Parameter: `hubVirtualNetworks.>Any_other_property<.bastionHost.disableCopyPaste` Enable/Disable copy/paste functionality. @@ -1736,6 +1780,13 @@ Enable/Disable IP connect functionality. - Required: No - Type: bool +### Parameter: `hubVirtualNetworks.>Any_other_property<.bastionHost.enableKerberos` + +Enable/Disable Kerberos authentication. + +- Required: No +- Type: bool + ### Parameter: `hubVirtualNetworks.>Any_other_property<.bastionHost.enableShareableLink` Enable/Disable shareable link functionality. @@ -1756,6 +1807,15 @@ The SKU name of the Bastion host. Defaults to Standard. - Required: No - Type: string +- Allowed: + ```Bicep + [ + 'Basic' + 'Developer' + 'Premium' + 'Standard' + ] + ``` ### Parameter: `hubVirtualNetworks.>Any_other_property<.ddosProtectionPlanResourceId` @@ -2151,6 +2211,13 @@ Routes to add to the virtual network route table. - Required: No - Type: array +### Parameter: `hubVirtualNetworks.>Any_other_property<.routeTableName` + +The name of the route table. + +- Required: No +- Type: string + ### Parameter: `hubVirtualNetworks.>Any_other_property<.subnets` The subnets of the virtual network. diff --git a/avm/ptn/network/hub-networking/main.bicep b/avm/ptn/network/hub-networking/main.bicep index b734027146..33573075b5 100644 --- a/avm/ptn/network/hub-networking/main.bicep +++ b/avm/ptn/network/hub-networking/main.bicep @@ -111,13 +111,14 @@ module hubRouteTable 'br/public:avm/res/network/route-table:0.4.0' = [ for (hub, index) in items(hubVirtualNetworks ?? {}): { name: '${uniqueString(deployment().name, location)}-${hub.key}-nrt' params: { - name: hub.key + name: hub.value.?routeTableName ?? hub.key location: hub.value.?location ?? location disableBgpRoutePropagation: true enableTelemetry: hub.value.?enableTelemetry ?? true roleAssignments: hub.value.?roleAssignments ?? [] routes: hub.value.?routes ?? [] tags: hub.value.?tags ?? {} + lock: hub.value.?lock ?? {} } dependsOn: hubVirtualNetwork } @@ -144,7 +145,7 @@ module hubBastion 'br/public:avm/res/network/bastion-host:0.4.0' = [ name: '${uniqueString(deployment().name, location)}-${hub.key}-nbh' params: { // Required parameters - name: hub.key + name: hub.value.?bastionHost.?bastionHostName ?? hub.key virtualNetworkResourceId: hubVirtualNetwork[index].outputs.resourceId // Non-required parameters diagnosticSettings: hub.value.?diagnosticSettings ?? [] @@ -158,6 +159,8 @@ module hubBastion 'br/public:avm/res/network/bastion-host:0.4.0' = [ scaleUnits: hub.value.?bastionHost.?scaleUnits ?? 4 skuName: hub.value.?bastionHost.?skuName ?? 'Standard' tags: hub.value.?tags ?? {} + lock: hub.value.?lock ?? {} + enableKerberos: hub.value.?bastionHost.?enableKerberos ?? false } dependsOn: hubVirtualNetwork } @@ -170,7 +173,7 @@ module hubAzureFirewall 'br/public:avm/res/network/azure-firewall:0.5.1' = [ name: '${uniqueString(deployment().name, location)}-${hub.key}-naf' params: { // Required parameters - name: hub.key + name: hub.value.?azureFirewallSettings.?azureFirewallName ?? hub.key // Conditional parameters hubIPAddresses: hub.value.?azureFirewallSettings.?hubIpAddresses ?? {} virtualHubId: hub.value.?azureFirewallSettings.?virtualHub ?? '' @@ -383,11 +386,17 @@ type hubVirtualNetworkType = { @description('Optional. Enable/Disable shareable link functionality.') enableShareableLink: bool? + @description('Optional. Enable/Disable Kerberos authentication.') + enableKerberos: bool? + @description('Optional. The number of scale units for the Bastion host. Defaults to 4.') scaleUnits: int? @description('Optional. The SKU name of the Bastion host. Defaults to Standard.') - skuName: string? + skuName: 'Basic' | 'Developer' | 'Premium' | 'Standard'? + + @description('Optional. The name of the bastion host.') + bastionHostName: string? }? @description('Optional. Enable/Disable usage telemetry for module.') @@ -429,6 +438,9 @@ type hubVirtualNetworkType = { @description('Optional. Routes to add to the virtual network route table.') routes: array? + @description('Optional. The name of the route table.') + routeTableName: string? + @description('Optional. The subnets of the virtual network.') subnets: array? @@ -461,6 +473,9 @@ type peeringSettingsType = { }[]? type azureFirewallType = { + @description('Optional. The name of the Azure Firewall.') + azureFirewallName: string? + @description('Optional. Hub IP addresses.') hubIpAddresses: object? @@ -474,7 +489,7 @@ type azureFirewallType = { applicationRuleCollections: array? @description('Optional. Azure Firewall SKU.') - azureSkuTier: string? + azureSkuTier: 'Basic' | 'Standard' | 'Premium'? @description('Optional. Diagnostic settings.') diagnosticSettings: diagnosticSettingType? diff --git a/avm/ptn/network/hub-networking/main.json b/avm/ptn/network/hub-networking/main.json index 124c1e8233..0c7284103c 100644 --- a/avm/ptn/network/hub-networking/main.json +++ b/avm/ptn/network/hub-networking/main.json @@ -6,7 +6,7 @@ "_generator": { "name": "bicep", "version": "0.33.93.31351", - "templateHash": "10309836000326275731" + "templateHash": "13578022486694565821" }, "name": "Hub Networking", "description": "This module is designed to simplify the creation of multi-region hub networks in Azure. It will create a number of virtual networks and subnets, and optionally peer them together in a mesh topology with routing." @@ -280,6 +280,13 @@ "description": "Optional. Enable/Disable shareable link functionality." } }, + "enableKerberos": { + "type": "bool", + "nullable": true, + "metadata": { + "description": "Optional. Enable/Disable Kerberos authentication." + } + }, "scaleUnits": { "type": "int", "nullable": true, @@ -289,10 +296,23 @@ }, "skuName": { "type": "string", + "allowedValues": [ + "Basic", + "Developer", + "Premium", + "Standard" + ], "nullable": true, "metadata": { "description": "Optional. The SKU name of the Bastion host. Defaults to Standard." } + }, + "bastionHostName": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The name of the bastion host." + } } }, "nullable": true, @@ -391,6 +411,13 @@ "description": "Optional. Routes to add to the virtual network route table." } }, + "routeTableName": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The name of the route table." + } + }, "subnets": { "type": "array", "nullable": true, @@ -473,6 +500,13 @@ "azureFirewallType": { "type": "object", "properties": { + "azureFirewallName": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The name of the Azure Firewall." + } + }, "hubIpAddresses": { "type": "object", "nullable": true, @@ -503,6 +537,11 @@ }, "azureSkuTier": { "type": "string", + "allowedValues": [ + "Basic", + "Premium", + "Standard" + ], "nullable": true, "metadata": { "description": "Optional. Azure Firewall SKU." @@ -2406,7 +2445,7 @@ "mode": "Incremental", "parameters": { "name": { - "value": "[items(coalesce(parameters('hubVirtualNetworks'), createObject()))[copyIndex()].key]" + "value": "[coalesce(tryGet(items(coalesce(parameters('hubVirtualNetworks'), createObject()))[copyIndex()].value, 'routeTableName'), items(coalesce(parameters('hubVirtualNetworks'), createObject()))[copyIndex()].key)]" }, "location": { "value": "[coalesce(tryGet(items(coalesce(parameters('hubVirtualNetworks'), createObject()))[copyIndex()].value, 'location'), parameters('location'))]" @@ -2425,6 +2464,9 @@ }, "tags": { "value": "[coalesce(tryGet(items(coalesce(parameters('hubVirtualNetworks'), createObject()))[copyIndex()].value, 'tags'), createObject())]" + }, + "lock": { + "value": "[coalesce(tryGet(items(coalesce(parameters('hubVirtualNetworks'), createObject()))[copyIndex()].value, 'lock'), createObject())]" } }, "template": { @@ -2790,7 +2832,7 @@ "mode": "Incremental", "parameters": { "name": { - "value": "[items(coalesce(parameters('hubVirtualNetworks'), createObject()))[copyIndex()].key]" + "value": "[coalesce(tryGet(tryGet(items(coalesce(parameters('hubVirtualNetworks'), createObject()))[copyIndex()].value, 'bastionHost'), 'bastionHostName'), items(coalesce(parameters('hubVirtualNetworks'), createObject()))[copyIndex()].key)]" }, "virtualNetworkResourceId": { "value": "[reference(format('hubVirtualNetwork[{0}]', copyIndex())).outputs.resourceId.value]" @@ -2827,6 +2869,12 @@ }, "tags": { "value": "[coalesce(tryGet(items(coalesce(parameters('hubVirtualNetworks'), createObject()))[copyIndex()].value, 'tags'), createObject())]" + }, + "lock": { + "value": "[coalesce(tryGet(items(coalesce(parameters('hubVirtualNetworks'), createObject()))[copyIndex()].value, 'lock'), createObject())]" + }, + "enableKerberos": { + "value": "[coalesce(tryGet(tryGet(items(coalesce(parameters('hubVirtualNetworks'), createObject()))[copyIndex()].value, 'bastionHost'), 'enableKerberos'), false())]" } }, "template": { @@ -4010,7 +4058,7 @@ "mode": "Incremental", "parameters": { "name": { - "value": "[items(coalesce(parameters('hubVirtualNetworks'), createObject()))[copyIndex()].key]" + "value": "[coalesce(tryGet(tryGet(items(coalesce(parameters('hubVirtualNetworks'), createObject()))[copyIndex()].value, 'azureFirewallSettings'), 'azureFirewallName'), items(coalesce(parameters('hubVirtualNetworks'), createObject()))[copyIndex()].key)]" }, "hubIPAddresses": { "value": "[coalesce(tryGet(tryGet(items(coalesce(parameters('hubVirtualNetworks'), createObject()))[copyIndex()].value, 'azureFirewallSettings'), 'hubIpAddresses'), createObject())]" diff --git a/avm/ptn/network/hub-networking/tests/e2e/max/main.test.bicep b/avm/ptn/network/hub-networking/tests/e2e/max/main.test.bicep index 4a21691f16..f03c4132ff 100644 --- a/avm/ptn/network/hub-networking/tests/e2e/max/main.test.bicep +++ b/avm/ptn/network/hub-networking/tests/e2e/max/main.test.bicep @@ -71,8 +71,14 @@ module testDeployment '../../../main.bicep' = [ name: 'hub1-waf-pip' } threatIntelMode: 'Alert' + zones: [ + 1 + 2 + 3 + ] } bastionHost: { + bastionHostName: 'bastion-hub1' disableCopyPaste: true enableFileCopy: false enableIpConnect: false From 0d9bb07381d25376f92efa8d78a2bc03d5998a18 Mon Sep 17 00:00:00 2001 From: hundredacres <hundredacres.rm@yahoo.com> Date: Thu, 13 Feb 2025 09:16:25 -0800 Subject: [PATCH 7/7] feat: Allowing shardCount to be nullified (#4437) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Description Allow shardCount to be nullified to disable default clustering behavior. Below is the behavior of AVM before implementing the change to shardCount. <img width="1148" alt="Screenshot 2025-02-12 at 11 29 37 AM" src="https://github.com/user-attachments/assets/6889136c-4473-4de3-9352-f763234f6430" /> After implementing the fix, the Premium cache is not clustered. <img width="1151" alt="Screenshot 2025-02-12 at 11 28 56 AM" src="https://github.com/user-attachments/assets/519f9de7-ece9-4d6e-ac39-4fbc551263dd" /> Fixes #4380 Closes #4380 ## Pipeline Reference <!-- Insert your Pipeline Status Badge below --> | Pipeline | | -------- | | [![avm.res.cache.redis](https://github.com/hundredacres/bicep-registry-modules/actions/workflows/avm.res.cache.redis.yml/badge.svg?branch=feat%2Fissues%2F4380)](https://github.com/hundredacres/bicep-registry-modules/actions/workflows/avm.res.cache.redis.yml) | ## Type of Change <!-- Use the checkboxes [x] on the options that are relevant. --> - [ ] Update to CI Environment or utilities (Non-module affecting changes) - [X] Azure Verified Module updates: - [X] Bugfix containing backwards-compatible bug fixes, and I have NOT bumped the MAJOR or MINOR version in `version.json`: - [X] Someone has opened a bug report issue, and I have included "Closes #{bug_report_issue_number}" in the PR description. - [ ] The bug was found by the module author, and no one has opened an issue to report it yet. - [ ] Feature update backwards compatible feature updates, and I have bumped the MINOR version in `version.json`. - [ ] Breaking changes and I have bumped the MAJOR version in `version.json`. - [ ] Update to documentation ## Checklist - [X] I'm sure there are no other open Pull Requests for the same update/change - [X] I have run `Set-AVMModule` locally to generate the supporting module files. - [X] My corresponding pipelines / checks run clean and green without any errors or warnings <!-- Please keep up to date with the contribution guide at https://aka.ms/avm/contribute/bicep --> --- avm/res/cache/redis/README.md | 1 - avm/res/cache/redis/linked-servers/main.json | 4 ++-- avm/res/cache/redis/main.bicep | 2 +- avm/res/cache/redis/main.json | 14 +++++++------- 4 files changed, 10 insertions(+), 11 deletions(-) diff --git a/avm/res/cache/redis/README.md b/avm/res/cache/redis/README.md index 49297597e7..a47ace669a 100644 --- a/avm/res/cache/redis/README.md +++ b/avm/res/cache/redis/README.md @@ -2183,7 +2183,6 @@ The number of shards to be created on a Premium Cluster Cache. - Required: No - Type: int -- Default: `1` - MinValue: 1 ### Parameter: `skuName` diff --git a/avm/res/cache/redis/linked-servers/main.json b/avm/res/cache/redis/linked-servers/main.json index 170402a3d5..5c70c19270 100644 --- a/avm/res/cache/redis/linked-servers/main.json +++ b/avm/res/cache/redis/linked-servers/main.json @@ -5,8 +5,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.33.13.18514", - "templateHash": "11294861621866290910" + "version": "0.33.93.31351", + "templateHash": "322534394383651316" }, "name": "Redis Cache Linked Servers", "description": "This module connects a primary and secondary Redis Cache together for geo-replication." diff --git a/avm/res/cache/redis/main.bicep b/avm/res/cache/redis/main.bicep index 461bab33b7..6494bafab4 100644 --- a/avm/res/cache/redis/main.bicep +++ b/avm/res/cache/redis/main.bicep @@ -64,7 +64,7 @@ param replicasPerPrimary int = 3 @minValue(1) @description('Optional. The number of shards to be created on a Premium Cluster Cache.') -param shardCount int = 1 +param shardCount int? @allowed([ 0 diff --git a/avm/res/cache/redis/main.json b/avm/res/cache/redis/main.json index 10bffbc878..d63179ebfc 100644 --- a/avm/res/cache/redis/main.json +++ b/avm/res/cache/redis/main.json @@ -5,8 +5,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.33.13.18514", - "templateHash": "1242857667100916577" + "version": "0.33.93.31351", + "templateHash": "2636464673565813214" }, "name": "Redis Cache", "description": "This module deploys a Redis Cache." @@ -842,7 +842,7 @@ }, "shardCount": { "type": "int", - "defaultValue": 1, + "nullable": true, "minValue": 1, "metadata": { "description": "Optional. The number of shards to be created on a Premium Cluster Cache." @@ -1947,8 +1947,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.33.13.18514", - "templateHash": "11294861621866290910" + "version": "0.33.93.31351", + "templateHash": "322534394383651316" }, "name": "Redis Cache Linked Servers", "description": "This module connects a primary and secondary Redis Cache together for geo-replication." @@ -2073,8 +2073,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.33.13.18514", - "templateHash": "14045530027687796477" + "version": "0.33.93.31351", + "templateHash": "8063348652715653257" } }, "definitions": {