diff --git a/README.md b/README.md index 96c2368..8b1915d 100644 --- a/README.md +++ b/README.md @@ -6,11 +6,10 @@ Welcome to the Azure Stack HCI 20H2 Evaluation Guide In this guide, we'll walk you experiencing a number of the amazing capabilities within [Azure Stack HCI 20H2](https://azure.microsoft.com/en-us/products/azure-stack/hci/ "link to the Azure Stack HCI 20H2 landing page"), and set the foundation for you to explore in your own time. You'll cover aspects such as: -* Deploying Azure Stack HCI 20H2 nodes -* Deploying supporting management infrastructure, including Active Directory domain and management tooling -* Building a Hyperconverged cluster using Windows Admin Center, including configuring a cloud witness +* Building a hyperconverged Azure Stack HCI cluster using Windows Admin Center +* Configuring a cloud witness * Registering Azure Stack HCI 20H2 with Azure -* and more...! +* Creating storage volumes and deploying a VM Contents ----------- @@ -18,7 +17,12 @@ Contents - [Contents](#contents) - [What is Azure Stack HCI 20H2?](#what-is-azure-stack-hci-20h2) - [Why follow this guide?](#why-follow-this-guide) -- [Start your Azure Stack HCI 20H2 evaluation using nested virtualization](#start-your-azure-stack-hci-20h2-evaluation-using-nested-virtualization) +- [Interested in AKS on Azure Stack HCI?](#interested-in-aks-on-azure-stack-hci) +- [Evaluating in Azure](#evaluating-in-azure) +- [Nested Virtualization](#nested-virtualization) +- [Deployment of Azure Stack HCI 20H2 nested in Azure](#deployment-of-azure-stack-hci-20h2-nested-in-azure) +- [Deployment Workflow](#deployment-workflow) +- [Get started](#get-started) - [Product improvements](#product-improvements) - [Raising issues](#raising-issues) - [Contributions & Legal](#contributions--legal) @@ -35,25 +39,66 @@ If you're interested in learning more about what Azure Stack HCI 20H2 is, make s Why follow this guide? ----------- -This evaluation guide will walk you through standing up a sandboxed, isolated Azure Stack HCI 20H2 environment using **nested virtualization**, and can run on a **single physical system**, such as a workstation, laptop, or server of you have one, or alternatively, you can run the whole configuration in Azure. We'll go into more details for this path, shortly. - -The important takeaway here is, by following this guide, you'll lay down a solid foundation on to which you can explore additional Azure Stack HCI 20H2 scenarios in the future, so keep checking back for additional scenarios over time. +This evaluation guide will walk you through standing up a sandboxed, isolated Azure Stack HCI 20H2 environment using **nested virtualization** in a **single Azure VM**. The important takeaway here is, by following this guide, you'll lay down a solid foundation on to which you can explore additional Azure Stack HCI 20H2 scenarios in the future, so keep checking back for additional scenarios over time. Interested in AKS on Azure Stack HCI? ----------- If you're interested in evaluating AKS on Azure Stack HCI (AKS-HCI), and you're planning to evaluate all the solutions using nested virtualization in Azure, it's certainly tempting to run AKS-HCI on top of an Azure Stack HCI 20H2 nested cluster in an Azure VM, however we **strongly discourage** this approach due to the performance impact of multiple layers of nested virtualization. The recommended approach to test AKS-HCI in an Azure VM using [the official AKS on Azure Stack HCI eval guide](https://aka.ms/aks-hci-evalonazure "AKS on Azure Stack HCI eval guide"). -Start your Azure Stack HCI 20H2 evaluation using nested virtualization +Evaluating in Azure ----------- -If you have a single physical system, which could be a laptop, desktop, or server, or you have no spare hardware at all, using **nested virtualization** would be a great approach to experiencing Azure Stack HCI 20H2. You can get more details at the start of the nested path. +As with any infrastructure technology, in order to test, validate and evaluate the technology, there's typically a requirement for hardware. If you're fortunate enough to have multiple server-class pieces of hardware going spare (ideally hardware validated for Azure Stack HCI 20H2, found on our [Azure Stack HCI 20H2 Catalog](https://aka.ms/azurestackhcicatalog "Azure Stack HCI 20H2 Catalog")), you can certainly perform a more real-world evaluation of Azure Stack HCI 20H2. -![Nested path image](/media/nested.png "Nested virtualization path image") +For the purpose of this evaluation guide however, we'll be relying on **nested virtualization** to allow us to consolidate a full lab infrastructure, down **onto a single Hyper-V host inside an Azure VM**. -[**Evaluate Azure Stack HCI 20H2 using Nested Virtualization**](/nested/README.md "Evaluate Azure Stack HCI 20H2 using Nested Virtualization") +************************* ### Important Note - Production Deployments ### -The use of nested virtualization in this evaluation guide is aimed at providing flexibility for deploying Azure Stack HCI 20H2 in a lab, or test environment. For **production** use, **Azure Stack HCI 20H2 should be deployed on validated physical hardware**, of which you can find a vast array of choices on the [Azure Stack HCI 20H2 Catalog](https://aka.ms/azurestackhcicatalog "Azure Stack HCI 20H2 Catalog"). +The use of nested virtualization in this evaluation guide is aimed at providing flexibility for evaluating Azure Stack HCI 20H2. For **production** use, **Azure Stack HCI 20H2 should be deployed on validated physical hardware**, of which you can find a vast array of choices on the [Azure Stack HCI 20H2 Catalog](https://aka.ms/azurestackhcicatalog "Azure Stack HCI 20H2 Catalog"). + +************************* + +Nested Virtualization +----------- +If you're not familiar with Nested Virtualization, at a high level, it allows a virtualization platform, such as Hyper-V, or VMware ESXi, to run virtual machines that, within those virtual machines, run a virtualization platform. It may be easier to think about this in an architectural view. + +![Nested virtualization architecture](/deployment/media/nested_virt.png "Nested virtualization architecture") + +As you can see from the graphic, at the base layer, you have your physical hardware, onto which you install a hypervisor. In this case, for our example, we're using Windows Server 2019 with the Hyper-V role enabled. The hypervisor on the lowest level is considered L0 or the level 0 hypervisor. On that physical host, you create a virtual machine, and into that virtual machine, you deploy an OS that itself, has a hypervisor enabled. In this example, that 1st Virtualized Layer is running a **nested** Azure Stack HCI 20H2 operating system. This would be an L1 or level 1 hypervisor. Finally, in our example, inside the Azure Stack HCI 20H2 OS, you create a virtual machine to run a workload. This could in fact also contain a hypervisor, which would be known as the L2 or level 2 hypervisor, and so the process continues, with multiple levels of nested virtualization possible. + +The use of nested virtualization opens up amazing opportunities for building complex scenarios on significantly reduced hardware footprints, however it shouldn't be seen as a substitute for real-world deployments, performance and scale testing etc. + +Deployment of Azure Stack HCI 20H2 nested in Azure +----------- +For those of you who don't have multiple server-class pieces of hardware to test a full hyperconverged solution, this evaluation guide will detail using **nested virtualization** in Azure to evaluate Azure Stack HCI. + +![Architecture diagram for Azure Stack HCI 20H2 nested in Azure](/deployment/media/nested_virt_arch_ga_oct21.png "Architecture diagram for Azure Stack HCI 20H2 nested in Azure") + +In this configuration, you'll take advantage of the nested virtualization support provided within certain Azure VM sizes. You'll deploy a single Azure VM running Windows Server 2019 to act as your main Hyper-V host - and through PowerShell DSC, this will be automatically configured with the relevant roles and features needed for this guide. It will also download all required binaries, and deploy 2 Azure Stack HCI 20H2 nodes, ready for clustering. + +To reiterate, the whole configuration will run **inside the single Azure VM**. + +Deployment Workflow +----------- + +This guide will walk you through deploying a sandboxed infrastructure - the general flow will be as follows: + +**Part 1 - Complete the pre-requisites - deploy your Azure VM**: In this step, you'll create a VM in Azure using an Azure Resource Manager template. This VM will run Windows Server 2019 Datacenter, with the full desktop experience. PowerShell DSC will automatically configure this VM with the appropriate roles and features, download the necessary binaries, and configure 2 Azure Stack HCI 20H2 nodes, ready for clustering. + +**Part 2 - Configure your Azure Stack HCI 20H2 Cluster**: In this step, you'll use Windows Admin Center to deploy an Azure Stack HCI 20H2 cluster - along with a Cloud Witness, a Cluster Shared Volume, and finally, you'll register this cluster with Azure. + +**Part 3 - Integrate Azure Stack HCI 20H2 with Azure**: In this step, you'll use Windows Admin Center to register your Azure Stack HCI Cluster with Azure and explore what's presented in the portal + +**Part 4 - Explore the management of your Azure Stack HCI 20H2 environment**: With your deployment completed, you're now ready to explore many of the management aspects within the Windows Admin Center. + +Get started +----------- + +* [**Part 1** - Complete the prerequisites - deploy your Azure VM](/steps/1_DeployAzureVM.md "Complete the prerequisites - deploy your Azure VM") +* [**Part 2** - Configure your Azure Stack HCI 20H2 Cluster](/steps/2_DeployAzSHCI.md "Configure your Azure Stack HCI 20H2 Cluster") +* [**Part 3** - Integrate Azure Stack HCI 20H2 with Azure](/steps/3_AzSHCIIntegration.md "Integrate Azure Stack HCI 20H2 with Azure") +* [**Part 4** - Explore Azure Stack HCI Management](/steps/4_ExploreAzSHCI.md "Explore Azure Stack HCI Management") Product improvements ----------- @@ -85,4 +130,4 @@ Microsoft, Windows, Microsoft Azure and/or other Microsoft products and services Privacy information can be found at https://privacy.microsoft.com/en-us/ -Microsoft and any contributors reserve all other rights, whether under their respective copyrights, patents, or trademarks, whether by implication, estoppel or otherwise. +Microsoft and any contributors reserve all other rights, whether under their respective copyrights, patents, or trademarks, whether by implication, estoppel or otherwise. \ No newline at end of file diff --git a/nested/README.md b/archive/README.md similarity index 69% rename from nested/README.md rename to archive/README.md index 1f84ea4..63d7525 100644 --- a/nested/README.md +++ b/archive/README.md @@ -1,6 +1,21 @@ Evaluate Azure Stack HCI 20H2 using Nested Virtualization ============== +Important Note +----------- + +*********************** +This section of the eval guide has been **archived**. If you're looking to evaluate Azure Stack HCI in an Azure VM, the best approach is to use the latest version of the guidance, which you can find here: + +* [**Part 1** - Complete the prerequisites - deploy your Azure VM](/deployment/steps/1_DeployAzureVM.md "Complete the prerequisites - deploy your Azure VM") +* [**Part 2** - Configure your Azure Stack HCI 20H2 Cluster](/deployment/steps/2_DeployAzSHCI.md "Configure your Azure Stack HCI 20H2 Cluster") +* [**Part 3** - Integrate Azure Stack HCI 20H2 with Azure](/deployment/steps/3_AzSHCIIntegration.md "Integrate Azure Stack HCI 20H2 with Azure") +* [**Part 4** - Explore Azure Stack HCI Management](/deployment/steps/4_ExploreAzSHCI.md "Explore Azure Stack HCI Management") + +If you wish to evaluate Azure Stack HCI on a **single physical system**, you can use the guidance below. + +*********************** + As with any infrastructure technology, in order to test, validate and evaluate the technology, there's typically a requirement for hardware. If you're fortunate enough to have multiple server-class pieces of hardware going spare (ideally hardware validated for Azure Stack HCI 20H2, found on our [Azure Stack HCI 20H2 Catalog](https://aka.ms/azurestackhcicatalog "Azure Stack HCI 20H2 Catalog")), you can certainly perform a more real-world evaluation of Azure Stack HCI 20H2. For the purpose of this evaluation guide however, we'll be relying on **nested virtualization** to allow us to consolidate a full lab infrastructure, down onto a single Hyper-V host, either on-prem, or in Azure. If you do have spare physical hardware, you should be able to follow along and use your own hardware - you can just skip the nested-specific steps. @@ -10,9 +25,10 @@ The use of nested virtualization in this evaluation guide is aimed at providing Contents ----------- +- [Important Note](#important-note) - [Contents](#contents) - [Nested Virtualization](#nested-virtualization) -- [Deployment Options](#deployment-options) +- [Deployment](#deployment) - [Deployment Workflow](#deployment-workflow) - [Product improvements](#product-improvements) - [Raising issues](#raising-issues) @@ -21,74 +37,61 @@ Nested Virtualization ----------- If you're not familiar with Nested Virtualization, at a high level, it allows a virtualization platform, such as Hyper-V, or VMware ESXi, to run virtual machines that, within those virtual machines, run a virtualization platform. It may be easier to think about this in an architectural view. -![Nested virtualization architecture](/media/nested_virt.png "Nested virtualization architecture") +![Nested virtualization architecture](/archive/media/nested_virt.png "Nested virtualization architecture") As you can see from the graphic, at the base layer, you have your physical hardware, onto which you install a hypervisor. In this case, for our example, we're using Windows Server 2019 with the Hyper-V role enabled. The hypervisor on the lowest level is considered L0 or the level 0 hypervisor. On that physical host, you create a virtual machine, and into that virtual machine, you deploy an OS that itself, has a hypervisor enabled. In this example, that 1st Virtualized Layer is running a **nested** Azure Stack HCI 20H2 operating system. This would be an L1 or level 1 hypervisor. Finally, in our example, inside the Azure Stack HCI 20H2 OS, you create a virtual machine to run a workload. This could in fact also contain a hypervisor, which would be known as the L2 or level 2 hypervisor, and so the process continues, with multiple levels of nested virtualization possible. The use of nested virtualization opens up amazing opportunities for building complex scenarios on significantly reduced hardware footprints, however it shouldn't be seen as a substitute for real-world deployments, performance and scale testing etc. -Deployment Options +Deployment ----------- -For those of you who don't have multiple server-class pieces of hardware to test a full hyperconverged solution, this evaluation guide will detail 2 configurations, both using **nested virtualization**, that should be of interest. +For those of you who don't have multiple server-class pieces of hardware to test a full hyperconverged solution, this evaluation guide will detail how you can deploy on a single physical system, using **nested virtualization**. ### Deployment of Azure Stack HCI 20H2 nested on a physical system ### -![Architecture diagram for Azure Stack HCI 20H2 nested on a physical system](/media/nested_virt_physical_ga.png "Architecture diagram for Azure Stack HCI 20H2 nested on a physical system") +![Architecture diagram for Azure Stack HCI 20H2 nested on a physical system](/archive/media/nested_virt_physical_ga.png "Architecture diagram for Azure Stack HCI 20H2 nested on a physical system") In this configuration, you'll again take advantage of nested virtualization, but in this case, you'll deploy the whole solution on a single desktop/laptop/server. On your physical system, you'll run either Windows Server 2016/2019 or Windows 10 Pro/Enterprise/Education, with the Hyper-V role enabled. On Hyper-V, you'll deploy a sandbox infrastructure, consisting of a Windows Server 2019 domain controller VM, a management VM running Windows 10 Enterprise, and a nested Azure Stack HCI 20H2 cluster. #### Important note for systems with AMD CPUs #### For those of you wanting to evaluate Azure Stack HCI 20H2 in a nested configuration, with **AMD-based systems**, the only way this is currently possible is to use **Windows 10 Insider Build 19636 or newer** as your Hyper-V host. Your system should have AMD's 1st generation Ryzen/Epyc or newer CPUs. You can get more information on [nested virtualization on AMD here](https://techcommunity.microsoft.com/t5/virtualization/amd-nested-virtualization-support/ba-p/1434841 "Nested virtualization on AMD-based systems"). -If you can't run the Windows 10 Insider builds on your AMD-based system, it may be a better approach to [deploy in Azure instead](/nested/steps/1b_NestedInAzure.md "Deploy in Azure"). We'll be sure to update this guidance as and when new updates to nested virtualization support become available. - -### Deployment of Azure Stack HCI 20H2 nested in Azure ### - -![Architecture diagram for Azure Stack HCI 20H2 nested in Azure](/media/nested_virt_arch_ga.png "Architecture diagram for Azure Stack HCI 20H2 nested in Azure") - -In this configuration, you'll take advantage of the nested virtualization support provided within certain Azure VM sizes. You'll first deploy a single Azure VM running Windows Server 2019. Inside this VM, you'll enable the Hyper-V role, and deploy a Windows Server 2019 domain controller VM, along with a management VM, running Windows 10 Enterprise. This management VM will also run the Windows Admin Center. Finally, you'll deploy a nested Azure Stack HCI 20H2 cluster, with a minimum of 2 nodes, however the number of nodes will be based on the size of your Azure VM. - -To reiterate, the whole configuration (Domain Controller VM, Management VM and Azure Stack HCI 20H2 Nodes) will run inside the single Azure VM. +If you can't run the Windows 10 Insider builds on your AMD-based system, it may be a better approach to [deploy in Azure instead](/steps/1b_NestedInAzure.md "Deploy in Azure"). We'll be sure to update this guidance as and when new updates to nested virtualization support become available. Deployment Workflow ----------- -This guide will walk you through deploying a sandboxed Azure Stack HCI 20H2 infrastructure. Many of the steps will be universal, regardless of whether you are deploying in Azure, or deploying on a single physical system, however to accommodate different preferences, we've provided paths for those of you who prefer PowerShell, or GUI (Graphical User Interface, such as Hyper-V Manager, Server Manager etc)-based deployments. +This guide will walk you through deploying a sandboxed Azure Stack HCI 20H2 infrastructure. To accommodate different preferences, we've provided paths for those of you who prefer PowerShell, or GUI (Graphical User Interface, such as Hyper-V Manager, Server Manager etc)-based deployments. The general flow will be as follows: -![Evaluation guide workflow using nested virtualization](/media/flow_chart_ga.png "Evaluation guide workflow using nested virtualization") +![Evaluation guide workflow using nested virtualization](/archive/media/flow_chart_ga_oct21.png "Evaluation guide workflow using nested virtualization") -#### Part 1a - Deploy Hyper-V on a physical system #### +#### Part 1 - Deploy Hyper-V on a physical system #### In this step, on your existing system, that's running Windows Server 2016/2019 or Windows 10 Pro/Enterprise/Education, you'll enable the Hyper-V role and create a NAT virtual switch to enable network communication between sandbox VMs, and out to the internet. -* [**Part 1a** - Start your deployment on a physical system](/nested/steps/1a_NestedOnPhysical.md "Start your deployment on a physical system") - -#### Part 1b - Deploy Hyper-V host in Azure #### -In this step, you'll create a suitable VM in Azure using PowerShell or an Azure Resource Manager template. This VM will run Windows Server 2019 Datacenter, with the full desktop experience. On this system, you'll enable the Hyper-V role and accompanying management tools, and create a NAT virtual switch to enable network communication between sandbox VMs, and out to the internet. - -* [**Part 1b** - Start your deployment into Azure](/nested/steps/1b_NestedInAzure.md "Start your deployment into Azure") +* [**Part 1a** - Start your deployment on a physical system](/steps/1_NestedOnPhysical.md "Start your deployment on a physical system") #### Part 2 - Deploy management infrastructure #### In this step, you'll use **either the GUI, or PowerShell** to deploy and configure both a Windows Server 2019 domain controller, and a Windows 10 management VM on your Hyper-V host. You'll create a Windows Server 2019 Active Directory domain, and join the Windows 10 management VM to this domain. You'll also install the Windows Admin Center ahead of deploying the nested Azure Stack HCI 20H2 cluster. -* [**Part 2a** - Deploy your management infrastructure with the GUI](/nested/steps/2a_ManagementInfraGUI.md "Deploy your management infrastructure with the GUI") -* [**Part 2b** - Deploy your management infrastructure with PowerShell](/nested/steps/2b_ManagementInfraPS.md "Deploy your management infrastructure with PowerShell") +* [**Part 2a** - Deploy your management infrastructure with the GUI](/steps/2a_ManagementInfraGUI.md "Deploy your management infrastructure with the GUI") +* [**Part 2b** - Deploy your management infrastructure with PowerShell](/steps/2b_ManagementInfraPS.md "Deploy your management infrastructure with PowerShell") #### Part 3 - Deploy nested Azure Stack HCI 20H2 nodes #### In this step, you'll use **either the GUI or PowerShell** to create a number of nested Azure Stack HCI 20H2 nodes. -* [**Part 3a** - Create your nested Azure Stack HCI 20H2 nodes with the GUI](/nested/steps/3a_AzSHCINodesGUI.md "Create your nested Azure Stack HCI 20H2 nodes with the GUI") -* [**Part 3b** - Create your nested Azure Stack HCI 20H2 nodes with PowerShell](/nested/steps/3b_AzSHCINodesPS.md "Create your nested Azure Stack HCI 20H2 nodes with PowerShell") +* [**Part 3a** - Create your nested Azure Stack HCI 20H2 nodes with the GUI](/steps/3a_AzSHCINodesGUI.md "Create your nested Azure Stack HCI 20H2 nodes with the GUI") +* [**Part 3b** - Create your nested Azure Stack HCI 20H2 nodes with PowerShell](/steps/3b_AzSHCINodesPS.md "Create your nested Azure Stack HCI 20H2 nodes with PowerShell") #### Part 4 - Create your nested Azure Stack HCI 20H2 cluster #### In this step, you'll use the Windows Admin Center, on the Windows 10 management VM, to create the nested Azure Stack HCI 20H2 cluster, and perform some post-deployment tasks to validate the configuration. -* [**Part 4** - Create your nested Azure Stack HCI 20H2 cluster](/nested/steps/4_AzSHCICluster.md "Create your nested Azure Stack HCI 20H2 cluster") +* [**Part 4** - Create your nested Azure Stack HCI 20H2 cluster](/steps/4_AzSHCICluster.md "Create your nested Azure Stack HCI 20H2 cluster") #### Part 5 - Explore the management of your Azure Stack HCI 20H2 environment #### With your deployment completed, you're now ready to explore many of the management aspects within the Windows Admin Center. -* [**Part 5** - Explore the management of your Azure Stack HCI 20H2 environment](/nested/steps/5_ExploreAzSHCI.md "Explore the management of your Azure Stack HCI 20H2 environment") +* [**Part 5** - Explore the management of your Azure Stack HCI 20H2 environment](/steps/5_ExploreAzSHCI.md "Explore the management of your Azure Stack HCI 20H2 environment") Product improvements ----------- diff --git a/nested/json/azshcilabvm.json b/archive/json/azshcilabvm.json similarity index 100% rename from nested/json/azshcilabvm.json rename to archive/json/azshcilabvm.json diff --git a/media/Login-AzAccount.png b/archive/media/Login-AzAccount.png similarity index 100% rename from media/Login-AzAccount.png rename to archive/media/Login-AzAccount.png diff --git a/media/New-AzRoleAssignment.png b/archive/media/New-AzRoleAssignment.png similarity index 100% rename from media/New-AzRoleAssignment.png rename to archive/media/New-AzRoleAssignment.png diff --git a/media/New-AzRoleDefinition.png b/archive/media/New-AzRoleDefinition.png similarity index 100% rename from media/New-AzRoleDefinition.png rename to archive/media/New-AzRoleDefinition.png diff --git a/media/aad_permissions.png b/archive/media/aad_permissions.png similarity index 100% rename from media/aad_permissions.png rename to archive/media/aad_permissions.png diff --git a/media/add_nodes.png b/archive/media/add_nodes.png similarity index 100% rename from media/add_nodes.png rename to archive/media/add_nodes.png diff --git a/media/add_nodes_ga.png b/archive/media/add_nodes_ga.png similarity index 100% rename from media/add_nodes_ga.png rename to archive/media/add_nodes_ga.png diff --git a/media/adds_group.png b/archive/media/adds_group.png similarity index 100% rename from media/adds_group.png rename to archive/media/adds_group.png diff --git a/media/adds_new_user.png b/archive/media/adds_new_user.png similarity index 100% rename from media/adds_new_user.png rename to archive/media/adds_new_user.png diff --git a/media/adds_prereq.png b/archive/media/adds_prereq.png similarity index 100% rename from media/adds_prereq.png rename to archive/media/adds_prereq.png diff --git a/media/adds_wizard.png b/archive/media/adds_wizard.png similarity index 100% rename from media/adds_wizard.png rename to archive/media/adds_wizard.png diff --git a/media/api_permissions.png b/archive/media/api_permissions.png similarity index 100% rename from media/api_permissions.png rename to archive/media/api_permissions.png diff --git a/media/api_permissions_ga.png b/archive/media/api_permissions_ga.png similarity index 100% rename from media/api_permissions_ga.png rename to archive/media/api_permissions_ga.png diff --git a/media/auto_shutdown.png b/archive/media/auto_shutdown.png similarity index 100% rename from media/auto_shutdown.png rename to archive/media/auto_shutdown.png diff --git a/media/azshci_data_disk.png b/archive/media/azshci_data_disk.png similarity index 100% rename from media/azshci_data_disk.png rename to archive/media/azshci_data_disk.png diff --git a/media/azshci_disks_added.png b/archive/media/azshci_disks_added.png similarity index 100% rename from media/azshci_disks_added.png rename to archive/media/azshci_disks_added.png diff --git a/media/azshci_disks_added_ga.png b/archive/media/azshci_disks_added_ga.png similarity index 100% rename from media/azshci_disks_added_ga.png rename to archive/media/azshci_disks_added_ga.png diff --git a/media/azshci_laptop.png b/archive/media/azshci_laptop.png similarity index 100% rename from media/azshci_laptop.png rename to archive/media/azshci_laptop.png diff --git a/media/azshci_settings_ps.png b/archive/media/azshci_settings_ps.png similarity index 100% rename from media/azshci_settings_ps.png rename to archive/media/azshci_settings_ps.png diff --git a/media/azshci_settings_ps_ga.png b/archive/media/azshci_settings_ps_ga.png similarity index 100% rename from media/azshci_settings_ps_ga.png rename to archive/media/azshci_settings_ps_ga.png diff --git a/media/azshci_setup.png b/archive/media/azshci_setup.png similarity index 100% rename from media/azshci_setup.png rename to archive/media/azshci_setup.png diff --git a/media/azshci_setup_complete.png b/archive/media/azshci_setup_complete.png similarity index 100% rename from media/azshci_setup_complete.png rename to archive/media/azshci_setup_complete.png diff --git a/media/azure_ad_app.png b/archive/media/azure_ad_app.png similarity index 100% rename from media/azure_ad_app.png rename to archive/media/azure_ad_app.png diff --git a/media/azure_ad_app_ga.png b/archive/media/azure_ad_app_ga.png similarity index 100% rename from media/azure_ad_app_ga.png rename to archive/media/azure_ad_app_ga.png diff --git a/media/azure_arm.png b/archive/media/azure_arm.png similarity index 100% rename from media/azure_arm.png rename to archive/media/azure_arm.png diff --git a/media/azure_blob.png b/archive/media/azure_blob.png similarity index 100% rename from media/azure_blob.png rename to archive/media/azure_blob.png diff --git a/media/azure_blob_ga.png b/archive/media/azure_blob_ga.png similarity index 100% rename from media/azure_blob_ga.png rename to archive/media/azure_blob_ga.png diff --git a/media/azure_cloud_witness.png b/archive/media/azure_cloud_witness.png similarity index 100% rename from media/azure_cloud_witness.png rename to archive/media/azure_cloud_witness.png diff --git a/media/azure_cloud_witness_ga.png b/archive/media/azure_cloud_witness_ga.png similarity index 100% rename from media/azure_cloud_witness_ga.png rename to archive/media/azure_cloud_witness_ga.png diff --git a/media/azure_keys.png b/archive/media/azure_keys.png similarity index 100% rename from media/azure_keys.png rename to archive/media/azure_keys.png diff --git a/media/azure_keys_ga.png b/archive/media/azure_keys_ga.png similarity index 100% rename from media/azure_keys_ga.png rename to archive/media/azure_keys_ga.png diff --git a/media/azure_login_reg.png b/archive/media/azure_login_reg.png similarity index 100% rename from media/azure_login_reg.png rename to archive/media/azure_login_reg.png diff --git a/media/azure_portal_hcicluster.png b/archive/media/azure_portal_hcicluster.png similarity index 100% rename from media/azure_portal_hcicluster.png rename to archive/media/azure_portal_hcicluster.png diff --git a/media/azure_subscriptions.png b/archive/media/azure_subscriptions.png similarity index 100% rename from media/azure_subscriptions.png rename to archive/media/azure_subscriptions.png diff --git a/media/azure_subscriptions_ga.png b/archive/media/azure_subscriptions_ga.png similarity index 100% rename from media/azure_subscriptions_ga.png rename to archive/media/azure_subscriptions_ga.png diff --git a/media/azure_vm_custom_template.png b/archive/media/azure_vm_custom_template.png similarity index 100% rename from media/azure_vm_custom_template.png rename to archive/media/azure_vm_custom_template.png diff --git a/media/azure_vm_custom_template_complete.png b/archive/media/azure_vm_custom_template_complete.png similarity index 100% rename from media/azure_vm_custom_template_complete.png rename to archive/media/azure_vm_custom_template_complete.png diff --git a/media/azure_vm_search.png b/archive/media/azure_vm_search.png similarity index 100% rename from media/azure_vm_search.png rename to archive/media/azure_vm_search.png diff --git a/media/azure_vm_search_ga.png b/archive/media/azure_vm_search_ga.png similarity index 100% rename from media/azure_vm_search_ga.png rename to archive/media/azure_vm_search_ga.png diff --git a/media/boot_from_dvd.png b/archive/media/boot_from_dvd.png similarity index 100% rename from media/boot_from_dvd.png rename to archive/media/boot_from_dvd.png diff --git a/media/connect_to_mgmt01.png b/archive/media/connect_to_mgmt01.png similarity index 100% rename from media/connect_to_mgmt01.png rename to archive/media/connect_to_mgmt01.png diff --git a/media/connect_to_vm.png b/archive/media/connect_to_vm.png similarity index 100% rename from media/connect_to_vm.png rename to archive/media/connect_to_vm.png diff --git a/media/connect_to_vm_properties.png b/archive/media/connect_to_vm_properties.png similarity index 100% rename from media/connect_to_vm_properties.png rename to archive/media/connect_to_vm_properties.png diff --git a/media/dc_created.png b/archive/media/dc_created.png similarity index 100% rename from media/dc_created.png rename to archive/media/dc_created.png diff --git a/media/dc_install_progress.png b/archive/media/dc_install_progress.png similarity index 100% rename from media/dc_install_progress.png rename to archive/media/dc_install_progress.png diff --git a/media/dc_nic.png b/archive/media/dc_nic.png similarity index 100% rename from media/dc_nic.png rename to archive/media/dc_nic.png diff --git a/media/dhcp_complete.png b/archive/media/dhcp_complete.png similarity index 100% rename from media/dhcp_complete.png rename to archive/media/dhcp_complete.png diff --git a/media/dhcp_dns.png b/archive/media/dhcp_dns.png similarity index 100% rename from media/dhcp_dns.png rename to archive/media/dhcp_dns.png diff --git a/media/dhcp_enabled.png b/archive/media/dhcp_enabled.png similarity index 100% rename from media/dhcp_enabled.png rename to archive/media/dhcp_enabled.png diff --git a/media/dhcp_scope.png b/archive/media/dhcp_scope.png similarity index 100% rename from media/dhcp_scope.png rename to archive/media/dhcp_scope.png diff --git a/media/download_files.png b/archive/media/download_files.png similarity index 100% rename from media/download_files.png rename to archive/media/download_files.png diff --git a/media/dynamicmem.png b/archive/media/dynamicmem.png similarity index 100% rename from media/dynamicmem.png rename to archive/media/dynamicmem.png diff --git a/media/dynamicmem_mgmt01.png b/archive/media/dynamicmem_mgmt01.png similarity index 100% rename from media/dynamicmem_mgmt01.png rename to archive/media/dynamicmem_mgmt01.png diff --git a/media/enable_nested.png b/archive/media/enable_nested.png similarity index 100% rename from media/enable_nested.png rename to archive/media/enable_nested.png diff --git a/media/enhanced_session.png b/archive/media/enhanced_session.png similarity index 100% rename from media/enhanced_session.png rename to archive/media/enhanced_session.png diff --git a/media/extension_update.png b/archive/media/extension_update.png similarity index 100% rename from media/extension_update.png rename to archive/media/extension_update.png diff --git a/media/extension_update_needed.png b/archive/media/extension_update_needed.png similarity index 100% rename from media/extension_update_needed.png rename to archive/media/extension_update_needed.png diff --git a/media/flow_chart_ga.png b/archive/media/flow_chart_ga.png similarity index 100% rename from media/flow_chart_ga.png rename to archive/media/flow_chart_ga.png diff --git a/archive/media/flow_chart_ga_oct21.png b/archive/media/flow_chart_ga_oct21.png new file mode 100644 index 0000000..bdbc7e7 Binary files /dev/null and b/archive/media/flow_chart_ga_oct21.png differ diff --git a/media/flow_chart_paths_old.png b/archive/media/flow_chart_paths_old.png similarity index 100% rename from media/flow_chart_paths_old.png rename to archive/media/flow_chart_paths_old.png diff --git a/media/get_net_nat.png b/archive/media/get_net_nat.png similarity index 100% rename from media/get_net_nat.png rename to archive/media/get_net_nat.png diff --git a/media/ie_enhanced.png b/archive/media/ie_enhanced.png similarity index 100% rename from media/ie_enhanced.png rename to archive/media/ie_enhanced.png diff --git a/media/instaill_rsat.png b/archive/media/instaill_rsat.png similarity index 100% rename from media/instaill_rsat.png rename to archive/media/instaill_rsat.png diff --git a/media/ip_settings.PNG b/archive/media/ip_settings.PNG similarity index 100% rename from media/ip_settings.PNG rename to archive/media/ip_settings.PNG diff --git a/media/nested.png b/archive/media/nested.png similarity index 100% rename from media/nested.png rename to archive/media/nested.png diff --git a/media/nested_virt.png b/archive/media/nested_virt.png similarity index 100% rename from media/nested_virt.png rename to archive/media/nested_virt.png diff --git a/media/nested_virt_arch.png b/archive/media/nested_virt_arch.png similarity index 100% rename from media/nested_virt_arch.png rename to archive/media/nested_virt_arch.png diff --git a/media/nested_virt_arch_ga.png b/archive/media/nested_virt_arch_ga.png similarity index 100% rename from media/nested_virt_arch_ga.png rename to archive/media/nested_virt_arch_ga.png diff --git a/media/nested_virt_mgmt.png b/archive/media/nested_virt_mgmt.png similarity index 100% rename from media/nested_virt_mgmt.png rename to archive/media/nested_virt_mgmt.png diff --git a/media/nested_virt_mgmt_ga.png b/archive/media/nested_virt_mgmt_ga.png similarity index 100% rename from media/nested_virt_mgmt_ga.png rename to archive/media/nested_virt_mgmt_ga.png diff --git a/media/nested_virt_nodes.png b/archive/media/nested_virt_nodes.png similarity index 100% rename from media/nested_virt_nodes.png rename to archive/media/nested_virt_nodes.png diff --git a/media/nested_virt_nodes_ga.png b/archive/media/nested_virt_nodes_ga.png similarity index 100% rename from media/nested_virt_nodes_ga.png rename to archive/media/nested_virt_nodes_ga.png diff --git a/media/nested_virt_physical.png b/archive/media/nested_virt_physical.png similarity index 100% rename from media/nested_virt_physical.png rename to archive/media/nested_virt_physical.png diff --git a/media/nested_virt_physical_ga.png b/archive/media/nested_virt_physical_ga.png similarity index 100% rename from media/nested_virt_physical_ga.png rename to archive/media/nested_virt_physical_ga.png diff --git a/media/new_vm_dynamicmem.png b/archive/media/new_vm_dynamicmem.png similarity index 100% rename from media/new_vm_dynamicmem.png rename to archive/media/new_vm_dynamicmem.png diff --git a/media/new_vm_mgmt01.png b/archive/media/new_vm_mgmt01.png similarity index 100% rename from media/new_vm_mgmt01.png rename to archive/media/new_vm_mgmt01.png diff --git a/media/new_vm_mgmt01_vhd.png b/archive/media/new_vm_mgmt01_vhd.png similarity index 100% rename from media/new_vm_mgmt01_vhd.png rename to archive/media/new_vm_mgmt01_vhd.png diff --git a/media/new_vm_mgmt01_vhd_ga.png b/archive/media/new_vm_mgmt01_vhd_ga.png similarity index 100% rename from media/new_vm_mgmt01_vhd_ga.png rename to archive/media/new_vm_mgmt01_vhd_ga.png diff --git a/media/new_vm_name.png b/archive/media/new_vm_name.png similarity index 100% rename from media/new_vm_name.png rename to archive/media/new_vm_name.png diff --git a/media/new_vm_node.png b/archive/media/new_vm_node.png similarity index 100% rename from media/new_vm_node.png rename to archive/media/new_vm_node.png diff --git a/media/new_vm_node_memory.png b/archive/media/new_vm_node_memory.png similarity index 100% rename from media/new_vm_node_memory.png rename to archive/media/new_vm_node_memory.png diff --git a/media/new_vm_node_memory_ga.png b/archive/media/new_vm_node_memory_ga.png similarity index 100% rename from media/new_vm_node_memory_ga.png rename to archive/media/new_vm_node_memory_ga.png diff --git a/media/new_vm_node_settings.png b/archive/media/new_vm_node_settings.png similarity index 100% rename from media/new_vm_node_settings.png rename to archive/media/new_vm_node_settings.png diff --git a/media/new_vm_node_settings_ga.png b/archive/media/new_vm_node_settings_ga.png similarity index 100% rename from media/new_vm_node_settings_ga.png rename to archive/media/new_vm_node_settings_ga.png diff --git a/media/new_vm_node_vhd.png b/archive/media/new_vm_node_vhd.png similarity index 100% rename from media/new_vm_node_vhd.png rename to archive/media/new_vm_node_vhd.png diff --git a/media/new_vm_vhd.png b/archive/media/new_vm_vhd.png similarity index 100% rename from media/new_vm_vhd.png rename to archive/media/new_vm_vhd.png diff --git a/media/nic_adapter.png b/archive/media/nic_adapter.png similarity index 100% rename from media/nic_adapter.png rename to archive/media/nic_adapter.png diff --git a/media/node_ipconfig.png b/archive/media/node_ipconfig.png similarity index 100% rename from media/node_ipconfig.png rename to archive/media/node_ipconfig.png diff --git a/media/node_turned_off.png b/archive/media/node_turned_off.png similarity index 100% rename from media/node_turned_off.png rename to archive/media/node_turned_off.png diff --git a/media/physical.png b/archive/media/physical.png similarity index 100% rename from media/physical.png rename to archive/media/physical.png diff --git a/media/powershell_vm_deployed.png b/archive/media/powershell_vm_deployed.png similarity index 100% rename from media/powershell_vm_deployed.png rename to archive/media/powershell_vm_deployed.png diff --git a/media/reg_check.png b/archive/media/reg_check.png similarity index 100% rename from media/reg_check.png rename to archive/media/reg_check.png diff --git a/media/reg_check_ga.png b/archive/media/reg_check_ga.png similarity index 100% rename from media/reg_check_ga.png rename to archive/media/reg_check_ga.png diff --git a/media/register_azshci.png b/archive/media/register_azshci.png similarity index 100% rename from media/register_azshci.png rename to archive/media/register_azshci.png diff --git a/media/register_azshci_ga.png b/archive/media/register_azshci_ga.png similarity index 100% rename from media/register_azshci_ga.png rename to archive/media/register_azshci_ga.png diff --git a/media/registration_record.png b/archive/media/registration_record.png similarity index 100% rename from media/registration_record.png rename to archive/media/registration_record.png diff --git a/media/registration_rg.png b/archive/media/registration_rg.png similarity index 100% rename from media/registration_rg.png rename to archive/media/registration_rg.png diff --git a/media/registration_rg_ga.png b/archive/media/registration_rg_ga.png similarity index 100% rename from media/registration_rg_ga.png rename to archive/media/registration_rg_ga.png diff --git a/media/registration_status.png b/archive/media/registration_status.png similarity index 100% rename from media/registration_status.png rename to archive/media/registration_status.png diff --git a/media/sconfig.png b/archive/media/sconfig.png similarity index 100% rename from media/sconfig.png rename to archive/media/sconfig.png diff --git a/media/sconfig_nic.png b/archive/media/sconfig_nic.png similarity index 100% rename from media/sconfig_nic.png rename to archive/media/sconfig_nic.png diff --git a/media/startvm.png b/archive/media/startvm.png similarity index 100% rename from media/startvm.png rename to archive/media/startvm.png diff --git a/media/startvm_mgmt01.png b/archive/media/startvm_mgmt01.png similarity index 100% rename from media/startvm_mgmt01.png rename to archive/media/startvm_mgmt01.png diff --git a/media/sysdm.png b/archive/media/sysdm.png similarity index 100% rename from media/sysdm.png rename to archive/media/sysdm.png diff --git a/media/systeminfo_upd.png b/archive/media/systeminfo_upd.png similarity index 100% rename from media/systeminfo_upd.png rename to archive/media/systeminfo_upd.png diff --git a/media/vm_connect.png b/archive/media/vm_connect.png similarity index 100% rename from media/vm_connect.png rename to archive/media/vm_connect.png diff --git a/media/vm_connect_ga.png b/archive/media/vm_connect_ga.png similarity index 100% rename from media/vm_connect_ga.png rename to archive/media/vm_connect_ga.png diff --git a/media/w10_install_complete.png b/archive/media/w10_install_complete.png similarity index 100% rename from media/w10_install_complete.png rename to archive/media/w10_install_complete.png diff --git a/media/w10_setup.png b/archive/media/w10_setup.png similarity index 100% rename from media/w10_setup.png rename to archive/media/w10_setup.png diff --git a/media/wac_azshciclus.png b/archive/media/wac_azshciclus.png similarity index 100% rename from media/wac_azshciclus.png rename to archive/media/wac_azshciclus.png diff --git a/media/wac_azshciclus_ga.png b/archive/media/wac_azshciclus_ga.png similarity index 100% rename from media/wac_azshciclus_ga.png rename to archive/media/wac_azshciclus_ga.png diff --git a/media/wac_azure_connect.png b/archive/media/wac_azure_connect.png similarity index 100% rename from media/wac_azure_connect.png rename to archive/media/wac_azure_connect.png diff --git a/media/wac_azure_key.png b/archive/media/wac_azure_key.png similarity index 100% rename from media/wac_azure_key.png rename to archive/media/wac_azure_key.png diff --git a/media/wac_azure_key_ga.png b/archive/media/wac_azure_key_ga.png similarity index 100% rename from media/wac_azure_key_ga.png rename to archive/media/wac_azure_key_ga.png diff --git a/media/wac_azure_permissions.png b/archive/media/wac_azure_permissions.png similarity index 100% rename from media/wac_azure_permissions.png rename to archive/media/wac_azure_permissions.png diff --git a/media/wac_azure_reg_dashboard.png b/archive/media/wac_azure_reg_dashboard.png similarity index 100% rename from media/wac_azure_reg_dashboard.png rename to archive/media/wac_azure_reg_dashboard.png diff --git a/media/wac_azure_reg_dashboard_2.png b/archive/media/wac_azure_reg_dashboard_2.png similarity index 100% rename from media/wac_azure_reg_dashboard_2.png rename to archive/media/wac_azure_reg_dashboard_2.png diff --git a/media/wac_azure_reg_dashboard_3.png b/archive/media/wac_azure_reg_dashboard_3.png similarity index 100% rename from media/wac_azure_reg_dashboard_3.png rename to archive/media/wac_azure_reg_dashboard_3.png diff --git a/media/wac_azure_register.png b/archive/media/wac_azure_register.png similarity index 100% rename from media/wac_azure_register.png rename to archive/media/wac_azure_register.png diff --git a/media/wac_check_drives_ga.png b/archive/media/wac_check_drives_ga.png similarity index 100% rename from media/wac_check_drives_ga.png rename to archive/media/wac_check_drives_ga.png diff --git a/media/wac_clean_drives.png b/archive/media/wac_clean_drives.png similarity index 100% rename from media/wac_clean_drives.png rename to archive/media/wac_clean_drives.png diff --git a/media/wac_clean_drives_ga.png b/archive/media/wac_clean_drives_ga.png similarity index 100% rename from media/wac_clean_drives_ga.png rename to archive/media/wac_clean_drives_ga.png diff --git a/media/wac_cloud_witness_new.png b/archive/media/wac_cloud_witness_new.png similarity index 100% rename from media/wac_cloud_witness_new.png rename to archive/media/wac_cloud_witness_new.png diff --git a/media/wac_cloud_witness_new_ga.png b/archive/media/wac_cloud_witness_new_ga.png similarity index 100% rename from media/wac_cloud_witness_new_ga.png rename to archive/media/wac_cloud_witness_new_ga.png diff --git a/media/wac_cluster_success.png b/archive/media/wac_cluster_success.png similarity index 100% rename from media/wac_cluster_success.png rename to archive/media/wac_cluster_success.png diff --git a/media/wac_cluster_success_ga.png b/archive/media/wac_cluster_success_ga.png similarity index 100% rename from media/wac_cluster_success_ga.png rename to archive/media/wac_cluster_success_ga.png diff --git a/media/wac_cluster_type.png b/archive/media/wac_cluster_type.png similarity index 100% rename from media/wac_cluster_type.png rename to archive/media/wac_cluster_type.png diff --git a/media/wac_cluster_type_ga.png b/archive/media/wac_cluster_type_ga.png similarity index 100% rename from media/wac_cluster_type_ga.png rename to archive/media/wac_cluster_type_ga.png diff --git a/media/wac_compute_vswitch.png b/archive/media/wac_compute_vswitch.png similarity index 100% rename from media/wac_compute_vswitch.png rename to archive/media/wac_compute_vswitch.png diff --git a/media/wac_compute_vswitch_ga.png b/archive/media/wac_compute_vswitch_ga.png similarity index 100% rename from media/wac_compute_vswitch_ga.png rename to archive/media/wac_compute_vswitch_ga.png diff --git a/media/wac_create_clus.png b/archive/media/wac_create_clus.png similarity index 100% rename from media/wac_create_clus.png rename to archive/media/wac_create_clus.png diff --git a/media/wac_create_clus_ga.png b/archive/media/wac_create_clus_ga.png similarity index 100% rename from media/wac_create_clus_ga.png rename to archive/media/wac_create_clus_ga.png diff --git a/media/wac_credssp.png b/archive/media/wac_credssp.png similarity index 100% rename from media/wac_credssp.png rename to archive/media/wac_credssp.png diff --git a/media/wac_credssp_ga.png b/archive/media/wac_credssp_ga.png similarity index 100% rename from media/wac_credssp_ga.png rename to archive/media/wac_credssp_ga.png diff --git a/media/wac_define_network.png b/archive/media/wac_define_network.png similarity index 100% rename from media/wac_define_network.png rename to archive/media/wac_define_network.png diff --git a/media/wac_define_network_ga.png b/archive/media/wac_define_network_ga.png similarity index 100% rename from media/wac_define_network_ga.png rename to archive/media/wac_define_network_ga.png diff --git a/media/wac_domain_joined.png b/archive/media/wac_domain_joined.png similarity index 100% rename from media/wac_domain_joined.png rename to archive/media/wac_domain_joined.png diff --git a/media/wac_domain_joined_ga.png b/archive/media/wac_domain_joined_ga.png similarity index 100% rename from media/wac_domain_joined_ga.png rename to archive/media/wac_domain_joined_ga.png diff --git a/media/wac_enable_dedup.png b/archive/media/wac_enable_dedup.png similarity index 100% rename from media/wac_enable_dedup.png rename to archive/media/wac_enable_dedup.png diff --git a/media/wac_enable_dedup_ga.png b/archive/media/wac_enable_dedup_ga.png similarity index 100% rename from media/wac_enable_dedup_ga.png rename to archive/media/wac_enable_dedup_ga.png diff --git a/media/wac_enable_rdma.png b/archive/media/wac_enable_rdma.png similarity index 100% rename from media/wac_enable_rdma.png rename to archive/media/wac_enable_rdma.png diff --git a/media/wac_installed.png b/archive/media/wac_installed.png similarity index 100% rename from media/wac_installed.png rename to archive/media/wac_installed.png diff --git a/media/wac_installed_features.png b/archive/media/wac_installed_features.png similarity index 100% rename from media/wac_installed_features.png rename to archive/media/wac_installed_features.png diff --git a/media/wac_installed_features_ga.png b/archive/media/wac_installed_features_ga.png similarity index 100% rename from media/wac_installed_features_ga.png rename to archive/media/wac_installed_features_ga.png diff --git a/media/wac_management_nic.png b/archive/media/wac_management_nic.png similarity index 100% rename from media/wac_management_nic.png rename to archive/media/wac_management_nic.png diff --git a/media/wac_management_nic_ga.png b/archive/media/wac_management_nic_ga.png similarity index 100% rename from media/wac_management_nic_ga.png rename to archive/media/wac_management_nic_ga.png diff --git a/media/wac_move.png b/archive/media/wac_move.png similarity index 100% rename from media/wac_move.png rename to archive/media/wac_move.png diff --git a/media/wac_move_ga.png b/archive/media/wac_move_ga.png similarity index 100% rename from media/wac_move_ga.png rename to archive/media/wac_move_ga.png diff --git a/media/wac_nic_selection.png b/archive/media/wac_nic_selection.png similarity index 100% rename from media/wac_nic_selection.png rename to archive/media/wac_nic_selection.png diff --git a/media/wac_restart.png b/archive/media/wac_restart.png similarity index 100% rename from media/wac_restart.png rename to archive/media/wac_restart.png diff --git a/media/wac_restart_ga.png b/archive/media/wac_restart_ga.png similarity index 100% rename from media/wac_restart_ga.png rename to archive/media/wac_restart_ga.png diff --git a/media/wac_s2d_enabled.png b/archive/media/wac_s2d_enabled.png similarity index 100% rename from media/wac_s2d_enabled.png rename to archive/media/wac_s2d_enabled.png diff --git a/media/wac_s2d_enabled_ga.png b/archive/media/wac_s2d_enabled_ga.png similarity index 100% rename from media/wac_s2d_enabled_ga.png rename to archive/media/wac_s2d_enabled_ga.png diff --git a/media/wac_singlemgmt.png b/archive/media/wac_singlemgmt.png similarity index 100% rename from media/wac_singlemgmt.png rename to archive/media/wac_singlemgmt.png diff --git a/media/wac_singlemgmt_ga.png b/archive/media/wac_singlemgmt_ga.png similarity index 100% rename from media/wac_singlemgmt_ga.png rename to archive/media/wac_singlemgmt_ga.png diff --git a/media/wac_storage_validated.png b/archive/media/wac_storage_validated.png similarity index 100% rename from media/wac_storage_validated.png rename to archive/media/wac_storage_validated.png diff --git a/media/wac_storage_validated_ga.png b/archive/media/wac_storage_validated_ga.png similarity index 100% rename from media/wac_storage_validated_ga.png rename to archive/media/wac_storage_validated_ga.png diff --git a/media/wac_validate_storage.png b/archive/media/wac_validate_storage.png similarity index 100% rename from media/wac_validate_storage.png rename to archive/media/wac_validate_storage.png diff --git a/media/wac_validate_storage_ga.png b/archive/media/wac_validate_storage_ga.png similarity index 100% rename from media/wac_validate_storage_ga.png rename to archive/media/wac_validate_storage_ga.png diff --git a/media/wac_validated.png b/archive/media/wac_validated.png similarity index 100% rename from media/wac_validated.png rename to archive/media/wac_validated.png diff --git a/media/wac_validated_ga.png b/archive/media/wac_validated_ga.png similarity index 100% rename from media/wac_validated_ga.png rename to archive/media/wac_validated_ga.png diff --git a/media/wac_verify_drives.png b/archive/media/wac_verify_drives.png similarity index 100% rename from media/wac_verify_drives.png rename to archive/media/wac_verify_drives.png diff --git a/media/wac_verify_network.png b/archive/media/wac_verify_network.png similarity index 100% rename from media/wac_verify_network.png rename to archive/media/wac_verify_network.png diff --git a/media/wac_verify_network_ga.png b/archive/media/wac_verify_network_ga.png similarity index 100% rename from media/wac_verify_network_ga.png rename to archive/media/wac_verify_network_ga.png diff --git a/media/wac_vm001.png b/archive/media/wac_vm001.png similarity index 100% rename from media/wac_vm001.png rename to archive/media/wac_vm001.png diff --git a/media/wac_vm001_ga.png b/archive/media/wac_vm001_ga.png similarity index 100% rename from media/wac_vm001_ga.png rename to archive/media/wac_vm001_ga.png diff --git a/media/wac_vm_storage.png b/archive/media/wac_vm_storage.png similarity index 100% rename from media/wac_vm_storage.png rename to archive/media/wac_vm_storage.png diff --git a/media/wac_vm_storage_deployed.png b/archive/media/wac_vm_storage_deployed.png similarity index 100% rename from media/wac_vm_storage_deployed.png rename to archive/media/wac_vm_storage_deployed.png diff --git a/media/wac_vm_storage_deployed_ga.png b/archive/media/wac_vm_storage_deployed_ga.png similarity index 100% rename from media/wac_vm_storage_deployed_ga.png rename to archive/media/wac_vm_storage_deployed_ga.png diff --git a/media/wac_vm_storage_ga.png b/archive/media/wac_vm_storage_ga.png similarity index 100% rename from media/wac_vm_storage_ga.png rename to archive/media/wac_vm_storage_ga.png diff --git a/media/wac_vswitches.png b/archive/media/wac_vswitches.png similarity index 100% rename from media/wac_vswitches.png rename to archive/media/wac_vswitches.png diff --git a/media/wac_vswitches_ga.png b/archive/media/wac_vswitches_ga.png similarity index 100% rename from media/wac_vswitches_ga.png rename to archive/media/wac_vswitches_ga.png diff --git a/media/ws_install_complete.png b/archive/media/ws_install_complete.png similarity index 100% rename from media/ws_install_complete.png rename to archive/media/ws_install_complete.png diff --git a/media/ws_setup.png b/archive/media/ws_setup.png similarity index 100% rename from media/ws_setup.png rename to archive/media/ws_setup.png diff --git a/nested/steps/1a_NestedOnPhysical.md b/archive/steps/1_NestedOnPhysical.md similarity index 96% rename from nested/steps/1a_NestedOnPhysical.md rename to archive/steps/1_NestedOnPhysical.md index ebbfccb..f1f6205 100644 --- a/nested/steps/1a_NestedOnPhysical.md +++ b/archive/steps/1_NestedOnPhysical.md @@ -28,7 +28,7 @@ Architecture From an architecture perspective, the following graphic showcases the different layers and interconnections between the different components: -![Architecture diagram for Azure Stack HCI 20H2 nested on a physical system](/media/nested_virt_physical_ga.png "Architecture diagram for Azure Stack HCI 20H2 nested on a physical system") +![Architecture diagram for Azure Stack HCI 20H2 nested on a physical system](/archive/media/nested_virt_physical_ga.png "Architecture diagram for Azure Stack HCI 20H2 nested on a physical system") Will my hardware support this? ----------- @@ -52,7 +52,7 @@ If you can't run the Windows 10 Insider builds on your AMD-based system, it may ### Verify Hardware Compatibility ### After checking the operating system and hardware requirements above, verify hardware compatibility in Windows by opening a PowerShell session or a command prompt (cmd.exe) window, typing **systeminfo**, and then checking the Hyper-V Requirements section. If all listed Hyper-V requirements have a value of **Yes**, your system can run the Hyper-V role. If any item returns No, check the requirements above and make adjustments where possible. -![Hyper-V requirements](/media/systeminfo_upd.png "Hyper-V requirements") +![Hyper-V requirements](/archive/media/systeminfo_upd.png "Hyper-V requirements") If you run **systeminfo** on an existing Hyper-V host, the Hyper-V Requirements section reads: @@ -64,7 +64,7 @@ With 16GB memory, running on a laptop, we'll need to ensure that we're taking ad **NOTE** When you configure your nested Azure Stack HCI 20H2 nodes later, they will **require a minimum of 4GB RAM per node**, otherwise, they won't boot, so on a 16GB system, expect 2-3 nodes plus management infrastructure realistically - if you see the screenshot below, on my 16GB laptop, you'll see 2 Azure Stack HCI 20H2 nodes, with DC01/MGMT01, with a little memory left over for the host. -![Azure Stack HCI 20H2 cluster running on a laptop](/media/azshci_laptop.png "Azure Stack HCI 20H2 cluster running on a laptop") +![Azure Stack HCI 20H2 cluster running on a laptop](/archive/media/azshci_laptop.png "Azure Stack HCI 20H2 cluster running on a laptop") Obviously, if you have a larger physical system, such as a workstation, or server, you'll likely have a greater amount of memory available to you, therefore you can adjust the memory levels for the different resources accordingly. @@ -149,7 +149,7 @@ Get-NetNat The **Get-NetNat** cmdlet gets Network Address Translation (NAT) objects configured on a computer. NAT modifies IP address and port information in packet headers. Your configuration should look similar to the configuration below: -![Result of Get-NetNat PowerShell command](/media/get_net_nat.png "Result of Get-NetNat PowerShell command") +![Result of Get-NetNat PowerShell command](/archive/media/get_net_nat.png "Result of Get-NetNat PowerShell command") The final part of the process is to enable Enhanced Session mode. Enhanced Session mode can be useful to enhance the user experience, particularly when using the Windows 10 Management VM later, when connecting to a VM over VMConnect. To enable Enhanced Session Mode with PowerShell, run the following on your Hyper-V host: diff --git a/nested/steps/2a_ManagementInfraGUI.md b/archive/steps/2a_ManagementInfraGUI.md similarity index 88% rename from nested/steps/2a_ManagementInfraGUI.md rename to archive/steps/2a_ManagementInfraGUI.md index 74a9b38..88410af 100644 --- a/nested/steps/2a_ManagementInfraGUI.md +++ b/archive/steps/2a_ManagementInfraGUI.md @@ -2,7 +2,7 @@ Deploy management infrastructure with the GUI ============== Overview ----------- -With your Hyper-V host up and running, either in Azure, or on a local physical system, it's now time to deploy the core management infrastructure to support the Azure Stack HCI 20H2 deployment in a future step. +With your Hyper-V host up and running, it's now time to deploy the core management infrastructure to support the Azure Stack HCI 20H2 deployment in a future step. ### Important Note ### In this step, you'll be using the GUI (Graphical User Interface, such as Hyper-V Manager, Server Manager etc) to create resources. If you prefer to use PowerShell, which may allow faster completion, head on over to the [PowerShell guide](/nested/steps/2b_ManagementInfraPS.md). @@ -24,7 +24,7 @@ Architecture As shown on the architecture graphic below, the core management infrastructure consists of a Windows Server 2019 domain controller VM, along with a Windows 10 Enterprise VM, which will run the Windows Admin Center. In this step, you'll deploy both of those key components. -![Architecture diagram for Azure Stack HCI 20H2 nested with management infra highlighted](/media/nested_virt_mgmt_ga.png "Architecture diagram for Azure Stack HCI 20H2 nested with management infra highlighted") +![Architecture diagram for Azure Stack HCI 20H2 nested with management infra highlighted](/archive/media/nested_virt_mgmt_ga.png "Architecture diagram for Azure Stack HCI 20H2 nested with management infra highlighted") However, before you deploy your management infrastructure, first, you need to download the necessary software components required to complete this evalution. @@ -52,7 +52,7 @@ If you're running Windows Server 2019 as your Hyper-V host, it doesn't ship with 3. In the **Properties** view, find the **IE Enhanced Security Configuration** item, and click on **On** 4. In the **Internet Explorer Enhanced Security Configuration** window, under **Administrators**, click **Off** and click **OK** -![Setting the Internet Explorer Enhanced Security Configuration to Off](/media/ie_enhanced.png "Setting the Internet Explorer Enhanced Security Configuration to Off") +![Setting the Internet Explorer Enhanced Security Configuration to Off](/archive/media/ie_enhanced.png "Setting the Internet Explorer Enhanced Security Configuration to Off") 5. Close **Server Manager** @@ -64,7 +64,7 @@ Next, in order to download the ISO files, **open your web browser** and follow t 3. Visit https://azure.microsoft.com/en-us/products/azure-stack/hci/hci-download, complete the registration form, and download the ISO. Save the file as **AzSHCI.iso** to C:\ISO 4. Visit https://aka.ms/wacdownload to download the executables for the Windows Admin Center. Save it as **WindowsAdminCenter.msi**, also in C:\ISO -![All files have been downloaded onto your Hyper-V host](/media/download_files.png "All files have been downloaded onto your Hyper-V host") +![All files have been downloaded onto your Hyper-V host](/archive/media/download_files.png "All files have been downloaded onto your Hyper-V host") With all files downloaded, proceed on to creating your management infrastructure. @@ -86,17 +86,17 @@ In this step, you'll be using Hyper-V Manager to deploy a Windows Server 2019 do 5. Tick the box for **Store the virtual machine in a different location** and click **Browse** 6. In the **Select Folder** window, click on **This PC**, navigate to **C:**, click on **New Folder**, name it **VMs** then click **Select Folder** and click **Next** -![Specify VM name and location](/media/new_vm_name.png "Specify VM name and location") +![Specify VM name and location](/archive/media/new_vm_name.png "Specify VM name and location") 7. On the **Specify Generation** page, select **Generation 2** and click **Next** 8. On the **Assign Memory** page, assign 4GB memory by entering **4096** for Startup memory and tick the **Use Dynamic Memory for this virtual machine**, then click **Next** -![Assign VM memory](/media/new_vm_dynamicmem.png "Assign VM memory") +![Assign VM memory](/archive/media/new_vm_dynamicmem.png "Assign VM memory") 9. On the **Configure Networking** page, select **InternalNAT** and click **Next** 10. On the **Connect Virtual Hard Disk** page, set the **size** to **30** and click **Next** -![Connect Virtual Hard Disk](/media/new_vm_vhd.png "Connect Virtual Hard Disk") +![Connect Virtual Hard Disk](/archive/media/new_vm_vhd.png "Connect Virtual Hard Disk") 11. On the **Installation Options** page, select **Install an operating system from a bootable image file**, and click **Browse** 12. Navigate to **C:\ISO** and select your **WS2019.iso** file, and click **Open**. Then click **Next** @@ -109,23 +109,23 @@ Your new DC01 virtual machine will now be created. Once created, we need to mak * Minimum RAM: 1024 * Maximum RAM: 4096 -![Updating memory for DC01](/media/dynamicmem.png "Updating memory for DC01") +![Updating memory for DC01](/archive/media/dynamicmem.png "Updating memory for DC01") 3. If you are running on a **Windows 10 Hyper-V host**, you should **disable automatic checkpoints**. From the **Settings** window, under **Management**, click **Checkpoints** and then if ticked, **untick** the **Enable checkpoints** box, then click **Apply** 4. Finally, under **Automatic Start Action**, select **Always start this virtual machine automatically**, then click **OK** With the VM configured correctly, in **Hyper-V Manager**, double-click DC01. This should open the VM Connect window. -![Starting up DC01](/media/startvm.png "Starting up DC01") +![Starting up DC01](/archive/media/startvm.png "Starting up DC01") In the center of the window, there is a message explaining the VM is currently switched off. Click on **Start** and then quickly **press any key** inside the VM to boot from the ISO file. If you miss the prompt to press a key to boot from CD or DVD, simply reset the VM and try again. -![Booting the VM and triggering the boot from DVD](/media/boot_from_dvd.png "Booting the VM and triggering the boot from DVD") +![Booting the VM and triggering the boot from DVD](/archive/media/boot_from_dvd.png "Booting the VM and triggering the boot from DVD") ### Complete the Out of Box Experience (OOBE) ### With the VM running, and the boot process initiated, you should be in a position to start the deployment of the Windows Server 2019 OS. -![Initiate setup of the Windows Server 2019 OS](/media/ws_setup.png "Initiate setup of the Windows Server 2019 OS") +![Initiate setup of the Windows Server 2019 OS](/archive/media/ws_setup.png "Initiate setup of the Windows Server 2019 OS") Proceed through the process, making the following selections: @@ -161,7 +161,7 @@ Firstly, you will configure the networking inside the VM and rename the OS, befo * Preferred DNS server: 1.1.1.1 * Alternate DNS server: 1.0.0.1 -![Network settings for DC01](/media/dc_nic.png "Network settings for DC01") +![Network settings for DC01](/archive/media/dc_nic.png "Network settings for DC01") #### Optional - Update DC01 with latest Windows Updates #### If you'd like to ensure DC01 is fully updated, click on **Start**, search for **Updates** and select **Check for Updates** in the results. Check for any new updates and install any that are required. This may take a few minutes. @@ -186,13 +186,13 @@ First, you'll configure Active Directory: 11. On the **DNS Server** page, click **Next** 12. On the **Confirmation** page, review the information and click **Install** -![Active Directory Domain Services installation progress](/media/dc_install_progress.png "Active Directory Domain Services installation progress") +![Active Directory Domain Services installation progress](/archive/media/dc_install_progress.png "Active Directory Domain Services installation progress") The installation of Active Directory Domain Services will begin, and take a few moments to complete. Once complete, click **Promote this server to a domain controller** to continue the configuration of DC01. The **Active Directory Domain Services Configuration Wizard** should open. 1. On the **Deployment configuration** page, select **Add a new forest**, enter **azshci.local** as the Root domain name, and click **Next** -![Active Directory Domain Services configuration wizard](/media/adds_wizard.png "Active Directory Domain Services configuration wizard") +![Active Directory Domain Services configuration wizard](/archive/media/adds_wizard.png "Active Directory Domain Services configuration wizard") 2. On the **Domain Controller options** page, leave the defaults, provide a **Directory Services Restore Mode (DSRM) password**, then click **Next** 3. On the **DNS Options** page, click **Next** @@ -202,7 +202,7 @@ The installation of Active Directory Domain Services will begin, and take a few The prerequisites will then be checked, and once completed, click **Install**. This will take a few moments. -![Active Directory Domain Services configuration wizard prerequisites check](/media/adds_prereq.png "Active Directory Domain Services configuration wizard prerequisites check") +![Active Directory Domain Services configuration wizard prerequisites check](/archive/media/adds_prereq.png "Active Directory Domain Services configuration wizard prerequisites check") Once completed, DC01 should reboot automatically, but if not, ensure you reboot it yourself. @@ -222,14 +222,14 @@ Rather than use the main domain admin account, we'll add an additional administr * Full name: Lab Admin * User logon name: labadmin -![Active Directory Domain Services New Object wizard - adding a user](/media/adds_new_user.png "Active Directory Domain Services New Object wizard - adding a user") +![Active Directory Domain Services New Object wizard - adding a user](/archive/media/adds_new_user.png "Active Directory Domain Services New Object wizard - adding a user") 4. Provide a password for this new account, and **tick the Password never expires** box, then click **Next**, then click **Finish** 5. Click on the **Users** OU, and find the new **Lab Admin** account 6. Right-click the **Lab Admin** account, and click **Add to a group...** 7. In the **Select Groups** window, in the **Enter the object names to select** box, enter **Domain Admins**, **Schema Admins** and **Enterprise Admins**, clicking **Check Names** after each one, then click **OK**, then **OK** to close the confirmation popup -![Active Directory Domain Services New Object wizard - adding a user to groups](/media/adds_group.png "Active Directory Domain Services New Object wizard - adding a user to groups") +![Active Directory Domain Services New Object wizard - adding a user to groups](/archive/media/adds_group.png "Active Directory Domain Services New Object wizard - adding a user to groups") 8. Close the **Active Directory Users and Computers** window @@ -253,17 +253,17 @@ In this step, you'll be using Hyper-V Manager to deploy a Windows 10 Enterprise 5. Tick the box for **Store the virtual machine in a different location** and click **Browse** 6. In the **Select Folder** window, click on **This PC**, navigate to **C:**, click on **VMs**, click **Select Folder** and click **Next** -![Specify VM name and location](/media/new_vm_mgmt01.png "Specify VM name and location") +![Specify VM name and location](/archive/media/new_vm_mgmt01.png "Specify VM name and location") 7. On the **Specify Generation** page, select **Generation 2** and click **Next** 8. On the **Assign Memory** page, assign 4GB memory by entering **4096** for Startup memory and tick the **Use Dynamic Memory for this virtual machine**, then click **Next** -![Assign VM memory](/media/new_vm_dynamicmem.png "Assign VM memory") +![Assign VM memory](/archive/media/new_vm_dynamicmem.png "Assign VM memory") 9. On the **Configure Networking** page, select **InternalNAT** and click **Next** 10. On the **Connect Virtual Hard Disk** page, leave the **size** at **127** and click **Next** -![Connect Virtual Hard Disk](/media/new_vm_mgmt01_vhd_ga.png "Connect Virtual Hard Disk") +![Connect Virtual Hard Disk](/archive/media/new_vm_mgmt01_vhd_ga.png "Connect Virtual Hard Disk") 11. On the **Installation Options** page, select **Install an operating system from a bootable image file**, and click **Browse** 12. Navigate to **C:\ISO** and select your **W10.iso** file, and click **Open**. Then click **Next** @@ -277,22 +277,22 @@ Your new MGMT01 virtual machine will now be created. Once created, we need to m * Minimum RAM: 2048 * Maximum RAM: 4096 -![Updating memory for MGMT01](/media/dynamicmem_mgmt01.png "Updating memory for MGMT01") +![Updating memory for MGMT01](/archive/media/dynamicmem_mgmt01.png "Updating memory for MGMT01") 3. If you are running on a **Windows 10 Hyper-V host**, you should **disable automatic checkpoints**. From the **Settings** window, under **Management**, click **Checkpoints** and then if ticked, **untick** the **Enable checkpoints** box, then click **OK** With the VM configured correctly, in **Hyper-V Manager**, double-click MGMT01. This should open the VM Connect window. -![Starting up MGMT01](/media/startvm_mgmt01.png "Starting up MGMT01") +![Starting up MGMT01](/archive/media/startvm_mgmt01.png "Starting up MGMT01") In the center of the window, there is a message explaining the VM is currently switched off. Click on **Start** and then quickly **press any key** inside the VM to boot from the ISO file. If you miss the prompt to press a key to boot from CD or DVD, simply reset the VM and try again. -![Booting the VM and triggering the boot from DVD](/media/boot_from_dvd.png "Booting the VM and triggering the boot from DVD") +![Booting the VM and triggering the boot from DVD](/archive/media/boot_from_dvd.png "Booting the VM and triggering the boot from DVD") ### Complete the Out of Box Experience (OOBE) ### With the VM running, and the boot process initiated, you should be in a position to start the deployment of the Windows 10 OS. -![Initiate setup of Windows 10](/media/w10_setup.png "Initiate setup of Windows 10") +![Initiate setup of Windows 10](/archive/media/w10_setup.png "Initiate setup of Windows 10") Proceed through the process, making the following selections: @@ -322,7 +322,7 @@ With MGMT01 up and running, it's time to configure the networking so it can comm 1. In the bottom-right corner, right-click the NIC icon, and select **Open Network & Internet settings** -![Select NIC](/media/nic_adapter.png "Select NIC") +![Select NIC](/archive/media/nic_adapter.png "Select NIC") 2. In the **Settings** window, click on **Ethernet** and then click on the **Ethernet adapter** shown in the central window 3. Under **IP settings**, click **Edit**, then enter the following information, then click **Save** and close **Settings** @@ -333,7 +333,7 @@ With MGMT01 up and running, it's time to configure the networking so it can comm * Gateway: 192.168.0.1 * Preferred DNS: 192.168.0.2 -![Setting static NIC settings](/media/ip_settings.PNG "Setting static NIC settings") +![Setting static NIC settings](/archive/media/ip_settings.PNG "Setting static NIC settings") #### Optional - Install the new Microsoft Edge #### It's highly recommended to install the new version of the Microsoft Edge browser, as it gives a much smoother browsing experience, and is more efficient with it's use of limited resources, if you've deployed in a memory-constrained environment. @@ -357,7 +357,7 @@ Before installing the Windows Admin Center, you'll join MGMT01 to the azshci.loc 1. Ensure you're logged into MGMT01, then click on **Start** and enter **sysdm.cpl**, then in the results, select **sysdm.cpl** -![Open the System Properties dialog box](/media/sysdm.png "Open the System Properties dialog box") +![Open the System Properties dialog box](/archive/media/sysdm.png "Open the System Properties dialog box") 2. In the **System Properties** window, click on **Change**, then enter the following details, then click **OK** @@ -380,11 +380,11 @@ Firstly, navigate to C:\ISO, or wherever you chose to store your ISOs and Window Navigate to **Hyper-V Manager**, locate **MGMT01** and double-click the VM. This will open the VM Connect window. If you haven't set this already, you should be presented with a **Connect to MGMT01** screen. Ensure that the display size is set to **Full Screen** and using the **Show Options** dropdown, ensure that **Save my settings for future connections to this virtual machine** is ticked, then click **Connect**. -![Establish a VM Connect session to MGMT01](/media/connect_to_mgmt01.png "Establish a VM Connect session to MGMT01") +![Establish a VM Connect session to MGMT01](/archive/media/connect_to_mgmt01.png "Establish a VM Connect session to MGMT01") **NOTE** if you don't see the prompt for **Enhanced Session Mode**, simply click on the **Enhanced Session** button in the VM Connect window to activate it, and define your default settings. -![Enhanced Session Mode button](/media/enhanced_session.png "Enhanced Session Mode button") +![Enhanced Session Mode button](/archive/media/enhanced_session.png "Enhanced Session Mode button") When prompted, enter your **Lab Admin (azshci.local\labadmin) credentials** to log into MGMT01. When on the desktop, **right-click** and select **paste** to transfer the Windows Admin Center executable onto the desktop of MGMT01. @@ -395,18 +395,18 @@ To install the Windows Admin Center, simply **double-click** the executable on t 3. On the **Install Windows Admin Center on Windows 10** screen, read the installation information, then click **Next** 4. On the **Installing Windows Admin Center** screen, tick the **Create a desktop shortcut...** box, and click **Install**. The install process will take a few minutes, and once completed, you should be presented with some certificate information. -![Windows Admin Center installed](/media/wac_installed.png "Windows Admin Center installed") +![Windows Admin Center installed](/archive/media/wac_installed.png "Windows Admin Center installed") 5. Tick the **Open Windows Admin Center** box, and click **Finish** 6. Windows Admin Center will now open on https://localhost:port/ 7. Once open, you may receive notifications in the top-right corner, indicating that some extensions may require updating. -![Windows Admin Center extensions available](/media/extension_update.png "Windows Admin Center extensions available") +![Windows Admin Center extensions available](/archive/media/extension_update.png "Windows Admin Center extensions available") 8. If you do require extension updates, click on the notification, then **Go to Extensions** 9. On the **Extensions** page, you'll find a list of installed extensions. Any that require an update will be listed: -![Windows Admin Center extensions required](/media/extension_update_needed.png "Windows Admin Center extensions required") +![Windows Admin Center extensions required](/archive/media/extension_update_needed.png "Windows Admin Center extensions required") 10. Click on the extension, and click **Update**. This will take a few moments, and will reload the page when complete. With the extensions updated, navigate back to the Windows Admin Center homepage. diff --git a/nested/steps/2b_ManagementInfraPS.md b/archive/steps/2b_ManagementInfraPS.md similarity index 95% rename from nested/steps/2b_ManagementInfraPS.md rename to archive/steps/2b_ManagementInfraPS.md index d88c13b..44f83c4 100644 --- a/nested/steps/2b_ManagementInfraPS.md +++ b/archive/steps/2b_ManagementInfraPS.md @@ -3,7 +3,7 @@ Deploy management infrastructure with PowerShell Overview ----------- -With your Hyper-V host up and running, either in Azure, or on a local physical system, it's now time to deploy the core management infrastructure to support the Azure Stack HCI 20H2 deployment in a future step. +With your Hyper-V host up and running, it's now time to deploy the core management infrastructure to support the Azure Stack HCI 20H2 deployment in a future step. Contents ----------- @@ -26,7 +26,7 @@ Architecture As shown on the architecture graphic below, the core management infrastructure consists of a Windows Server 2019 domain controller VM, along with a Windows 10 Enterprise VM, which will run the Windows Admin Center. In this step, you'll deploy both of those key components. -![Architecture diagram for Azure Stack HCI 20H2 nested with management infra highlighted](/media/nested_virt_mgmt_ga.png "Architecture diagram for Azure Stack HCI 20H2 nested with management infra highlighted") +![Architecture diagram for Azure Stack HCI 20H2 nested with management infra highlighted](/archive/media/nested_virt_mgmt_ga.png "Architecture diagram for Azure Stack HCI 20H2 nested with management infra highlighted") However, before you deploy your management infrastructure, first, you need to download the necessary software components required to complete this evalution. @@ -61,7 +61,7 @@ Next, in order to download the ISO files, **open your web browser** and follow t 3. Visit https://azure.microsoft.com/en-us/products/azure-stack/hci/hci-download, complete the registration form, and download the ISO. Save the file as **AzSHCI.iso** to C:\ISO 4. Visit https://aka.ms/wacdownload to download the executables for the Windows Admin Center. Save it as **WindowsAdminCenter.msi**, also in C:\ISO -![All files have been downloaded onto your Hyper-V host](/media/download_files.png "All files have been downloaded onto your Hyper-V host") +![All files have been downloaded onto your Hyper-V host](/archive/media/download_files.png "All files have been downloaded onto your Hyper-V host") With all files downloaded, proceed on to creating your management infrastructure. @@ -116,12 +116,12 @@ Start-Sleep -Seconds 5 # Just gives enough time to see the "Press any key..." me Start-VM -Name DC01 ``` -![Booting the VM and triggering the boot from DVD](/media/boot_from_dvd.png "Booting the VM and triggering the boot from DVD") +![Booting the VM and triggering the boot from DVD](/archive/media/boot_from_dvd.png "Booting the VM and triggering the boot from DVD") ### Complete the Out of Box Experience (OOBE) ### With the VM running, and the boot process initiated, you should be in a position to start the deployment of the Windows Server 2019 OS. -![Initiate setup of the Windows Server 2019 OS](/media/ws_setup.png "Initiate setup of the Windows Server 2019 OS") +![Initiate setup of the Windows Server 2019 OS](/archive/media/ws_setup.png "Initiate setup of the Windows Server 2019 OS") Proceed through the process, making the following selections: @@ -134,7 +134,7 @@ Proceed through the process, making the following selections: Installation will then begin, and will take a few minutes, automatically rebooting as part of the process. -![Initiate setup of the Windows Server 2019 OS](/media/ws_install_complete.png "Initiate setup of the Windows Server 2019 OS") +![Initiate setup of the Windows Server 2019 OS](/archive/media/ws_install_complete.png "Initiate setup of the Windows Server 2019 OS") With the installation complete, you'll be prompted to change the password before logging in. Enter a password, and once complete, you should be at the **C:\Users\Administrator** screen. You can **close** the VM Connect window, as we will continue configuring the domain controller using PowerShell, from AzSHCIHost001. @@ -229,7 +229,7 @@ Invoke-Command -VMName DC01 -Credential $dcCreds -ScriptBlock { When the process is completed successfully, you should see a message similar to this below. Once validated, you should be able to reboot the domain controller and proceed on through the process. -![Active Directory role successfully install and domain controller configured](/media/dc_created.png "Active Directory role successfully install and domain controller configured") +![Active Directory role successfully install and domain controller configured](/archive/media/dc_created.png "Active Directory role successfully install and domain controller configured") ```powershell Write-Verbose "Rebooting DC01 to finish installing of Active Directory" -Verbose @@ -327,12 +327,12 @@ Start-Sleep -Seconds 5 Start-VM -Name MGMT01 ``` -![Booting the VM and triggering the boot from DVD](/media/boot_from_dvd.png "Booting the VM and triggering the boot from DVD") +![Booting the VM and triggering the boot from DVD](/archive/media/boot_from_dvd.png "Booting the VM and triggering the boot from DVD") ### Complete the Out of Box Experience (OOBE) ### With the VM running, and the boot process initiated, you should be in a position to start the deployment of the Windows 10 OS. -![Initiate setup of Windows 10](/media/w10_setup.png "Initiate setup of Windows 10") +![Initiate setup of Windows 10](/archive/media/w10_setup.png "Initiate setup of Windows 10") Proceed through the process, making the following selections: @@ -381,7 +381,7 @@ vmconnect.exe localhost MGMT01 This will open the VM Connect window. You should be presented with a **Connect to MGMT01** screen. Ensure that the display size is set to **Full Screen** and using the **Show Options** dropdown, ensure that **Save my settings for future connections to this virtual machine** is ticked, then click **Connect**. -![Establish a VM Connect session to MGMT01](/media/connect_to_mgmt01.png "Establish a VM Connect session to MGMT01") +![Establish a VM Connect session to MGMT01](/archive/media/connect_to_mgmt01.png "Establish a VM Connect session to MGMT01") 1. Open the existing **Microsoft Edge** browser, and navigate to https://www.microsoft.com/edge 2. On the landing page, click on **Download** and when prompted, **read the license terms** then click **Accept and download** @@ -452,18 +452,18 @@ To install the Windows Admin Center, simply **double-click** the executable on t 3. On the **Install Windows Admin Center on Windows 10** screen, read the installation information, then click **Next** 4. On the **Installing Windows Admin Center** screen, tick the **Create a desktop shortcut...** box, and click **Install**. The install process will take a few minutes, and once completed, you should be presented with some certificate information. -![Windows Admin Center installed](/media/wac_installed.png "Windows Admin Center installed") +![Windows Admin Center installed](/archive/media/wac_installed.png "Windows Admin Center installed") 5. Tick the **Open Windows Admin Center** box, and click **Finish** 6. Windows Admin Center will now open on https://localhost:port/ 7. Once open, you may receive notifications in the top-right corner, indicating that some extensions may require updating. -![Windows Admin Center extensions available](/media/extension_update.png "Windows Admin Center extensions available") +![Windows Admin Center extensions available](/archive/media/extension_update.png "Windows Admin Center extensions available") 8. If you do require extension updates, click on the notification, then **Go to Extensions** 9. On the **Extensions** page, you'll find a list of installed extensions. Any that require an update will be listed: -![Windows Admin Center extensions required](/media/extension_update_needed.png "Windows Admin Center extensions required") +![Windows Admin Center extensions required](/archive/media/extension_update_needed.png "Windows Admin Center extensions required") 10. Click on the extension, and click **Update**. This will take a few moments, and will reload the page when complete. With the extensions updated, navigate back to the Windows Admin Center homepage. diff --git a/nested/steps/3a_AzSHCINodesGUI.md b/archive/steps/3a_AzSHCINodesGUI.md similarity index 89% rename from nested/steps/3a_AzSHCINodesGUI.md rename to archive/steps/3a_AzSHCINodesGUI.md index 1c402b3..4c07ea4 100644 --- a/nested/steps/3a_AzSHCINodesGUI.md +++ b/archive/steps/3a_AzSHCINodesGUI.md @@ -20,7 +20,7 @@ Architecture As shown on the architecture graphic below, in this step, **you'll deploy a number of nested Azure Stack HCI 20H2 nodes**. The minimum number for deployment of a local Azure Stack HCI 20H2 cluster is **2 nodes**, however if your Hyper-V host has enough spare capacity, you could deploy additional nested nodes, and explore more complex scenarios, such as a nested **stretch cluster**. For the purpose of this step, we'll focus on deploying 2 nodes, however you should make adjustments based on your environment. -![Architecture diagram for Azure Stack HCI 20H2 nested](/media/nested_virt_nodes_ga.png "Architecture diagram for Azure Stack HCI 20H2 nested") +![Architecture diagram for Azure Stack HCI 20H2 nested](/archive/media/nested_virt_nodes_ga.png "Architecture diagram for Azure Stack HCI 20H2 nested") Create your first nested Azure Stack HCI 20H2 node ----------- @@ -40,12 +40,12 @@ In this step, you'll be using Hyper-V Manager to deploy an Azure Stack HCI 20H2 5. Tick the box for **Store the virtual machine in a different location** and click **Browse** 6. In the **Select Folder** window, click on **This **PC****, navigate to **C:**, click on the **VMs** folder, click **Select Folder** and then click **Next** -![Specify VM name and location](/media/new_vm_node.png "Specify VM name and location") +![Specify VM name and location](/archive/media/new_vm_node.png "Specify VM name and location") 7. On the **Specify Generation** page, select **Generation 2** and click **Next** 8. On the **Assign Memory** page, assign 24GB memory by entering **24576** for Startup memory and leave the the **Use Dynamic Memory for this virtual machine** empty, then click **Next** -![Assign VM memory](/media/new_vm_node_memory_ga.png "Assign VM memory") +![Assign VM memory](/archive/media/new_vm_node_memory_ga.png "Assign VM memory") #### Dynamic Memory and Runtime Memory Resize #### When Hyper-V is running inside a virtual machine, the virtual machine must be turned off to adjust its memory. This means that even if dynamic memory is enabled, **the amount of memory will not fluctuate**. For virtual machines without dynamic memory enabled, any attempt to adjust the amount of memory while it's on will fail. Note that simply enabling nested virtualization will have no effect on dynamic memory or runtime memory resize. The incompatibility only occurs while Hyper-V is running in the VM. @@ -55,7 +55,7 @@ When Hyper-V is running inside a virtual machine, the virtual machine must be tu 9. On the **Configure Networking** page, select **InternalNAT** and click **Next** 10. On the **Connect Virtual Hard Disk** page, change **size** to **30** and click **Next** -![Connect Virtual Hard Disk](/media/new_vm_node_vhd.png "Connect Virtual Hard Disk") +![Connect Virtual Hard Disk](/archive/media/new_vm_node_vhd.png "Connect Virtual Hard Disk") 11. On the **Installation Options** page, select **Install an operating system from a bootable image file**, and click **Browse** 12. Navigate to **C:\ISO** and select your **AzSHCI.iso** file, and click **Open**. Then click **Next** @@ -74,7 +74,7 @@ Your new AZSHCINODE01 virtual machine will now be created. Once created, we nee 9. Once you have **4 network adapters**, click on **Processor** 10. For **Number of virtual processors**, choose a number appropriate to your underlying hardware. In this case, we'll choose **16** but you should choose a number appropriate to your physical system size, then click **Apply** -![Configuring the vm settings](/media/new_vm_node_settings.png "Configuring the vm settings") +![Configuring the vm settings](/archive/media/new_vm_node_settings.png "Configuring the vm settings") You now need to add additional hard drives to support the Azure Stack HCI 20H2 nodes and cluster. You need to add a minimum of 2 data disks, but we will add 4 data disks to each node. @@ -85,14 +85,14 @@ You now need to add additional hard drives to support the Azure Stack HCI 20H2 n 13. On the **Choose Disk Type** page, ensure **Dynamically expanding** is selected, then click **Next** 14. On the **Specify Name and Location** page, enter **DATA01.vhdx**, and change the location to **C:\VMs\AZSHCINODE01\Virtual Hard Disks**, then click **Next** -![Adding additional hard drives to AzSHCINode01](/media/azshci_data_disk.png "Adding additional hard drives to AzSHCINode01") +![Adding additional hard drives to AzSHCINode01](/archive/media/azshci_data_disk.png "Adding additional hard drives to AzSHCINode01") 15. On the **Configure Disk** page, ensure **Create a blank virtual hard disk** is selected, set size to **100**, then click **Next** 16. On the **Completing the New Virtual Hard Disk Wizard** page, review your settings and click **Finish** 17. Back in the **AZSHCINODE01 settings**, click **Apply** 18. **Repeat steps 11-17** to add **at least 3 more data disks** -![Finished adding additional hard drives to AzSHCINode01](/media/azshci_disks_added_ga.png "Finished adding additional hard drives to AzSHCINode01") +![Finished adding additional hard drives to AzSHCINode01](/archive/media/azshci_disks_added_ga.png "Finished adding additional hard drives to AzSHCINode01") 19. If you are running on a **Windows 10 Hyper-V host**, you should also **disable automatic checkpoints**. From the **Settings** window, under **Management**, click **Checkpoints** and then if ticked, **untick** the **Enable checkpoints** box, then click **OK** @@ -102,20 +102,20 @@ Before starting the VM, in order to enable Hyper-V to function inside the AZSHCI Set-VMProcessor -VMName AZSHCINODE01 -ExposeVirtualizationExtensions $true -Verbose ``` -![Enabling nested virtualization on AZSHCINODE01](/media/enable_nested.png "Enabling nested virtualization on AZSHCINODE01") +![Enabling nested virtualization on AZSHCINODE01](/archive/media/enable_nested.png "Enabling nested virtualization on AZSHCINODE01") With the VM configured correctly, in **Hyper-V Manager**, double-click the AZSHCINODE01 VM. This should open the VM Connect window. -![Starting up AZSHCINODE01](/media/node_turned_off.png "Starting up AZSHCINODE01") +![Starting up AZSHCINODE01](/archive/media/node_turned_off.png "Starting up AZSHCINODE01") In the center of the window, there is a message explaining the VM is currently switched off. Click on **Start** and then quickly **press any key** inside the VM to boot from the ISO file. If you miss the prompt to press a key to boot from CD or DVD, simply reset the VM and try again. -![Booting the VM and triggering the boot from DVD](/media/boot_from_dvd.png "Booting the VM and triggering the boot from DVD") +![Booting the VM and triggering the boot from DVD](/archive/media/boot_from_dvd.png "Booting the VM and triggering the boot from DVD") ### Complete the Out of Box Experience (OOBE) ### With the VM running, and the boot process initiated, you should be in a position to start the deployment of the Azure Stack HCI 20H2 OS. -![Initiate setup of the Azure Stack HCI 20H2 OS](/media/azshci_setup.png "Initiate setup of the Azure Stack HCI 20H2 OS") +![Initiate setup of the Azure Stack HCI 20H2 OS](/archive/media/azshci_setup.png "Initiate setup of the Azure Stack HCI 20H2 OS") Proceed through the process, making the following selections: @@ -127,11 +127,11 @@ Proceed through the process, making the following selections: Installation will then begin, and will take a few minutes, automatically rebooting as part of the process. -![Completed setup of the Azure Stack HCI 20H2 OS](/media/azshci_setup_complete.png "Completed setup of the Azure Stack HCI 20H2 OS") +![Completed setup of the Azure Stack HCI 20H2 OS](/archive/media/azshci_setup_complete.png "Completed setup of the Azure Stack HCI 20H2 OS") With the installation complete, you'll be prompted to change the password before logging in. Enter a password, and once complete, you should be at the **command prompt** on the "Welcome to Azure Stack HCI" screen. -![Azure Stack HCI 20H2 Welcome Screen](/media/sconfig.png "Azure Stack HCI 20H2 Welcome Screen") +![Azure Stack HCI 20H2 Welcome Screen](/archive/media/sconfig.png "Azure Stack HCI 20H2 Welcome Screen") #### Configure Azure Stack HCI 20H2 node networking using SConfig #### With the node up and running, it's time to configure the networking with SConfig, a useful local administrative interface. @@ -140,7 +140,7 @@ With the node up and running, it's time to configure the networking with SConfig 2. Enter **8** then press **Enter** to select **Network Settings** 3. Choose one of the interfaces by typing the corresponding number, and pressing **Enter** -![Showing NICs using SConfig](/media/sconfig_nic.png "Showing NICs using SConfig") +![Showing NICs using SConfig](/archive/media/sconfig_nic.png "Showing NICs using SConfig") 4. On the **Network Adapter Settings** screen, press **1**, then **Enter** 5. Enter **S** for **Static IP** and press **Enter** diff --git a/nested/steps/3b_AzSHCINodesPS.md b/archive/steps/3b_AzSHCINodesPS.md similarity index 96% rename from nested/steps/3b_AzSHCINodesPS.md rename to archive/steps/3b_AzSHCINodesPS.md index 1a1f2c5..2fe290c 100644 --- a/nested/steps/3b_AzSHCINodesPS.md +++ b/archive/steps/3b_AzSHCINodesPS.md @@ -22,7 +22,7 @@ Architecture As shown on the architecture graphic below, in this step, **you'll deploy a number of nested Azure Stack HCI 20H2 nodes**. The minimum number for deployment of a local Azure Stack HCI 20H2 cluster is **2 nodes**, however if your Hyper-V host has enough spare capacity, you could deploy additional nested nodes, and explore more complex scenarios, such as a nested **stretch cluster**. For the purpose of this step, we'll focus on deploying 2 nodes, however you should make adjustments based on your environment. -![Architecture diagram for Azure Stack HCI 20H2 nested](/media/nested_virt_nodes_ga.png "Architecture diagram for Azure Stack HCI 20H2 nested") +![Architecture diagram for Azure Stack HCI 20H2 nested](/archive/media/nested_virt_nodes_ga.png "Architecture diagram for Azure Stack HCI 20H2 nested") Create your first nested Azure Stack HCI 20H2 node ----------- @@ -87,7 +87,7 @@ Set-VMProcessor -VMName $nodeName -ExposeVirtualizationExtensions $true -Verbose When those commands have completed, this is what you would see in Hyper-V Manager, in the settings view: -![Finished settings for the AZSHCINODE01 node](/media/azshci_settings_ps_ga.png "Finished settings for the AZSHCINODE01 node") +![Finished settings for the AZSHCINODE01 node](/archive/media/azshci_settings_ps_ga.png "Finished settings for the AZSHCINODE01 node") With the VM configured correctly, you can use the following commands to connect to the VM using VM Connect, and at the same time, start the VM. To boot from the ISO, you'll need to click on the VM and quickly press a key to trigger the boot from the DVD inside the VM. If you miss the prompt to press a key to boot from CD or DVD, simply reset the VM and try again. @@ -98,12 +98,12 @@ Start-Sleep -Seconds 5 Start-VM -Name $nodeName ``` -![Booting the VM and triggering the boot from DVD](/media/boot_from_dvd.png "Booting the VM and triggering the boot from DVD") +![Booting the VM and triggering the boot from DVD](/archive/media/boot_from_dvd.png "Booting the VM and triggering the boot from DVD") ### Complete the Out of Box Experience (OOBE) ### With the VM running, and the boot process initiated, you should be in a position to start the deployment of the Azure Stack HCI 20H2 OS. -![Initiate setup of the Azure Stack HCI 20H2 OS](/media/azshci_setup.png "Initiate setup of the Azure Stack HCI 20H2 OS") +![Initiate setup of the Azure Stack HCI 20H2 OS](/archive/media/azshci_setup.png "Initiate setup of the Azure Stack HCI 20H2 OS") Proceed through the process, making the following selections: @@ -115,7 +115,7 @@ Proceed through the process, making the following selections: Installation will then begin, and will take a few minutes, automatically rebooting as part of the process. -![Completed setup of the Azure Stack HCI 20H2 OS](/media/azshci_setup_complete.png "Completed setup of the Azure Stack HCI 20H2 OS") +![Completed setup of the Azure Stack HCI 20H2 OS](/archive/media/azshci_setup_complete.png "Completed setup of the Azure Stack HCI 20H2 OS") With the installation complete, you'll be prompted to change the password before logging in. Enter a password and exit to command line. Once complete, you should be at the **command prompt** on the "Welcome to Azure Stack HCI" screen. Minimize the VM Connect window. diff --git a/nested/steps/4_AzSHCICluster.md b/archive/steps/4_AzSHCICluster.md similarity index 87% rename from nested/steps/4_AzSHCICluster.md rename to archive/steps/4_AzSHCICluster.md index 8686ac4..85ef1e7 100644 --- a/nested/steps/4_AzSHCICluster.md +++ b/archive/steps/4_AzSHCICluster.md @@ -24,7 +24,7 @@ Architecture As shown on the architecture graphic below, in this step, you'll take the nodes that you previously deployed, and be **clustering them into an Azure Stack HCI 20H2 cluster**. You'll be focused on **creating a cluster in a single site**, but in later articles, we'll also cover creating a stretch cluster. -![Architecture diagram for Azure Stack HCI 20H2 nested](/media/nested_virt_nodes_ga.png "Architecture diagram for Azure Stack HCI 20H2 nested") +![Architecture diagram for Azure Stack HCI 20H2 nested](/archive/media/nested_virt_nodes_ga.png "Architecture diagram for Azure Stack HCI 20H2 nested") Before you begin ----------- @@ -64,34 +64,34 @@ If you have just 2 nodes, or if your preference is for a cluster running in a si ### Get started ### -![Choose cluster type in the Create Cluster wizard](/media/wac_cluster_type_ga.png "Choose cluster type in the Create Cluster wizard") +![Choose cluster type in the Create Cluster wizard](/archive/media/wac_cluster_type_ga.png "Choose cluster type in the Create Cluster wizard") 1. Ensure you select **Azure Stack HCI**, select **All servers in one site** and cick **Create** 2. On the **Check the prerequisites** page, review the requirements and click **Next** 3. On the **Add Servers** page, supply a **username**, which should be **azshci\labadmin** and **your-domain-admin-password** and then one by one, enter the node names (or IP addresses if names don't resolve) of your Azure Stack HCI 20H2 nodes, clicking **Add** after each one has been located. Each node will be validated, and given a **Ready** status when fully validated. This may take a few moments - once you've added all nodes, click **Next** -![Add servers in the Create Cluster wizard](/media/add_nodes_ga.png "Add servers in the Create Cluster wizard") +![Add servers in the Create Cluster wizard](/archive/media/add_nodes_ga.png "Add servers in the Create Cluster wizard") 4. On the **Join a domain** page, details should already be in place, as we joined the domain previously, so click **Next** -![Joined the domain in the Create Cluster wizard](/media/wac_domain_joined_ga.png "Joined the domain in the Create Cluster wizard") +![Joined the domain in the Create Cluster wizard](/archive/media/wac_domain_joined_ga.png "Joined the domain in the Create Cluster wizard") 5. On the **Install features** page, Windows Admin Center will query the nodes for currently installed features, and will request you install required features. Click **Install features**. This will take a few moments - once complete, click **Next** -![Installing required features in the Create Cluster wizard](/media/wac_installed_features_ga.png "Installing required features in the Create Cluster wizard") +![Installing required features in the Create Cluster wizard](/archive/media/wac_installed_features_ga.png "Installing required features in the Create Cluster wizard") 6. On the **Install updates** page, Windows Admin Center will query the nodes for available updates, and will request you install any that are required. Optionally, click **Install updates**. This will take a few moments - once complete, click **Next** 7. On the **Install hardware updates** page, in a nested environment it's likely you'll have no updates, so click **Next** 8. On the **Restart servers** page, if required, click **Restart servers** -![Restart nodes in the Create Cluster wizard](/media/wac_restart_ga.png "Restart nodes in the Create Cluster wizard") +![Restart nodes in the Create Cluster wizard](/archive/media/wac_restart_ga.png "Restart nodes in the Create Cluster wizard") ### Networking ### With the servers domain joined, configured with the appropriate features, updated and rebooted, you're ready to configure your network. You have a number of different choices here, so we'll try to explain why we're making each selection, so you can better apply it to your environment further down the road. Firstly, Windows Admin Center will verify your networking setup - it'll tell you how many NICs are in each node, along with relevant hardware information, MAC address and status information. Review for accuracy, and then click **Next** -![Verify network in the Create Cluster wizard](/media/wac_verify_network_ga.png "Verify network in the Create Cluster wizard") +![Verify network in the Create Cluster wizard](/archive/media/wac_verify_network_ga.png "Verify network in the Create Cluster wizard") The first key step with setting up the networking with Windows Admin Center, is to choose a management NIC that will be dedicated for management use. You can choose either a single NIC, or two NICs for redundancy. This step specifically designates 1 or 2 adapters that will be used by the Windows Admin Center to orchestrate the cluster creation flow. It's mandatory to select at least one of the adapters for management, and in a physical deployment, the 1GbE NICs are usually good candidates for this. @@ -100,24 +100,24 @@ As it stands, this is the way that the Windows Admin Center approaches the netwo #### Network Setup Overview #### Each of your Azure Stack HCI 20H2 nodes should have 4 NICs. For this simple evaluation, you'll dedicate the NICs in the following way: -* 1 NIC will be dedicated to management. It will reside on the 192.168.0.0/24 subnet. No virtual switch will be attached to this NIC. -* 1 NIC will be dedicated to VM traffic. A virtual switch will be attached to this NIC and the Azure Stack HCI 20H2 host will no longer use this NIC for it's own traffic. +* 1 NIC will be dedicated to management. It will reside on the 192.168.0.0/24 subnet. No virtual switch will be attached to this NIC. +* 1 NIC will be dedicated to VM traffic. A virtual switch will be attached to this NIC and the Azure Stack HCI 20H2 host will no longer use this NIC for it's own traffic. * 2 NICs will be dedicated to storage traffic. They will reside on 2 separate subnets, 10.10.10.0/24 and 10.10.11.0/24. No virtual switches will be attached to these NICs. Again, this is just one **example** network configuration for the simple purpose of evaluation. 1. Back in the Windows Admin Center, on the **Select the adapters to use for management** page, ensure you select the **One physical network adapter for management** box -![Select management adapter in the Create Cluster wizard](/media/wac_management_nic_ga.png "Select management adapter in the Create Cluster wizard") +![Select management adapter in the Create Cluster wizard](/archive/media/wac_management_nic_ga.png "Select management adapter in the Create Cluster wizard") 2. Then, for each node, **select the highlighted NIC** that will be dedicated for management. The reason only one NIC is highlighted, is because this is the only one that has an IP address assigned from a previous step. Once you've finished your selections, scroll to the bottom, then click **Apply and test** -![Select management adapters in the Create Cluster wizard](/media/wac_singlemgmt_ga.png "Select management adapters in the Create Cluster wizard") +![Select management adapters in the Create Cluster wizard](/archive/media/wac_singlemgmt_ga.png "Select management adapters in the Create Cluster wizard") 3. Windows Admin Center will then apply the configuration to your NIC. When complete and successful, click **Next** 4. On the **Virtual Switch** page, you have a number of options -![Select vSwitch in the Create Cluster wizard](/media/wac_vswitches_ga.png "Select vSwitch in the Create Cluster wizard") +![Select vSwitch in the Create Cluster wizard](/archive/media/wac_vswitches_ga.png "Select vSwitch in the Create Cluster wizard") * **Create one virtual switch for compute and storage together** - in this configuration, your Azure Stack HCI 20H2 nodes will create a vSwitch, comprised of multiple NICs, and the bandwidth available across these NICs will be shared by the Azure Stack HCI 20H2 nodes themselves, for storage traffic, and in addition, any VMs you deploy on top of the nodes, will also share this bandwidth. * **Create one virtual switch for compute only** - in this configuration, you would leave some NICs dedicated to storage traffic, and have a set of NICs attached to a vSwitch, to which your VMs traffic would be dedicated. @@ -126,11 +126,11 @@ Again, this is just one **example** network configuration for the simple purpose 5. Select the **Create one virtual switch for compute only**, and select the **Ethernet 2** NICs on each node, then click **Next** -![Create single vSwitch for Compute in the Create Cluster wizard](/media/wac_compute_vswitch_ga.png "Create single vSwitch for Compute in the Create Cluster wizard") +![Create single vSwitch for Compute in the Create Cluster wizard](/archive/media/wac_compute_vswitch_ga.png "Create single vSwitch for Compute in the Create Cluster wizard") 6. On the **RDMA** page, you're now able to configure the appropriate RDMA settings for your host networks. If you do choose to tick the box, in a nested environment, you'll be presented with an error, so click **Next** -![Error message when configuring RDMA in a nested environment](/media/wac_enable_rdma.png "Error message when configuring RDMA in a nested environment") +![Error message when configuring RDMA in a nested environment](/archive/media/wac_enable_rdma.png "Error message when configuring RDMA in a nested environment") 7. On the **Define networks** page, this is where you can define the specific networks, separate subnets, and optionally apply VLANs. In this **nested environment**, we now have 3 NICs remaining. Configure your remaining NICs as follows, by clicking on a field in the table and entering the appropriate information. @@ -147,7 +147,7 @@ Again, this is just one **example** network configuration for the simple purpose When you click **Apply and test**, Windows Admin Center validates network connectivity between the adapters in the same VLAN and subnet, which may take a few moments. Once complete, your configuration should look similar to this: -![Define networks in the Create Cluster wizard](/media/wac_define_network_ga.png "Define networks in the Create Cluster wizard") +![Define networks in the Create Cluster wizard](/archive/media/wac_define_network_ga.png "Define networks in the Create Cluster wizard") 8. Once the networks have been verified, you can optionally review the networking test report, and once complete, click **Next** @@ -158,7 +158,7 @@ With the network configured for the evaluation environment, it's time to constru 1. At the start of the **Cluster** wizard, on the **Validate the cluster** page, click **Validate**. You *may* be prompted with a **Credential Security Service Provider (CredSSP)** box - read the information, then click **Yes** -![Validate cluster in the Create Cluster wizard](/media/wac_credssp_ga.png "Validate cluster in the Create Cluster wizard") +![Validate cluster in the Create Cluster wizard](/archive/media/wac_credssp_ga.png "Validate cluster in the Create Cluster wizard") 2. Cluster validation will then start, and will take a few moments to complete - once completed, you should see a successful message. @@ -166,7 +166,7 @@ With the network configured for the evaluation environment, it's time to constru **NOTE** - if you see an issues when trying to validate the cluster, [see the workarounds here](#troubleshooting-cluster-validation-issues). -![Validation complete in the Create Cluster wizard](/media/wac_validated_ga.png "Validation complete in the Create Cluster wizard") +![Validation complete in the Create Cluster wizard](/archive/media/wac_validated_ga.png "Validation complete in the Create Cluster wizard") 1. Optionally, if you want to review the validation report, click on **Download report** and open the file in your browser. 2. Back in the **Validate the cluster** screen, click **Next** @@ -174,34 +174,34 @@ With the network configured for the evaluation environment, it's time to constru 4. Under **IP address**, click **Specify one or more static addresses**, and enter **192.168.0.10** (assuming you've deployed less than 10 nodes, otherwise adjust accordingly), and click **Add** 5. Expand **Advanced** and review the settings, then click **Create cluster** -![Finalize cluster creation in the Create Cluster wizard](/media/wac_create_clus_ga.png "Finalize cluster creation in the Create Cluster wizard") +![Finalize cluster creation in the Create Cluster wizard](/archive/media/wac_create_clus_ga.png "Finalize cluster creation in the Create Cluster wizard") 6. With all settings confirmed, click **Create cluster**. This will take a few moments. Once complete, click **Next: Storage** -![Cluster creation successful in the Create Cluster wizard](/media/wac_cluster_success_ga.png "Cluster creation successful in the Create Cluster wizard") +![Cluster creation successful in the Create Cluster wizard](/archive/media/wac_cluster_success_ga.png "Cluster creation successful in the Create Cluster wizard") ### Storage ### With the cluster successfully created, you're now good to proceed on to configuring your storage. Whilst less important in a fresh nested environment, it's always good to start from a clean slate, so first, you'll clean the drives before configuring storage. 1. On the storage landing page within the Create Cluster wizard, click **Erase Drives**, and when prompted, with **You're about to erase all existing data**, click **Erase drives**. Once complete, you should have a successful confirmation message, then click **Next** -![Cleaning drives in the Create Cluster wizard](/media/wac_clean_drives_ga.png "Cleaning drives in the Create Cluster wizard") +![Cleaning drives in the Create Cluster wizard](/archive/media/wac_clean_drives_ga.png "Cleaning drives in the Create Cluster wizard") 2. On the **Check drives** page, validate that all your drives have been detected, and show correctly. As these are virtual disks in a nested environment, they won't display as SSD or HDD etc. You should have **4 data drives** per node. Once verified, click **Next** -![Verified drives in the Create Cluster wizard](/media/wac_check_drives_ga.png "Verified drives in the Create Cluster wizard") +![Verified drives in the Create Cluster wizard](/archive/media/wac_check_drives_ga.png "Verified drives in the Create Cluster wizard") 3. Storage Spaces Direct validation tests will then automatically run, which will take a few moments. -![Verifying Storage Spaces Direct in the Create Cluster wizard](/media/wac_validate_storage_ga.png "Verifying Storage Spaces Direct in the Create Cluster wizard") +![Verifying Storage Spaces Direct in the Create Cluster wizard](/archive/media/wac_validate_storage_ga.png "Verifying Storage Spaces Direct in the Create Cluster wizard") 4. Once completed, you should see a successful confirmation. You can scroll through the brief list of tests, or alternatively, click to **Download report** to view more detailed information, then click **Next** -![Storage verified in the Create Cluster wizard](/media/wac_storage_validated_ga.png "Storage verified in the Create Cluster wizard") +![Storage verified in the Create Cluster wizard](/archive/media/wac_storage_validated_ga.png "Storage verified in the Create Cluster wizard") 5. The final step with storage, is to **Enable Storage Spaces Direct**, so click **Enable**. This will take a few moments. -![Storage Spaces Direct enabled in the Create Cluster wizard](/media/wac_s2d_enabled_ga.png "Storage Spaces Direct enabled in the Create Cluster wizard") +![Storage Spaces Direct enabled in the Create Cluster wizard](/archive/media/wac_s2d_enabled_ga.png "Storage Spaces Direct enabled in the Create Cluster wizard") 6. With Storage Spaces Direct enabled, click **Finish** 7. On the **confirmation page**, click on **Go to connections list** @@ -218,14 +218,14 @@ As part of this guide, we're going to set up cluster quorum, using **Windows Adm 1. If you're not already, ensure you're logged into your **Windows Admin Center** instance, and click on your **azshciclus** cluster that you created earlier -![Connect to your cluster with Windows Admin Center](/media/wac_azshciclus_ga.png "Connect to your cluster with Windows Admin Center") +![Connect to your cluster with Windows Admin Center](/archive/media/wac_azshciclus_ga.png "Connect to your cluster with Windows Admin Center") 2. You may be prompted for credentials, so log in with your **azshci\labadmin** credentials and tick the **Use these credentials for all connections** box. You should then be connected to your **azshciclus cluster** 3. After a few moments of verification, the **cluster dashboard** will open. 4. On the **cluster dashboard**, at the very bottom-left of the window, click on **Settings** 5. In the **Settings** window, click on **Witness** and under **Witness type**, use the drop-down to select **Cloud witness** -![Set up cloud witness in Windows Admin Center](/media/wac_cloud_witness_new_ga.png "Set up cloud witness in Windows Admin Center") +![Set up cloud witness in Windows Admin Center](/archive/media/wac_cloud_witness_new_ga.png "Set up cloud witness in Windows Admin Center") 6. Open a new tab in your browser, and navigate to **https://portal.azure.com** and login with your Azure credentials 7. You should already have a subscription from an earlier step, but if not, you should [review those steps and create one, then come back here](/nested/steps/1b_NestedInAzure.md#get-an-azure-subscription) @@ -239,7 +239,7 @@ As part of this guide, we're going to set up cluster quorum, using **Windows Adm * Account kind: **Storage (general purpose v1)** is the best option for cloud witness * Replication: **Locally-redundant storage (LRS)** - Failover Clustering uses the blob file as the arbitration point, which requires some consistency guarantees when reading the data. Therefore you must select Locally-redundant storage for Replication type. -![Set up storage account in Azure](/media/azure_cloud_witness_ga.png "Set up storage account in Azure") +![Set up storage account in Azure](/archive/media/azure_cloud_witness_ga.png "Set up storage account in Azure") 10. On the **Networking** and **Data protection** pages, accept the defaults and press **Next** 11. On the **Advanced** page, ensure that **Blob public access** is **disabled**, and **Minimum TLS version** is set to **Version 1.2** @@ -248,17 +248,17 @@ As part of this guide, we're going to set up cluster quorum, using **Windows Adm 14. On the left-hand navigation, under Settings, click **Access Keys**. When you create a Microsoft Azure Storage Account, it is associated with two Access Keys that are automatically generated - Primary Access key and Secondary Access key. For a first-time creation of Cloud Witness, use the **Primary Access Key**. There is no restriction regarding which key to use for Cloud Witness. 15. Click on **Show keys** and take a copy of the **Storage account name** and **key1** -![Configure Primary Access key in Azure](/media/azure_keys_ga.png "Configure Primary Access key in Azure") +![Configure Primary Access key in Azure](/archive/media/azure_keys_ga.png "Configure Primary Access key in Azure") 16. On the left-hand navigation, under Settings, click **Properties** and make a note of your **blob service endpoint**. -![Blob Service endpoint in Azure](/media/azure_blob_ga.png "Blob Service endpoint in Azure") +![Blob Service endpoint in Azure](/archive/media/azure_blob_ga.png "Blob Service endpoint in Azure") **NOTE** - The required service endpoint is the section of the Blob service URL **after blob.**, i.e. for our configuration, **core.windows.net** 17. With all the information gathered, return to the **Windows Admin Center** and complete the form with your values, then click **Save** -![Providing storage account info in Windows Admin Center](/media/wac_azure_key_ga.png "Providing storage account info in Windows Admin Center") +![Providing storage account info in Windows Admin Center](/archive/media/wac_azure_key_ga.png "Providing storage account info in Windows Admin Center") 18. Within a few moments, your witness settings should be successfully applied and you have now completed configuring the quorum settings for the **azshciclus** cluster. @@ -280,14 +280,14 @@ You'll need appropriate **Azure Active Directory permissions** to complete the r #### What happens when you register Azure Stack HCI 20H2? #### When you register your Azure Stack HCI 20H2 cluster, the process creates an Azure Resource Manager (ARM) resource to represent the on-prem cluster. This resource is provisioned by an Azure resource provider (RP) and placed inside a resource group, within your chosen Azure subscription. If these Azure concepts are new to you, you can check out an [overview of them, and more, here](https://docs.microsoft.com/en-us/azure/azure-resource-manager/management/overview "Azure Resource Manager overview"). -![ARM architecture for Azure Stack HCI 20H2](/media/azure_arm.png "ARM architecture for Azure Stack HCI 20H2") +![ARM architecture for Azure Stack HCI 20H2](/archive/media/azure_arm.png "ARM architecture for Azure Stack HCI 20H2") In addition to creating an Azure resource in your subscription, registering Azure Stack HCI 20H2 creates an app identity, conceptually similar to a user, in your Azure Active Directory tenant. The app identity inherits the cluster name. This identity acts on behalf on the Azure Stack HCI 20H2 cloud service, as appropriate, within your subscription. #### Understanding required Azure Active Directory permissions #### If the user who registers Azure Stack HCI 20H2 is an Azure Active Directory global administrator or has been delegated sufficient permissions, this all happens automatically, and no additional action is required. If not, approval may be needed from your Azure Active Directory global administrator (or someone with appropriate permissions) to complete registration. Your global administrator can either explicitly grant consent to the app, or they can delegate permissions so that you can grant consent to the app. -![Azure Active Directory Permissions](/media/aad_permissions.png "Azure Active Directory Permissions") +![Azure Active Directory Permissions](/archive/media/aad_permissions.png "Azure Active Directory Permissions") The user who runs Register-AzStackHCI needs Azure AD permissions to: @@ -383,31 +383,31 @@ To complete registration, you have 2 options - you can use **Windows Admin Cente 1. On **MGMT01**, logged in as **azshci\labadmin**, open the Windows Admin Center, and on the **All connections** page, select your azshciclus 2. When the cluster dashboard has loaded, in the top-right corner, you'll see the **status of the Azure registration/connection** -![Azure registration status in Windows Admin Center](/media/wac_azure_reg_dashboard.png "Azure registration status in Windows Admin Center") +![Azure registration status in Windows Admin Center](/archive/media/wac_azure_reg_dashboard.png "Azure registration status in Windows Admin Center") 3. Click on **Install PowerShell modules** to trigger Windows Admin Center to download and install the appropriate PowerShell modules to the Azure Stack HCI 20H2 node. This may take a few moments. -![Azure registration status in Windows Admin Center](/media/wac_azure_reg_dashboard_2.png "Azure registration status in Windows Admin Center") +![Azure registration status in Windows Admin Center](/archive/media/wac_azure_reg_dashboard_2.png "Azure registration status in Windows Admin Center") 4. Once installed, you can begin the registration process by clicking **Register this cluster** 5. If you haven't already, you'll be prompted to register Windows Admin Center with an Azure tenant. Follow the instructions to **Copy the code** and then click on the link to configure device login. 6. When prompted for credentials, **enter your Azure credentials** for a tenant you'd like to register the Windows Admin Center 7. Back in Windows Admin Center, you'll notice your tenant information has been added. You can now click **Connect** to connect Windows Admin Center to Azure -![Connecting Windows Admin Center to Azure](/media/wac_azure_connect.png "Connecting Windows Admin Center to Azure") +![Connecting Windows Admin Center to Azure](/archive/media/wac_azure_connect.png "Connecting Windows Admin Center to Azure") 8. Click on **Sign in** and when prompted for credentials, **enter your Azure credentials** and you should see a popup that asks for you to accept the permissions, so click **Accept** -![Permissions for Windows Admin Center](/media/wac_azure_permissions.png "Permissions for Windows Admin Center") +![Permissions for Windows Admin Center](/archive/media/wac_azure_permissions.png "Permissions for Windows Admin Center") 9. Back in Windows Admin Center, you may need to refresh the page if your 'Register this cluster' link is not active. Once active, click **Register this cluster** and you should be presented with a window requesting more information. 10. Choose your **Azure subscription** that you'd like to use to register, along with an **Azure resource group** and **region**, then click **Register**. This will take a few moments. -![Final step for registering Azure Stack HCI with Windows Admin Center](/media/wac_azure_register.png "Final step for registering Azure Stack HCI with Windows Admin Center") +![Final step for registering Azure Stack HCI with Windows Admin Center](/archive/media/wac_azure_register.png "Final step for registering Azure Stack HCI with Windows Admin Center") 11. Once completed, you should see updated status on the Windows Admin Center dashboard, showing that the cluster has been correctly registered. -![Azure registration status in Windows Admin Center](/media/wac_azure_reg_dashboard_3.png "Azure registration status in Windows Admin Center") +![Azure registration status in Windows Admin Center](/archive/media/wac_azure_reg_dashboard_3.png "Azure registration status in Windows Admin Center") You can now proceed on to [Viewing registration details in the Azure portal](#View-registration-details-in-the-Azure-portal) @@ -433,13 +433,13 @@ Invoke-Command -ComputerName AZSHCINODE01 -ScriptBlock { } ``` -![Check the registration status of the Azure Stack HCI 20H2 cluster](/media/reg_check.png "Check the registration status of the Azure Stack HCI 20H2 cluster") +![Check the registration status of the Azure Stack HCI 20H2 cluster](/archive/media/reg_check.png "Check the registration status of the Azure Stack HCI 20H2 cluster") As you can see from the result, the cluster is yet to be registered, and the cluster status identifies as **Clustered**. Azure Stack HCI 20H2 needs to register within 30 days of installation per the Azure Online Services Terms. If not clustered after 30 days, the **ClusterStatus** will show **OutOfPolicy**, and if not registered after 30 days, the **RegistrationStatus** will show **OutOfPolicy**. 3. To register the cluster, you'll first need to get your **Azure subscription ID**. An easy way to do this is to quickly **log into https://portal.azure.com**, and in the **search box** at the top of the screen, search for **subscriptions** and then click on **Subscriptions** -![Azure Subscriptions](/media/azure_subscriptions_ga.png "Azure Subscriptions") +![Azure Subscriptions](/archive/media/azure_subscriptions_ga.png "Azure Subscriptions") 4. Your **subscription** should be shown in the main window. If you have more than one subscription listed here, click the correct one, and in the new blade, copy the **Subscription ID**. @@ -472,11 +472,11 @@ Of these commands, many are optional: 6. Once dependencies have been installed, you'll receive a popup on **MGMT01** to authenticate to Azure. Provide your **Azure credentials**. -![Login to Azure](/media/azure_login_reg.png "Login to Azure") +![Login to Azure](/archive/media/azure_login_reg.png "Login to Azure") 7. Once successfully authenticated, the registration process will begin, and will take a few moments. Once complete, you should see a message indicating success, as per below: -![Register Azure Stack HCI 20H2 with PowerShell](/media/register_azshci_ga.png "Register Azure Stack HCI 20H2 with PowerShell") +![Register Azure Stack HCI 20H2 with PowerShell](/archive/media/register_azshci_ga.png "Register Azure Stack HCI 20H2 with PowerShell") **NOTE** - if upon registering, you receive an error similar to that below, please **try a different region**. You can still proceed to [Step 5](#next-steps) and continue with your evaluation, and it won't affect any functionality. Just make sure you come back and register later! @@ -491,7 +491,7 @@ Invoke-Command -ComputerName AZSHCINODE01 -ScriptBlock { Get-AzureStackHCI } ``` -![Check updated registration status with PowerShell](/media/registration_status.png "Check updated registration status with PowerShell") +![Check updated registration status with PowerShell](/archive/media/registration_status.png "Check updated registration status with PowerShell") You can see the **ConnectionStatus** and **LastConnected** time, which is usually within the last day unless the cluster is temporarily disconnected from the Internet. An Azure Stack HCI 20H2 cluster can operate fully offline for up to 30 consecutive days. @@ -501,23 +501,23 @@ With registration complete, either through Windows Admin Center, or through Powe 1. On **MGMT01**, open the Edge browser and **log into https://portal.azure.com** to check the resources created there. In the **search box** at the top of the screen, search for **Resource groups** and then click on **Resource groups** 2. You should see a new **Resource group** listed, with the name you specified earlier, which in our case, is **AZSHCICLUS_RG** -![Registration resource group in Azure](/media/registration_rg_ga.png "Registration resource group in Azure") +![Registration resource group in Azure](/archive/media/registration_rg_ga.png "Registration resource group in Azure") 12. Click on the **AZSHCICLUS_RG** resource group, and in the central pane, you'll see that a record with the name **azshciclus** has been created inside the resource group 13. Click on the **azihciclus** record, and you'll be taken to the new Azure Stack HCI Resource Provider, which shows information about all of your clusters, including details on the currently selected cluster -![Overview of the recently registered cluster in the Azure portal](/media/azure_portal_hcicluster.png "Overview of the recently registered cluster in the Azure portal") +![Overview of the recently registered cluster in the Azure portal](/archive/media/azure_portal_hcicluster.png "Overview of the recently registered cluster in the Azure portal") 14. Next, still in the Azure portal, in the **search box** at the top of the screen, search for **Azure Active Directory** and then click on **Azure Active Directory** 15. Click on **App Registrations**, then (you may need to click on **All applications**) in the box labeled "Start typing a name or Application ID to filter these results", enter **azshciclus** and in the results, click on your application -![Application ID in App Registrations in Azure](/media/azure_ad_app_ga.png "Application ID in App Registrations in Azure") +![Application ID in App Registrations in Azure](/archive/media/azure_ad_app_ga.png "Application ID in App Registrations in Azure") 1. Within the application, click on **API permissions**. From there, you can see the **Configured permissions** which have been created as part of the **Register-AzureStackHCI** you ran earlier. You can see that a number of services that have been granted appropriate permissions for billing and cluster management. Optionally, you can click on these services to see more information -![Application ID API Permissions for App Registration in Azure](/media/api_permissions_ga.png "Application ID API Permissions for App Registration in Azure") +![Application ID API Permissions for App Registration in Azure](/archive/media/api_permissions_ga.png "Application ID API Permissions for App Registration in Azure") **NOTE** - If when you ran **Register-AzureStackHCI**, you don't have appropriate permissions in Azure Active Directory, to grant admin consent, you will need to work with your Azure Active Directory administrator to complete registration later. You can exit and leave the registration in status "**pending admin consent**," i.e. partially completed. Once consent has been granted, **simply re-run Register-AzureStackHCI** to complete registration. @@ -598,4 +598,4 @@ Set-WinSystemLocale -SystemLocale en-US Restart-Computer -Force ``` -Then run validate again, and it should pass this step. This bug is being actively worked on, and should be addressed soon. +Then run validate again, and it should pass this step. This bug is being actively worked on, and should be addressed soon. \ No newline at end of file diff --git a/nested/steps/5_ExploreAzSHCI.md b/archive/steps/5_ExploreAzSHCI.md similarity index 94% rename from nested/steps/5_ExploreAzSHCI.md rename to archive/steps/5_ExploreAzSHCI.md index 876ce03..d15843b 100644 --- a/nested/steps/5_ExploreAzSHCI.md +++ b/archive/steps/5_ExploreAzSHCI.md @@ -30,11 +30,11 @@ You should be over on **MGMT01**, but if you're not, log into MGMT01, and open t 4. In the **Create volume** pane, enter **VMSTORAGE** for the volume name, and leave **Resiliency** as **Two-way mirror** 5. In Size on HDD, specify **20GB** for the size of the volume, then click **Create**. -![Create a volume on Azure Stack HCI 20H2](/media/wac_vm_storage_ga.png "Create a volume on Azure Stack HCI 20H2") +![Create a volume on Azure Stack HCI 20H2](/archive/media/wac_vm_storage_ga.png "Create a volume on Azure Stack HCI 20H2") 6. Creating the volume can take a few minutes. Notifications in the upper-right will let you know when the volume is created. The new volume appears in the Inventory list -![Volume created on Azure Stack HCI 20H2](/media/wac_vm_storage_deployed_ga.png "Volume created on Azure Stack HCI 20H2") +![Volume created on Azure Stack HCI 20H2](/archive/media/wac_vm_storage_deployed_ga.png "Volume created on Azure Stack HCI 20H2") ### Optional - Create a mirror-accelerated parity volume ### @@ -55,7 +55,7 @@ You may have seen, during the **Create volume** wizard, you could have enabled d 1. Still in **Windows Admin Center** on **MGMT01**, on the Volumes page, select the **Inventory** tab, and then select your **VMSTORAGE** volume 2. On the Volume VMSTORAGE pane, you'll see a simple rocker switch to enable **Deduplication and compression**. Click to enable it, and click **Start** -![Enable deduplication on volume](/media/wac_enable_dedup_ga.png "Enable deduplication on volume") +![Enable deduplication on volume](/archive/media/wac_enable_dedup_ga.png "Enable deduplication on volume") 3. In the **Enable deduplication** pane, use the drop-down to select **Hyper-V** then click **Enable Deduplication**. This should be enabled quickly, as there's no files on the volume. @@ -87,12 +87,12 @@ You should still be over on **MGMT01**, but if you're not, log into MGMT01, and 5. The creation process will take a few moments, and once complete, **VM001** should show within the **Virtual machines view** 6. Click on the **VM** and then click **Start** - within moments, the VM should be running -![VM001 up and running](/media/wac_vm001_ga.png "VM001 up and running") +![VM001 up and running](/archive/media/wac_vm001_ga.png "VM001 up and running") 7. Click on **VM001** to view the properties and status for this running VM 8. Click on **Connect** - you may get a **VM Connect** prompt: -![Connect to VM001](/media/vm_connect_ga.png "Connect to VM001") +![Connect to VM001](/archive/media/vm_connect_ga.png "Connect to VM001") 9. Click on **Go to Settings** and in the **Remote Desktop** pane, click on **Allow remote connections to this computer**, then **Save** 10. Click the **Back** button in your browser to return to the VM001 view, then click **Connect**, and when prompted with the certificate prompt, click **Connect** and enter appropriate credentials @@ -109,7 +109,7 @@ The final step we'll cover is using Windows Admin Center to live migrate VM001 f 3. Under **Host server**, make a note of the node that VM001 is currently running on. You may need to expand the column width to see the name 4. Next to **VM001**, click the tick box next to VM001, then click **More**. You'll notice you can Clone, Domain Join and also Move the VM. Click **Move** -![Start Live Migration using Windows Admin Center](/media/wac_move_ga.png "Start Live Migration using Windows Admin Center") +![Start Live Migration using Windows Admin Center](/archive/media/wac_move_ga.png "Start Live Migration using Windows Admin Center") 5. In the **Move Virtual Machine** pane, ensure **Failover Cluster** is selected, and leave the default **Best available cluster node** to allow Windows Admin Center to pick where to migrate the VM to, then click **Move** 6. The live migration will then begin, and within a few seconds, the VM should be running on a different node. @@ -140,7 +140,7 @@ Get-VM | Stop-VM -Force 3. Once all the VMs are switched off, you can then shut down your Hyper-V host. If you're running this environment on physical gear on-prem, you're all done, but if you deployed in Azure, visit https://portal.azure.com/, and login with your Azure credentials. Once logged in, using the search box on the dashboard, enter "azshci" and once the results are returned, click on your AzSHCIHost virtual machine. -![Virtual machine located in Azure](/media/azure_vm_search_ga.png "Virtual machine located in Azure") +![Virtual machine located in Azure](/archive/media/azure_vm_search_ga.png "Virtual machine located in Azure") 4. Once on the overview blade for your VM, along the **top navigation**, click **Stop**, and then click **OK**. Your VM will then be deallocated and **compute charges** will cease. @@ -148,7 +148,7 @@ Congratulations! ----------- You've reached the end of the evaluation guide. In this guide you have: -* Deployed/Configured a Hyper-V host, either on-prem or in Azure, to run your nested sandbox environment +* Deployed/Configured a Hyper-V host to run your nested sandbox environment * Deployed a management infrastructure including a Windows Server 2019 Active Directory and Windows 10 management server * Installed and configured the Windows Admin Center * Created, deployed and configured a number of Azure Stack HCI 20H2 nodes, in nested virtual machines diff --git a/deployment/dsc/azshcihost.zip b/deployment/dsc/azshcihost.zip new file mode 100644 index 0000000..cf9360b Binary files /dev/null and b/deployment/dsc/azshcihost.zip differ diff --git a/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/ActiveDirectoryDsc.psd1 b/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/ActiveDirectoryDsc.psd1 new file mode 100644 index 0000000..e9434c8 --- /dev/null +++ b/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/ActiveDirectoryDsc.psd1 @@ -0,0 +1,283 @@ +@{ +# Version number of this module. +moduleVersion = '6.0.1' + +# ID used to uniquely identify this module +GUID = '9FECD4F6-8F02-4707-99B3-539E940E9FF5' + +# Author of this module +Author = 'DSC Community' + +# Company or vendor of this module +CompanyName = 'DSC Community' + +# Copyright statement for this module +Copyright = 'Copyright the DSC Community contributors. All rights reserved.' + +# Description of the functionality provided by this module +Description = 'The ActiveDirectoryDsc module contains DSC resources for deployment and configuration of Active Directory. + +These DSC resources allow you to configure new domains, child domains, and high availability domain controllers, establish cross-domain trusts and manage users, groups and OUs.' + +# Minimum version of the Windows PowerShell engine required by this module +PowerShellVersion = '5.0' + +# Minimum version of the common language runtime (CLR) required by this module +CLRVersion = '4.0' + +# Nested modules to load when this module is imported. +NestedModules = 'Modules\ActiveDirectoryDsc.Common\ActiveDirectoryDsc.Common.psm1' + +# Functions to export from this module +FunctionsToExport = @( + # Exported so that WaitForADDomain can use this function in a separate scope. + 'Find-DomainController' +) + +# Cmdlets to export from this module +CmdletsToExport = @() + +# Variables to export from this module +VariablesToExport = @() + +# Aliases to export from this module +AliasesToExport = @() + +# Dsc Resources to export from this module +DscResourcesToExport = @( + 'ADComputer' + 'ADDomain' + 'ADDomainController' + 'ADDomainControllerProperties' + 'ADDomainDefaultPasswordPolicy' + 'ADDomainFunctionalLevel' + 'ADDomainTrust' + 'ADForestFunctionalLevel' + 'ADForestProperties' + 'ADGroup' + 'ADKDSKey' + 'ADManagedServiceAccount' + 'ADObjectEnabledState' + 'ADObjectPermissionEntry' + 'ADOptionalFeature' + 'ADOrganizationalUnit' + 'ADReplicationSite' + 'ADReplicationSiteLink' + 'ADServicePrincipalName' + 'ADUser' + 'WaitForADDomain' +) + +# Private data to pass to the module specified in RootModule/ModuleToProcess. This may also contain a PSData hashtable with additional module metadata used by PowerShell. +PrivateData = @{ + + PSData = @{ + + # Tags applied to this module. These help with module discovery in online galleries. + Tags = @('DesiredStateConfiguration', 'DSC', 'DSCResourceKit', 'DSCResource') + + # A URL to the license for this module. + LicenseUri = 'https://github.com/dsccommunity/ActiveDirectoryDsc/blob/master/LICENSE' + + # A URL to the main website for this project. + ProjectUri = 'https://github.com/dsccommunity/ActiveDirectoryDsc' + + # A URL to an icon representing this module. + IconUri = 'https://dsccommunity.org/images/DSC_Logo_300p.png' + + # ReleaseNotes of this module + ReleaseNotes = '## [6.0.1] - 2020-04-16 + +### Fixed + +- ActiveDirectoryDsc + - The regular expression for `minor-version-bump-message` in the file + `GitVersion.yml` was changed to only raise minor version when the + commit message contain the word `add`, `adds`, `minor`, `feature`, + or `features` ([issue #588](https://github.com/dsccommunity/ActiveDirectoryDsc/issues/588)). + - Rename folder ''Tests'' to folder ''tests'' (lower-case). + - Moved oldest changelog details to historic changelog. +- ADDomain + - Added additional Get-ADDomain retry exceptions + ([issue #581](https://github.com/dsccommunity/ActiveDirectoryDsc/issues/581)). +- ADUser + - Fixed PasswordAuthentication parameter handling + ([issue #582](https://github.com/dsccommunity/ActiveDirectoryDsc/issues/582)). + +### Changed + +- ActiveDirectoryDsc + - Only run CI pipeline on branch `master` when there are changes to files + inside the `source` folder. + +## [6.0.0] - 2020-03-12 + +### Added + +- ActiveDirectoryDsc + - Added [Codecov.io](https://codecov.io) support. + - Fixed miscellaneous spelling errors. + - Added Strict-Mode v1.0 to all unit tests. +- ADDomain + - Added integration tests + ([issue #345](https://github.com/dsccommunity/ActiveDirectoryDsc/issues/345)). +- ADGroup + - Added support for Managed Service Accounts + ([issue #532](https://github.com/dsccommunity/ActiveDirectoryDsc/issues/532)). +- ADForestProperties + - Added TombstoneLifetime property + ([issue #302](https://github.com/dsccommunity/ActiveDirectoryDsc/issues/302)). + - Added Integration tests + ([issue #349](https://github.com/dsccommunity/ActiveDirectoryDsc/issues/349)). + +### Fixed + +- ADForestProperties + - Fixed ability to clear `ServicePrincipalNameSuffix` and `UserPrincipalNameSuffix` + ([issue #548](https://github.com/PowerShell/ActiveDirectoryDsc/issues/548)). +- WaitForADDomain + - Fixed `Find-DomainController` to correctly handle an exception thrown when a domain controller is not ready + ([issue #530](https://github.com/dsccommunity/ActiveDirectoryDsc/issues/530)). +- ADObjectPermissionEntry + - Fixed issue where Get-DscConfiguration / Test-DscConfiguration throw an exception when target object path does not + yet exist + ([issue #552](https://github.com/dsccommunity/ActiveDirectoryDsc/issues/552)). + - Fixed issue where Get-TargetResource throw an exception, `Cannot find drive. A drive with the name ''AD'' does not + exist`, when running soon after domain controller restart + ([issue #547](https://github.com/dsccommunity/ActiveDirectoryDsc/issues/547)). +- ADOrganizationalUnit + - Fixed issue where Get-DscConfiguration/Test-DscConfiguration throws an exception when parent path does not yet exist + ([issue #553](https://github.com/dsccommunity/ActiveDirectoryDsc/issues/553)). +- ADReplicationSiteLink + - Fixed issue creating a Site Link with options specified + ([issue #571](https://github.com/dsccommunity/ActiveDirectoryDsc/issues/571)). +- ADDomain + - Added additional Get-ADDomain retry exceptions + ([issue #574](https://github.com/dsccommunity/ActiveDirectoryDsc/issues/574)). + +### Changed + +- ActiveDirectoryDsc + - BREAKING CHANGE: Required PowerShell version increased from v4.0 to v5.0 + - Updated Azure Pipeline Windows image + ([issue #551](https://github.com/dsccommunity/ActiveDirectoryDsc/issues/551)). + - Updated license copyright + ([issue #550](https://github.com/dsccommunity/ActiveDirectoryDsc/issues/550)). +- ADDomain + - Changed Domain Install Tracking File to use NetLogon Registry Test. + ([issue #560](https://github.com/dsccommunity/ActiveDirectoryDsc/issues/560)). + - Updated the Get-TargetResource function with the following: + - Removed unused parameters. + - Removed unnecessary domain membership check. + - Removed unneeded catch exception blocks. + - Changed Get-ADDomain and Get-ADForest to use localhost as the server. + - Improved Try/Catch blocks to only cover cmdlet calls. + - Simplified retry timing loop. + - Refactored unit tests. + - Updated NewChildDomain example to clarify the contents of the credential parameter and use Windows 2016 rather than + 2012 R2. +- ADDomainController + - Updated the Get-TargetResource function with the following: + - Removed unused parameters. + - Added IsDnsServer read-only property + ([issue #490](https://github.com/dsccommunity/ActiveDirectoryDsc/issues/490)). +- ADForestProperties + - Refactored unit tests. +- ADReplicationSiteLink + - Refactored the `Set-TargetResource` function so that properties are only set if they have been changed. + - Refactored the resource unit tests. + - Added quotes to all the variables in the localised string data. +- ADOrganizationalUnit + - Replaced throws with `New-InvalidOperationException`. + - Refactored `Get-TargetResource` to not reference properties of a `$null` object + - Fixed organization references to organizational. + - Refactored `Test-TargetResource` to use `Compare-ResourcePropertyState` common function. + - Reformatted code to keep line lengths to less than 120 characters. + - Removed redundant `Assert-Module` and `Get-ADOrganizationalUnit` function calls from `Set-TargetResource`. + - Wrapped `Set-ADOrganizationalUnit` and `Remove-ADOrganizationalUnit` with try/catch blocks and used common exception + function. + - Added `DistinguishedName` read-only property. + - Refactored unit tests. +- ADUser + - Improve Try/Catch blocks to only cover cmdlet calls. + - Move the Test-Password function to the ActiveDirectoryDsc.Common module and add unit tests. + - Reformat code to keep line lengths to less than 120 characters. + - Fix Password parameter processing when PasswordNeverResets is $true. + - Remove unnecessary Enabled parameter check. + - Remove unnecessary Clear explicit parameter check. + - Add check to only call Set-ADUser if there are properties to change. + - Refactored Unit Tests - ([issue #467](https://github.com/dsccommunity/ActiveDirectoryDsc/issues/467)) + +## [5.0.0] - 2020-01-14 + +### Added + +- ADServicePrincipalName + - Added Integration tests + ([issue #358](https://github.com/dsccommunity/ActiveDirectoryDsc/issues/358)). +- ADManagedServiceAccount + - Added Integration tests. +- ADKDSKey + - Added Integration tests + ([issue #351](https://github.com/dsccommunity/ActiveDirectoryDsc/issues/351)). + +### Changed + +- ADManagedServiceAccount + - KerberosEncryptionType property added. + ([issue #511](https://github.com/dsccommunity/ActiveDirectoryDsc/issues/511)). + - BREAKING CHANGE: AccountType parameter ValidateSet changed from (''Group'', ''Single'') to (''Group'', ''Standalone'') - + Standalone is the correct terminology. + Ref: [Service Accounts](https://docs.microsoft.com/en-us/windows/security/identity-protection/access-control/service-accounts). + ([issue #515](https://github.com/dsccommunity/ActiveDirectoryDsc/issues/515)). + - BREAKING CHANGE: AccountType parameter default of Single removed. - Enforce positive choice of account type. + - BREAKING CHANGE: MembershipAttribute parameter ValidateSet member SID changed to ObjectSid to match result property + of Get-AdObject. Previous code does not work if SID is specified. + - BREAKING CHANGE: AccountTypeForce parameter removed - unnecessary complication. + - BREAKING CHANGE: Members parameter renamed to ManagedPasswordPrincipals - to closer match Get-AdServiceAccount result + property PrincipalsAllowedToRetrieveManagedPassword. This is so that a DelegateToAccountPrincipals parameter can be + added later. + - Common Compare-ResourcePropertyState function used to replace function specific Compare-TargetResourceState and code + refactored. + ([issue #512](https://github.com/dsccommunity/ActiveDirectoryDsc/issues/512)). + - Resource unit tests refactored to use nested contexts and follow the logic of the module. +- ActiveDirectoryDsc + - Updated PowerShell help files. + - Updated Wiki link in README.md. + - Remove verbose parameters from unit tests. + - Fix PowerShell script file formatting and culture string alignment. + - Add the `pipelineIndentationStyle` setting to the Visual Studio Code settings file. + - Remove unused common function Test-DscParameterState + ([issue #522](https://github.com/dsccommunity/ActiveDirectoryDsc/issues/522)). + +### Fixed + +- ActiveDirectoryDsc + - Fix tests ErrorAction on DscResource.Test Import-Module. +- ADObjectPermissionEntry + - Updated Assert-ADPSDrive with PSProvider Checks + ([issue #527](https://github.com/dsccommunity/ActiveDirectoryDsc/issues/527)). +- ADReplicationSite + - Fixed incorrect evaluation of site configuration state when no description is defined + ([issue #534](https://github.com/dsccommunity/ActiveDirectoryDsc/issues/534)). +- ADReplicationSiteLink + - Fix RemovingSites verbose message + ([issue #518](https://github.com/dsccommunity/ActiveDirectoryDsc/issues/518)). +- ADComputer + - Fixed the SamAcountName property description + ([issue #529](https://github.com/dsccommunity/ActiveDirectoryDsc/issues/529)). + +' + + # Set to a prerelease string value if the release should be a prerelease. + Prerelease = '' + + } # End of PSData hashtable + +} # End of PrivateData hashtable +} + + + + + diff --git a/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_ADComputer/MSFT_ADComputer.psm1 b/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_ADComputer/MSFT_ADComputer.psm1 new file mode 100644 index 0000000..feeefdd --- /dev/null +++ b/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_ADComputer/MSFT_ADComputer.psm1 @@ -0,0 +1,964 @@ +$script:resourceModulePath = Split-Path -Path (Split-Path -Path $PSScriptRoot -Parent) -Parent +$script:modulesFolderPath = Join-Path -Path $script:resourceModulePath -ChildPath 'Modules' + +$script:localizationModulePath = Join-Path -Path $script:modulesFolderPath -ChildPath 'ActiveDirectoryDsc.Common' +Import-Module -Name (Join-Path -Path $script:localizationModulePath -ChildPath 'ActiveDirectoryDsc.Common.psm1') + +$script:localizedData = Get-LocalizedData -ResourceName 'MSFT_ADComputer' + +<# + A property map that maps the resource parameters to the corresponding + Active Directory computer account object attribute. +#> +$script:computerObjectPropertyMap = @( + @{ + ParameterName = 'ComputerName' + PropertyName = 'CN' + }, + @{ + ParameterName = 'Location' + }, + @{ + ParameterName = 'DnsHostName' + }, + @{ + ParameterName = 'ServicePrincipalNames' + PropertyName = 'ServicePrincipalName' + }, + @{ + ParameterName = 'UserPrincipalName' + }, + @{ + ParameterName = 'DisplayName' + }, + @{ + ParameterName = 'Path' + PropertyName = 'DistinguishedName' + }, + @{ + ParameterName = 'Description' + }, + @{ + ParameterName = 'Enabled' + }, + @{ + ParameterName = 'Manager' + PropertyName = 'ManagedBy' + }, + @{ + ParameterName = 'DistinguishedName' + ParameterType = 'Read' + PropertyName = 'DistinguishedName' + }, + @{ + ParameterName = 'SID' + ParameterType = 'Read' + } +) + +<# + .SYNOPSIS + Returns the current state of the Active Directory computer account. + + .PARAMETER ComputerName + Specifies the name of the Active Directory computer account to manage. + You can identify a computer by its distinguished name, GUID, security + identifier (SID) or Security Accounts Manager (SAM) account name. + + .PARAMETER RequestFile + Specifies the full path to the Offline Domain Join Request file to create. + + .PARAMETER EnabledOnCreation + Specifies if the computer account is created enabled or disabled. + By default the Enabled property of the computer account will be set to + the default value of the cmdlet New-ADComputer. This property is ignored + if the parameter RequestFile is specified in the same configuration. + This parameter does not enforce the property `Enabled`. To enforce the + property `Enabled` see the resource ADObjectEnabledState. + + .PARAMETER DomainController + Specifies the Active Directory Domain Services instance to connect to perform the task. + + Used by Get-ADCommonParameters and is returned as a common parameter. + + .PARAMETER Credential + Specifies the user account credentials to use to perform the task. + + Used by Get-ADCommonParameters and is returned as a common parameter. + + .PARAMETER RestoreFromRecycleBin + Try to restore the organizational unit from the recycle bin before + creating a new one. +#> +function Get-TargetResource +{ + [CmdletBinding()] + [OutputType([System.Collections.Hashtable])] + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $ComputerName, + + [Parameter()] + [ValidateNotNull()] + [System.String] + $RequestFile, + + [Parameter()] + [ValidateNotNull()] + [System.String] + $DomainController, + + [Parameter()] + [ValidateNotNull()] + [System.Management.Automation.PSCredential] + [System.Management.Automation.CredentialAttribute()] + $Credential, + + [Parameter()] + [ValidateNotNull()] + [System.Boolean] + $RestoreFromRecycleBin, + + [Parameter()] + [ValidateNotNull()] + [System.Boolean] + $EnabledOnCreation + ) + + Assert-Module -ModuleName 'ActiveDirectory' -ImportModule + + <# + These are properties that have no corresponding property in a + Computer account object. + #> + $getTargetResourceReturnValue = @{ + Ensure = 'Absent' + ComputerName = $null + Location = $null + DnsHostName = $null + ServicePrincipalNames = $null + UserPrincipalName = $null + DisplayName = $null + Path = $null + Description = $null + Enabled = $false + Manager = $null + DomainController = $DomainController + Credential = $Credential + RequestFile = $RequestFile + RestoreFromRecycleBin = $RestoreFromRecycleBin + EnabledOnCreation = $EnabledOnCreation + DistinguishedName = $null + SID = $null + SamAccountName = $null + } + + $getADComputerResult = $null + + try + { + <# + Create an array of the Active Directory Computer object property + names to retrieve from the Computer object. + #> + $computerObjectProperties = Convert-PropertyMapToObjectProperties -PropertyMap $script:computerObjectPropertyMap + + <# + When the property ServicePrincipalName is read with Get-ADComputer + the property name must be 'ServicePrincipalNames', but when it is + written with Set-ADComputer the property name must be + 'ServicePrincipalName'. This difference is handled here. + #> + $computerObjectProperties = @($computerObjectProperties | + Where-Object -FilterScript { + $_ -ne 'ServicePrincipalName' + }) + + $computerObjectProperties += @('ServicePrincipalNames') + + Write-Verbose -Message ($script:localizedData.RetrievingComputerAccount -f $ComputerName) + + $getADComputerParameters = Get-ADCommonParameters @PSBoundParameters + $getADComputerParameters['Properties'] = $computerObjectProperties + + # If the computer account is not found Get-ADComputer will throw an error. + $getADComputerResult = Get-ADComputer @getADComputerParameters + + Write-Verbose -Message ($script:localizedData.ComputerAccountIsPresent -f $ComputerName) + + $getTargetResourceReturnValue['Ensure'] = 'Present' + $getTargetResourceReturnValue['ComputerName'] = $getADComputerResult.CN + $getTargetResourceReturnValue['Location'] = $getADComputerResult.Location + $getTargetResourceReturnValue['DnsHostName'] = $getADComputerResult.DnsHostName + $getTargetResourceReturnValue['ServicePrincipalNames'] = [System.String[]] $getADComputerResult.ServicePrincipalNames + $getTargetResourceReturnValue['UserPrincipalName'] = $getADComputerResult.UserPrincipalName + $getTargetResourceReturnValue['DisplayName'] = $getADComputerResult.DisplayName + $getTargetResourceReturnValue['Path'] = Get-ADObjectParentDN -DN $getADComputerResult.DistinguishedName + $getTargetResourceReturnValue['Description'] = $getADComputerResult.Description + $getTargetResourceReturnValue['Enabled'] = $getADComputerResult.Enabled + $getTargetResourceReturnValue['Manager'] = $getADComputerResult.ManagedBy + $getTargetResourceReturnValue['DomainController'] = $DomainController + $getTargetResourceReturnValue['Credential'] = $Credential + $getTargetResourceReturnValue['RequestFile'] = $RequestFile + $getTargetResourceReturnValue['RestoreFromRecycleBin'] = $RestoreFromRecycleBin + $getTargetResourceReturnValue['EnabledOnCreation'] = $EnabledOnCreation + $getTargetResourceReturnValue['DistinguishedName'] = $getADComputerResult.DistinguishedName + $getTargetResourceReturnValue['SID'] = $getADComputerResult.SID + $getTargetResourceReturnValue['SamAccountName'] = $getADComputerResult.SamAccountName + } + catch [Microsoft.ActiveDirectory.Management.ADIdentityNotFoundException] + { + Write-Verbose -Message ($script:localizedData.ComputerAccountIsAbsent -f $ComputerName) + } + catch + { + $errorMessage = $script:localizedData.FailedToRetrieveComputerAccount -f $ComputerName + New-InvalidOperationException -Message $errorMessage -ErrorRecord $_ + } + + return $getTargetResourceReturnValue +} + +<# + .SYNOPSIS + Determines if the Active Directory computer account is in the desired state. + + .PARAMETER ComputerName + Specifies the name of the Active Directory computer account to manage. + You can identify a computer by its distinguished name, GUID, security + identifier (SID) or Security Accounts Manager (SAM) account name. + + .PARAMETER Ensure + Specifies whether the computer account is present or absent. + Valid values are 'Present' and 'Absent'. The default is 'Present'. + + .PARAMETER UserPrincipalName + Specifies the UPN assigned to the computer account. + + .PARAMETER DisplayName + Specifies the display name of the computer. + + .PARAMETER Path + Specifies the X.500 path of the container where the computer is located. + + .PARAMETER Location + Specifies the location of the computer, such as an office number. + + .PARAMETER DnsHostName + Specifies the fully qualified domain name (FQDN) of the computer. + + .PARAMETER ServicePrincipalNames + Specifies the service principal names for the computer account. + + .PARAMETER Description + Specifies a description of the computer account. + + .PARAMETER Manager + Specifies the user or group Distinguished Name that manages the computer + account. Valid values are the user's or group's DistinguishedName, + ObjectGUID, SID or SamAccountName. + + .PARAMETER RequestFile + Specifies the full path to the Offline Domain Join Request file to create. + + .PARAMETER DomainController + Specifies the Active Directory Domain Services instance to connect to perform the task. + + .PARAMETER Credential + Specifies the user account credentials to use to perform the task. + + .PARAMETER RestoreFromRecycleBin + Try to restore the organizational unit from the recycle bin before + creating a new one. + + .PARAMETER EnabledOnCreation + Specifies if the computer account is created enabled or disabled. + By default the Enabled property of the computer account will be set to + the default value of the cmdlet New-ADComputer. This property is ignored + if the parameter RequestFile is specified in the same configuration. + This parameter does not enforce the property `Enabled`. To enforce the + property `Enabled` see the resource ADObjectEnabledState. +#> +function Test-TargetResource +{ + [CmdletBinding()] + [OutputType([System.Boolean])] + param + ( + # Common Name + [Parameter(Mandatory = $true)] + [System.String] + $ComputerName, + + [Parameter()] + [ValidateSet('Present', 'Absent')] + [System.String] + $Ensure = 'Present', + + [Parameter()] + [ValidateNotNull()] + [System.String] + $UserPrincipalName, + + [Parameter()] + [ValidateNotNull()] + [System.String] + $DisplayName, + + [Parameter()] + [ValidateNotNull()] + [System.String] + $Path, + + [Parameter()] + [ValidateNotNull()] + [System.String] + $Location, + + [Parameter()] + [ValidateNotNull()] + [System.String] + $DnsHostName, + + [Parameter()] + [ValidateNotNull()] + [System.String[]] + $ServicePrincipalNames, + + [Parameter()] + [ValidateNotNull()] + [System.String] + $Description, + + [Parameter()] + [ValidateNotNull()] + [System.String] + $Manager, + + [Parameter()] + [ValidateNotNull()] + [System.String] + $RequestFile, + + [Parameter()] + [ValidateNotNull()] + [System.String] + $DomainController, + + [Parameter()] + [ValidateNotNull()] + [System.Management.Automation.PSCredential] + [System.Management.Automation.CredentialAttribute()] + $Credential, + + [Parameter()] + [ValidateNotNull()] + [System.Boolean] + $RestoreFromRecycleBin, + + [Parameter()] + [ValidateNotNull()] + [System.Boolean] + $EnabledOnCreation + ) + + Write-Verbose -Message ( + $script:localizedData.TestConfiguration -f $ComputerName + ) + + $getTargetResourceParameters = @{ + ComputerName = $ComputerName + RequestFile = $RequestFile + DomainController = $DomainController + Credential = $Credential + RestoreFromRecycleBin = $RestoreFromRecycleBin + EnabledOnCreation = $EnabledOnCreation + } + + # Need the @() around this to get a new array to enumerate. + @($getTargetResourceParameters.Keys) | + ForEach-Object { + if (-not $PSBoundParameters.ContainsKey($_)) + { + $getTargetResourceParameters.Remove($_) + } + } + + $getTargetResourceResult = Get-TargetResource @getTargetResourceParameters + + $testTargetResourceReturnValue = $true + + if ($Ensure -eq 'Absent') + { + if ($getTargetResourceResult.Ensure -eq 'Present') + { + Write-Verbose -Message ( + $script:localizedData.ComputerAccountShouldBeAbsent -f $ComputerName + ) + + $testTargetResourceReturnValue = $false + } + } + else + { + if ($getTargetResourceResult.Ensure -eq 'Absent') + { + Write-Verbose -Message ( + $script:localizedData.ComputerAccountShouldBePresent -f $ComputerName + ) + + $testTargetResourceReturnValue = $false + } + else + { + <# + - Ignores the parameter ComputerName since we are not supporting + renaming a computer account. + - Ignore to compare the parameter ServicePrincipalNames here + because it needs a special comparison, so it is handled + afterwards. + - Ignores the Enabled property because it is not enforced in this + resource. + #> + $compareTargetResourceStateParameters = @{ + CurrentValues = $getTargetResourceResult + DesiredValues = $PSBoundParameters + # This gives an array of properties to compare. + Properties = $script:computerObjectPropertyMap.ParameterName + # But these properties + IgnoreProperties = @( + 'ComputerName' + 'ServicePrincipalNames' + 'Enabled' + ) + } + + $compareTargetResourceStateResult = Compare-ResourcePropertyState @compareTargetResourceStateParameters + + if ($false -in $compareTargetResourceStateResult.InDesiredState) + { + $testTargetResourceReturnValue = $false + } + + if ($PSBoundParameters.ContainsKey('ServicePrincipalNames')) + { + $testServicePrincipalNamesParameters = @{ + ExistingServicePrincipalNames = $getTargetResourceResult.ServicePrincipalNames + ServicePrincipalNames = $ServicePrincipalNames + } + + $testTargetResourceReturnValue = Test-ServicePrincipalNames @testServicePrincipalNamesParameters + } + } + + } + + if ($testTargetResourceReturnValue) + { + Write-Verbose -Message ($script:localizedData.ComputerAccountInDesiredState -f $ComputerName) + } + else + { + Write-Verbose -Message ($script:localizedData.ComputerAccountNotInDesiredState -f $ComputerName) + } + + return $testTargetResourceReturnValue +} + +<# + .SYNOPSIS + Creates, removes or modifies the Active Directory computer account. + + .PARAMETER ComputerName + Specifies the name of the Active Directory computer account to manage. + You can identify a computer by its distinguished name, GUID, security + identifier (SID) or Security Accounts Manager (SAM) account name. + + .PARAMETER Ensure + Specifies whether the computer account is present or absent. + Valid values are 'Present' and 'Absent'. The default is 'Present'. + + .PARAMETER UserPrincipalName + Specifies the UPN assigned to the computer account. + + .PARAMETER DisplayName + Specifies the display name of the computer. + + .PARAMETER Path + Specifies the X.500 path of the container where the computer is located. + + .PARAMETER Location + Specifies the location of the computer, such as an office number. + + .PARAMETER DnsHostName + Specifies the fully qualified domain name (FQDN) of the computer. + + .PARAMETER ServicePrincipalNames + Specifies the service principal names for the computer account. + + .PARAMETER Description + Specifies a description of the computer account. + + .PARAMETER Manager + Specifies the user or group Distinguished Name that manages the computer + account. Valid values are the user's or group's DistinguishedName, + ObjectGUID, SID or SamAccountName. + + .PARAMETER RequestFile + Specifies the full path to the Offline Domain Join Request file to create. + + .PARAMETER DomainController + Specifies the Active Directory Domain Services instance to connect to perform the task. + + .PARAMETER Credential + Specifies the user account credentials to use to perform the task. + + .PARAMETER RestoreFromRecycleBin + Try to restore the organizational unit from the recycle bin before + creating a new one. + + .PARAMETER EnabledOnCreation + Specifies if the computer account is created enabled or disabled. + By default the Enabled property of the computer account will be set to + the default value of the cmdlet New-ADComputer. This property is ignored + if the parameter RequestFile is specified in the same configuration. + This parameter does not enforce the property `Enabled`. To enforce the + property `Enabled` see the resource ADObjectEnabledState. +#> +function Set-TargetResource +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $ComputerName, + + [Parameter()] + [ValidateSet('Present', 'Absent')] + [System.String] + $Ensure = 'Present', + + [Parameter()] + [ValidateNotNull()] + [System.String] + $UserPrincipalName, + + [Parameter()] + [ValidateNotNull()] + [System.String] + $DisplayName, + + [Parameter()] + [ValidateNotNull()] + [System.String] + $Path, + + [Parameter()] + [ValidateNotNull()] + [System.String] + $Location, + + [Parameter()] + [ValidateNotNull()] + [System.String] + $DnsHostName, + + [Parameter()] + [ValidateNotNull()] + [System.String[]] + $ServicePrincipalNames, + + [Parameter()] + [ValidateNotNull()] + [System.String] + $Description, + + [Parameter()] + [ValidateNotNull()] + [System.String] + $Manager, + + [Parameter()] + [ValidateNotNull()] + [System.String] + $RequestFile, + + [Parameter()] + [ValidateNotNull()] + [System.String] + $DomainController, + + [Parameter()] + [ValidateNotNull()] + [System.Management.Automation.PSCredential] + [System.Management.Automation.CredentialAttribute()] + $Credential, + + [Parameter()] + [ValidateNotNull()] + [System.Boolean] + $RestoreFromRecycleBin, + + [Parameter()] + [ValidateNotNull()] + [System.Boolean] + $EnabledOnCreation + ) + + $getTargetResourceParameters = @{ + ComputerName = $ComputerName + RequestFile = $RequestFile + DomainController = $DomainController + Credential = $Credential + RestoreFromRecycleBin = $RestoreFromRecycleBin + EnabledOnCreation = $EnabledOnCreation + } + + # Need the @() around this to get a new array to enumerate. + @($getTargetResourceParameters.Keys) | + ForEach-Object { + if (-not $PSBoundParameters.ContainsKey($_)) + { + $getTargetResourceParameters.Remove($_) + } + } + + $getTargetResourceResult = Get-TargetResource @getTargetResourceParameters + + if ($Ensure -eq 'Present') + { + if ($getTargetResourceResult.Ensure -eq 'Absent') + { + $restorationSuccessful = $false + + # Try to restore computer account from recycle bin if it exists. + if ($RestoreFromRecycleBin) + { + Write-Verbose -Message ( + $script:localizedData.RestoringComputerAccount -f $ComputerName + ) + + $restoreADCommonObjectParameters = Get-ADCommonParameters @PSBoundParameters + $restoreADCommonObjectParameters['ObjectClass'] = 'Computer' + $restoreADCommonObjectParameters['ErrorAction'] = 'Stop' + + $restorationSuccessful = Restore-ADCommonObject @restoreADCommonObjectParameters + } + + if (-not $RestoreFromRecycleBin -or ($RestoreFromRecycleBin -and -not $restorationSuccessful)) + { + <# + The computer account does not exist, or the computer account + was not present in recycle bin, so the computer account needs + to be created. + #> + + if ($RequestFile) + { + <# + Use DJOIN to create the computer account as well as the + Offline Domain Join (ODJ) request file. + #> + + # This should only be performed on a Domain Member, so detect the Domain Name. + $domainName = Get-DomainName + + Write-Verbose -Message ( + $script:localizedData.CreateOfflineDomainJoinRequest -f $RequestFile, $ComputerName, $domainName + ) + + $dJoinArguments = @( + '/PROVISION' + '/DOMAIN', + $domainName + '/MACHINE', + $ComputerName + ) + + if ($PSBoundParameters.ContainsKey('Path')) + { + $dJoinArguments += @( + '/MACHINEOU', + $Path + ) + } + + if ($PSBoundParameters.ContainsKey('DomainController')) + { + $dJoinArguments += @( + '/DCNAME', + $DomainController + ) + } + + $dJoinArguments += @( + '/SAVEFILE', + $RequestFile + ) + + $startProcessParameters = @{ + FilePath = 'djoin.exe' + ArgumentList = $dJoinArguments + Timeout = 300 + } + + $dJoinProcessExitCode = Start-ProcessWithTimeout @startProcessParameters + + if ($dJoinProcessExitCode -ne 0) + { + $errorMessage = $script:localizedData.FailedToCreateOfflineDomainJoinRequest -f $ComputerName, $dJoinProcessExitCode + New-InvalidOperationException -Message $errorMessage + } + else + { + Write-Verbose -Message ( + $script:localizedData.CreatedOfflineDomainJoinRequestFile -f $RequestFile + ) + } + } + else + { + $newADComputerParameters = Get-ADCommonParameters @PSBoundParameters -UseNameParameter + + if ($PSBoundParameters.ContainsKey('Path')) + { + Write-Verbose -Message ( + $script:localizedData.CreateComputerAccountInPath -f $ComputerName, $Path + ) + + $newADComputerParameters['Path'] = $Path + } + else + { + Write-Verbose -Message ( + $script:localizedData.CreateComputerAccount -f $ComputerName + ) + } + + <# + If the parameter EnabledOnCreation is specified, then the + property Enabled is set to that value. + #> + if ($PSBoundParameters.ContainsKey('EnabledOnCreation')) + { + if ($EnabledOnCreation) + { + Write-Verbose -Message ($script:localizedData.EnabledComputerAccount -f $ComputerName) + } + else + { + Write-Verbose -Message ($script:localizedData.DisabledComputerAccount -f $ComputerName) + } + + $newADComputerParameters['Enabled'] = $EnabledOnCreation + } + + New-ADComputer @newADComputerParameters + } + } + + <# + Now retrieve the newly created computer account so the other + properties can be set if specified. + #> + $getTargetResourceResult = Get-TargetResource @getTargetResourceParameters + } + + <# + - Ignores the parameter ComputerName since we are not supporting + renaming a computer account. + - Ignore to compare the parameter ServicePrincipalNames here + because it needs a special comparison, so it is handled + afterwards. + - Ignores the Enabled property because it is not enforced in this + resource. + #> + $compareTargetResourceStateParameters = @{ + CurrentValues = $getTargetResourceResult + DesiredValues = $PSBoundParameters + # This gives an array of properties to compare. + Properties = $script:computerObjectPropertyMap.ParameterName + # But these properties + IgnoreProperties = @( + 'ComputerName' + 'ServicePrincipalNames' + 'Enabled' + ) + } + + $compareTargetResourceStateResult = Compare-ResourcePropertyState @compareTargetResourceStateParameters + + if ($PSBoundParameters.ContainsKey('ServicePrincipalNames')) + { + $testServicePrincipalNamesParameters = @{ + ExistingServicePrincipalNames = $getTargetResourceResult.ServicePrincipalNames + ServicePrincipalNames = $ServicePrincipalNames + } + + $compareTargetResourceStateResult += @{ + ParameterName = 'ServicePrincipalNames' + Expected = $testServicePrincipalNamesParameters.ServicePrincipalNames + Actual = $testServicePrincipalNamesParameters.ExistingServicePrincipalNames + InDesiredState = Test-ServicePrincipalNames @testServicePrincipalNamesParameters + } + } + + $commonParameters = Get-ADCommonParameters @PSBoundParameters + + if ($compareTargetResourceStateResult.Where( { $_.ParameterName -eq 'Path' -and -not $_.InDesiredState })) + { + <# + Must move the computer account since we can't simply + update the DistinguishedName property + + It does not work moving the computer account using the + SamAccountName as the identity, so using the property + DistinguishedName instead. + #> + $moveADObjectParameters = $commonParameters.Clone() + $moveADObjectParameters['Identity'] = $getTargetResourceResult.DistinguishedName + + Write-Verbose -Message ( + $script:localizedData.MovingComputerAccount -f $ComputerName, $getTargetResourceResult.Path, $Path + ) + + Move-ADObject @moveADObjectParameters -TargetPath $Path + } + + $replaceComputerProperties = @{} + $removeComputerProperties = @{} + + # Get all properties, other than Path, that is not in desired state. + $propertiesNotInDesiredState = $compareTargetResourceStateResult | + Where-Object -FilterScript { + $_.ParameterName -ne 'Path' -and -not $_.InDesiredState + } + + foreach ($property in $propertiesNotInDesiredState) + { + $computerAccountPropertyName = ($script:computerObjectPropertyMap | + Where-Object -FilterScript { + $_.ParameterName -eq $property.ParameterName + }).PropertyName + + if (-not $computerAccountPropertyName) + { + $computerAccountPropertyName = $property.ParameterName + } + + if ($property.Expected) + { + Write-Verbose -Message ( + $script:localizedData.UpdatingComputerAccountProperty -f $computerAccountPropertyName, ($property.Expected -join ''',''') + ) + + # Replace the current value. + $replaceComputerProperties[$computerAccountPropertyName] = $property.Expected + } + else + { + Write-Verbose -Message ( + $script:localizedData.RemovingComputerAccountProperty -f $property.ParameterName, ($property.Actual -join ''',''') + ) + + # Remove the current value since the desired value is empty or nothing. + $removeComputerProperties[$computerAccountPropertyName] = $property.Actual + } + } + + $setADComputerParameters = $commonParameters.Clone() + + # Set-ADComputer is only called if we have something to change. + if ($replaceComputerProperties.Count -gt 0 -or $removeComputerProperties.Count -gt 0) + { + if ($replaceComputerProperties.Count -gt 0) + { + $setADComputerParameters['Replace'] = $replaceComputerProperties + } + if ($removeComputerProperties.Count -gt 0) + { + $setADComputerParameters['Remove'] = $removeComputerProperties + } + + Set-DscADComputer -Parameters $setADComputerParameters + + Write-Verbose -Message ( + $script:localizedData.UpdatedComputerAccount -f $ComputerName + ) + } + } + elseif ($Ensure -eq 'Absent' -and $getTargetResourceResult.Ensure -eq 'Present') + { + # User exists and needs removing + Write-Verbose -Message ( + $script:localizedData.RemovingComputerAccount -f $ComputerName + ) + + $removeADComputerParameters = Get-ADCommonParameters @PSBoundParameters + $removeADComputerParameters['Confirm'] = $false + + Remove-ADComputer @removeADComputerParameters | + Out-Null + } +} + +<# + .SYNOPSIS + This evaluates the service principal names current state against the + desired state. + + .PARAMETER ExistingServicePrincipalNames + An array of existing service principal names that should be compared + against the array in parameter ServicePrincipalNames. + + .PARAMETER ServicePrincipalNames + An array of the desired service principal names that should be compared + against the array in parameter ExistingServicePrincipalNames. +#> +function Test-ServicePrincipalNames +{ + param + ( + [Parameter(Mandatory = $true)] + [AllowEmptyCollection()] + [System.String[]] + $ExistingServicePrincipalNames, + + [Parameter(Mandatory = $true)] + [AllowEmptyCollection()] + [AllowEmptyString()] + [System.String[]] + $ServicePrincipalNames + + ) + + $testServicePrincipalNamesReturnValue = $true + + $testMembersParameters = @{ + ExistingMembers = $ExistingServicePrincipalNames + Members = $ServicePrincipalNames + } + + if (-not (Test-Members @testMembersParameters)) + { + Write-Verbose -Message ( + $script:localizedData.ServicePrincipalNamesNotInDesiredState ` + -f ($ExistingServicePrincipalNames -join ','), ($ServicePrincipalNames -join ',') + ) + + $testServicePrincipalNamesReturnValue = $false + } + else + { + Write-Verbose -Message ( + $script:localizedData.ServicePrincipalNamesInDesiredState + ) + } + + return $testServicePrincipalNamesReturnValue +} + +Export-ModuleMember -Function *-TargetResource diff --git a/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_ADComputer/MSFT_ADComputer.schema.mof b/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_ADComputer/MSFT_ADComputer.schema.mof new file mode 100644 index 0000000..04a4b79 --- /dev/null +++ b/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_ADComputer/MSFT_ADComputer.schema.mof @@ -0,0 +1,23 @@ +[ClassVersion("1.0.0.0"), FriendlyName("ADComputer")] +class MSFT_ADComputer : OMI_BaseResource +{ + [Key, Description("Specifies the name of the Active Directory computer account to manage. You can identify a computer by its distinguished name, GUID, security identifier (SID) or Security Accounts Manager (SAM) account name.")] String ComputerName; + [Write, Description("Specifies the location of the computer, such as an office number.")] String Location; + [Write, Description("Specifies the fully qualified domain name (FQDN) of the computer account.")] String DnsHostName; + [Write, Description("Specifies the service principal names for the computer account.")] String ServicePrincipalNames[]; + [Write, Description("Specifies the User Principal Name (UPN) assigned to the computer account.")] String UserPrincipalName; + [Write, Description("Specifies the display name of the computer account.")] String DisplayName; + [Write, Description("Specifies the X.500 path of the Organizational Unit (OU) or container where the computer is located.")] String Path; + [Write, Description("Specifies a description of the computer account.")] String Description; + [Write, Description("Specifies the user or group Distinguished Name that manages the computer account. Valid values are the user's or group's DistinguishedName, ObjectGUID, SID or SamAccountName.")] String Manager; + [Write, Description("Specifies the Active Directory Domain Services instance to connect to perform the task.")] String DomainController; + [Write, Description("Specifies the user account credentials to use to perform the task."), EmbeddedInstance("MSFT_Credential")] String Credential; + [Write, Description("Specifies the full path to the Offline Domain Join Request file to create.")] String RequestFile; + [Write, Description("Specifies whether the computer account is present or absent. Default value is 'Present'."), ValueMap{"Present", "Absent"},Values{"Present", "Absent"}] String Ensure; + [Write, Description("Try to restore the computer account from the recycle bin before creating a new one.")] Boolean RestoreFromRecycleBin; + [Write, Description("Specifies if the computer account is created enabled or disabled. By default the Enabled property of the computer account will be set to the default value of the cmdlet New-ADComputer. This property is ignored if the parameter RequestFile is specified in the same configuration. This parameter does not enforce the property Enabled. To enforce the property Enabled see the resource ADObjectEnabledState.")] Boolean EnabledOnCreation; + [Read, Description("Returns the X.500 path of the computer object.")] String DistinguishedName; + [Read, Description("Returns the security identifier of the computer object.")] String SID; + [Read, Description("Returns the SAM account name of the computer object.")] String SamAccountName; + [Read, Description("Returns $true if the computer object is enabled, otherwise it returns $false.")] Boolean Enabled; +}; diff --git a/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_ADComputer/README.md b/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_ADComputer/README.md new file mode 100644 index 0000000..e70463d --- /dev/null +++ b/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_ADComputer/README.md @@ -0,0 +1,20 @@ +# Description + +The ADComputer DSC resource will manage computer accounts within Active Directory. +This resource can be used to provision a computer account before the computer is +added to the domain. These pre-created computer objects can be used with offline +domain join, unsecure domain Join and RODC domain join scenarios. + +>**Note:** An Offline Domain Join (ODJ) request file will only be created +>when a computer account is first created in the domain. Setting an Offline +>Domain Join (ODJ) Request file path for a configuration that updates a +>computer account that already exists, or restore it from the recycle bin +>will not cause the Offline Domain Join (ODJ) request file to be created. + +## Requirements + +* Target machine must be running Windows Server 2008 R2 or later. +* The parameter `RestoreFromRecycleBin` requires that the feature Recycle + Bin has been enabled prior to an object is deleted. If the feature + Recycle Bin is disabled then the property `msDS-LastKnownRDN` is not + added the deleted object. diff --git a/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_ADComputer/en-US/MSFT_ADComputer.strings.psd1 b/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_ADComputer/en-US/MSFT_ADComputer.strings.psd1 new file mode 100644 index 0000000..d160755 --- /dev/null +++ b/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_ADComputer/en-US/MSFT_ADComputer.strings.psd1 @@ -0,0 +1,27 @@ +# culture="en-US" +ConvertFrom-StringData @' + RetrievingComputerAccount = Retrieving the information about the computer account '{0}' from Active Directory. (ADC0002) + ComputerAccountIsPresent = The computer account '{0}' is present in Active Directory. (ADC0003) + ComputerAccountIsAbsent = The computer account '{0}' is absent from Active Directory. (ADC0004) + FailedToRetrieveComputerAccount = Failed to retrieve the computer account '{0}' from Active Directory. (ADC0005) + TestConfiguration = Determining the current state of the computer account '{0}'. (ADC0006) + ComputerAccountShouldBeAbsent = The computer account '{0}' is present in Active Directory, but expected it to be absent. (ADC0007) + ComputerAccountShouldBePresent = The computer account '{0}' is absent in Active Directory, but expected it to be present. (ADC0008) + ServicePrincipalNamesInDesiredState = The service principal names was in desired state. (ADC0009) + ServicePrincipalNamesNotInDesiredState = The service principal names was '{0}', but expected them to be '{1}'. (ADC0010) + ComputerAccountInDesiredState = The computer account '{0}' is in the desired state. (ADC0011) + ComputerAccountNotInDesiredState = The computer account '{0}' is not in the desired state. (ADC0012) + RestoringComputerAccount = Attempting to restore the computer object {0} from recycle bin. (ADC0013) + FailedToCreateOfflineDomainJoinRequest = Failed to create the Offline Domain Join (ODJ) request file for the computer account '{0}' with the error code '{1}'. (ADC0014) + CreateOfflineDomainJoinRequest = Attempting to create the Offline Domain Join (ODJ) request file '{0}' for the computer account '{1}' in the domain '{2}'. (ADC0015) + CreatedOfflineDomainJoinRequestFile = The Offline Domain Join (ODJ) request file '{0}' was created successfully. (ADC0016) + CreateComputerAccount = The computer account '{0}' is created in Active Directory, at the default path. (ADC0017) + CreateComputerAccountInPath = The computer account '{0}' is created in Active Directory, at the path '{1}'. (ADC0018) + DisabledComputerAccount = The computer account '{0}' is created disabled. (ADC0019) + EnabledComputerAccount = The computer account '{0}' is created enabled. (ADC0020) + MovingComputerAccount = Moving the computer account '{0}' from the path '{1}' to the path '{2}'. (ADC0021) + UpdatingComputerAccountProperty = Updating the computer account property '{0}' with the value(s) '{1}'. (ADC0022) + RemovingComputerAccountProperty = Removing the value(s) '{1}' from the computer account property '{0}'. (ADC0023) + UpdatedComputerAccount = The computer account '{0}' was updated in Active Directory. (ADC0024) + RemovingComputerAccount = Removing the computer account '{0}' from Active Directory. (ADC0025) +'@ diff --git a/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_ADComputer/en-US/about_ADComputer.help.txt b/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_ADComputer/en-US/about_ADComputer.help.txt new file mode 100644 index 0000000..582c045 --- /dev/null +++ b/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_ADComputer/en-US/about_ADComputer.help.txt @@ -0,0 +1,275 @@ +.NAME + ADComputer + +.DESCRIPTION + The ADComputer DSC resource will manage computer accounts within Active Directory. + This resource can be used to provision a computer account before the computer is + added to the domain. These pre-created computer objects can be used with offline + domain join, unsecure domain Join and RODC domain join scenarios. + + >**Note:** An Offline Domain Join (ODJ) request file will only be created + >when a computer account is first created in the domain. Setting an Offline + >Domain Join (ODJ) Request file path for a configuration that updates a + >computer account that already exists, or restore it from the recycle bin + >will not cause the Offline Domain Join (ODJ) request file to be created. + + ## Requirements + + * Target machine must be running Windows Server 2008 R2 or later. + * The parameter `RestoreFromRecycleBin` requires that the feature Recycle + Bin has been enabled prior to an object is deleted. If the feature + Recycle Bin is disabled then the property `msDS-LastKnownRDN` is not + added the deleted object. + +.PARAMETER ComputerName + Key - String + Specifies the name of the Active Directory computer account to manage. You can identify a computer by its distinguished name, GUID, security identifier (SID) or Security Accounts Manager (SAM) account name. + +.PARAMETER Location + Write - String + Specifies the location of the computer, such as an office number. + +.PARAMETER DnsHostName + Write - String + Specifies the fully qualified domain name (FQDN) of the computer account. + +.PARAMETER ServicePrincipalNames + Write - StringArray + Specifies the service principal names for the computer account. + +.PARAMETER UserPrincipalName + Write - String + Specifies the User Principal Name (UPN) assigned to the computer account. + +.PARAMETER DisplayName + Write - String + Specifies the display name of the computer account. + +.PARAMETER Path + Write - String + Specifies the X.500 path of the Organizational Unit (OU) or container where the computer is located. + +.PARAMETER Description + Write - String + Specifies a description of the computer account. + +.PARAMETER Manager + Write - String + Specifies the user or group Distinguished Name that manages the computer account. Valid values are the user's or group's DistinguishedName, ObjectGUID, SID or SamAccountName. + +.PARAMETER DomainController + Write - String + Specifies the Active Directory Domain Services instance to connect to perform the task. + +.PARAMETER Credential + Write - PSCredential + Specifies the user account credentials to use to perform the task. + +.PARAMETER RequestFile + Write - String + Specifies the full path to the Offline Domain Join Request file to create. + +.PARAMETER Ensure + Write - String + Allowed values: Present, Absent + Specifies whether the computer account is present or absent. Default value is 'Present'. + +.PARAMETER RestoreFromRecycleBin + Write - Boolean + Try to restore the computer account from the recycle bin before creating a new one. + +.PARAMETER EnabledOnCreation + Write - Boolean + Specifies if the computer account is created enabled or disabled. By default the Enabled property of the computer account will be set to the default value of the cmdlet New-ADComputer. This property is ignored if the parameter RequestFile is specified in the same configuration. This parameter does not enforce the property Enabled. To enforce the property Enabled see the resource ADObjectEnabledState. + +.PARAMETER DistinguishedName + Read - String + Returns the X.500 path of the computer object. + +.PARAMETER SID + Read - String + Returns the security identifier of the computer object. + +.PARAMETER SamAccountName + Read - String + Returns the SAM account name of the computer object. + +.PARAMETER Enabled + Read - Boolean + Returns $true if the computer object is enabled, otherwise it returns $false. + +.EXAMPLE 1 + +This configuration will create two Active Directory computer accounts +enabled. The property Enabled will not be enforced in either case. + +Configuration ADComputer_AddComputerAccount_Config +{ + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.Management.Automation.PSCredential] + $Credential + ) + + Import-DscResource -ModuleName ActiveDirectoryDsc + + node localhost + { + ADComputer 'CreateEnabled_SQL01' + { + ComputerName = 'SQL01' + + PsDscRunAsCredential = $Credential + } + + ADComputer 'CreateEnabled_SQL02' + { + ComputerName = 'SQL02' + EnabledOnCreation = $true + + PsDscRunAsCredential = $Credential + } + } +} + +.EXAMPLE 2 + +This configuration will create an Active Directory computer account +disabled. The property Enabled will not be enforced. + +Configuration ADComputer_AddComputerAccountDisabled_Config +{ + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.Management.Automation.PSCredential] + $Credential + ) + + Import-DscResource -ModuleName ActiveDirectoryDsc + + node localhost + { + ADComputer 'CreateDisabled' + { + ComputerName = 'CLU_CNO01' + EnabledOnCreation = $false + + PsDscRunAsCredential = $Credential + } + } +} + +.EXAMPLE 3 + +This configuration will create an Active Directory computer account +on the specified domain controller and in the specific organizational +unit. + +Configuration ADComputer_AddComputerAccountSpecificPath_Config +{ + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.Management.Automation.PSCredential] + $Credential + ) + + Import-DscResource -ModuleName ActiveDirectoryDsc + + node localhost + { + ADComputer 'CreateComputerAccount' + { + DomainController = 'DC01' + ComputerName = 'SQL01' + Path = 'OU=Servers,DC=contoso,DC=com' + Credential = $Credential + } + } +} + +.EXAMPLE 4 + +This configuration will create an Active Directory computer account +on the specified domain controller and in the specific organizational +unit. After the account is create an Offline Domain Join Request file +is created to the specified path. + +Configuration ADComputer_AddComputerAccountAndCreateODJRequest_Config +{ + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.Management.Automation.PSCredential] + $Credential + ) + + Import-DscResource -ModuleName ActiveDirectoryDsc + + node localhost + { + ADComputer 'CreateComputerAccount' + { + DomainController = 'DC01' + ComputerName = 'NANO-200' + Path = 'OU=Servers,DC=contoso,DC=com' + RequestFile = 'D:\ODJFiles\NANO-200.txt' + Credential = $Credential + } + } +} + +.EXAMPLE 5 + +This configuration will create a computer account disabled, configure +a cluster using the disabled computer account, and enforcing the +computer account to be enabled. + +Configuration ADComputer_CreateClusterComputerAccount_Config +{ + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.Management.Automation.PSCredential] + $Credential + ) + + Import-DscResource -ModuleName ActiveDirectoryDsc + Import-DscResource -ModuleName xFailoverCluster -ModuleVersion '1.14.1' + + node localhost + { + ADComputer 'ClusterAccount' + { + ComputerName = 'CLU_CNO01' + EnabledOnCreation = $false + } + + xCluster 'CreateCluster' + { + Name = 'CLU_CNO01' + StaticIPAddress = '192.168.100.20/24' + DomainAdministratorCredential = $Credential + + DependsOn = '[ADComputer]ClusterAccount' + } + + ADObjectEnabledState 'EnforceEnabledPropertyToEnabled' + { + Identity = 'CLU_CNO01' + ObjectClass = 'Computer' + Enabled = $true + + DependsOn = '[xCluster]CreateCluster' + } + } +} + + diff --git a/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_ADDomain/MSFT_ADDomain.psm1 b/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_ADDomain/MSFT_ADDomain.psm1 new file mode 100644 index 0000000..bd6f8e9 --- /dev/null +++ b/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_ADDomain/MSFT_ADDomain.psm1 @@ -0,0 +1,555 @@ +$script:resourceModulePath = Split-Path -Path (Split-Path -Path $PSScriptRoot -Parent) -Parent +$script:modulesFolderPath = Join-Path -Path $script:resourceModulePath -ChildPath 'Modules' + +$script:localizationModulePath = Join-Path -Path $script:modulesFolderPath -ChildPath 'ActiveDirectoryDsc.Common' +Import-Module -Name (Join-Path -Path $script:localizationModulePath -ChildPath 'ActiveDirectoryDsc.Common.psm1') + +$script:localizedData = Get-LocalizedData -ResourceName 'MSFT_ADDomain' + +<# + .SYNOPSIS + Get the current state of the Domain. + + .PARAMETER DomainName + The fully qualified domain name (FQDN) of a new domain. If setting up a + child domain this must be set to a single-label DNS name. + + .PARAMETER Credential + Specifies the user name and password that corresponds to the account used to install + the domain controller. These are only used when adding a child domain and these credentials + need the correct permission in the parent domain. This will not be created as a user in the + new domain. The domain administrator password will be the same as the password of the local + Administrator of this node. + + .PARAMETER SafeModeAdministratorPassword + Password for the administrator account when the computer is started in Safe Mode. + + .PARAMETER ParentDomainName + Fully qualified domain name (FQDN) of the parent domain. + + .NOTES + Used Functions: + Name | Module + -------------------------------|-------------------------- + Get-ADDomain | ActiveDirectory + Get-ADForest | ActiveDirectory + Assert-Module | ActiveDirectoryDsc.Common + Resolve-DomainFQDN | ActiveDirectoryDsc.Common + New-InvalidOperationException | ActiveDirectoryDsc.Common + ConvertTo-DeploymentForestMode | ActiveDirectoryDsc.Common + ConvertTo-DeploymentDomainMode | ActiveDirectoryDsc.Common +#> +function Get-TargetResource +{ + [OutputType([System.Collections.Hashtable])] + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $DomainName, + + [Parameter(Mandatory = $true)] + [System.Management.Automation.PSCredential] + $Credential, + + [Parameter(Mandatory = $true)] + [System.Management.Automation.PSCredential] + $SafeModeAdministratorPassword, + + [Parameter()] + [ValidateNotNullOrEmpty()] + [System.String] + $ParentDomainName + ) + + Assert-Module -ModuleName 'ADDSDeployment' -ImportModule + $domainFQDN = Resolve-DomainFQDN -DomainName $DomainName -ParentDomainName $ParentDomainName + + # If the domain has been installed then the Netlogon SysVol registry item will exist. + $domainShouldBePresent = $true + try + { + $sysvolPath = Get-ItemPropertyValue -Path 'HKLM:\SYSTEM\CurrentControlSet\Services\Netlogon\Parameters' -Name 'SysVol' + } + catch + { + $domainShouldBePresent = $false + } + + if ($domainShouldBePresent) + { + # Test that the correct domain SysVol path exists + $domainSysVolPath = Join-Path -Path $sysvolPath -ChildPath $domainFQDN + + if (-not (Test-Path -Path $domainSysVolPath)) + { + $errorMessage = $script:localizedData.SysVolPathDoesNotExistError -f $domainSysVolPath + New-InvalidOperationException -Message $errorMessage + } + + Write-Verbose ($script:localizedData.QueryDomain -f $domainFQDN) + + $retries = 0 + $maxRetries = 15 + $retryIntervalInSeconds = 30 + + do + { + $domainFound = $true + try + { + $domain = Get-ADDomain -Identity $domainFQDN -Server localhost -ErrorAction Stop + } + catch [Microsoft.ActiveDirectory.Management.ADServerDownException], ` + [System.Security.Authentication.AuthenticationException], ` + [System.InvalidOperationException], ` + [System.ArgumentException] + { + Write-Verbose ($script:localizedData.ADServerNotReady -f $domainFQDN) + $domainFound = $false + # will fall into the retry mechanism. + } + catch + { + $errorMessage = $script:localizedData.GetAdDomainUnexpectedError -f $domainFQDN + New-InvalidOperationException -Message $errorMessage -ErrorRecord $_ + } + + if (-not $domainFound) + { + $retries++ + + Write-Verbose ($script:localizedData.RetryingGetADDomain -f + $retries, $maxRetries, $retryIntervalInSeconds) + + Start-Sleep -Seconds $retryIntervalInSeconds + } + } while ((-not $domainFound) -and $retries -lt $maxRetries) + + if ($retries -eq $maxRetries) + { + $errorMessage = $script:localizedData.MaxDomainRetriesReachedError -f $domainFQDN + New-InvalidOperationException -Message $errorMessage + } + } + else + { + $domain = $null + } + + if ($domain) + { + Write-Verbose ($script:localizedData.DomainFound -f $domain.DnsRoot) + + try + { + $forest = Get-ADForest -Identity $domain.Forest -Server localhost -ErrorAction Stop + } + catch + { + $errorMessage = $script:localizedData.GetAdForestUnexpectedError -f $domain.Forest + New-InvalidOperationException -Message $errorMessage -ErrorRecord $_ + } + + $deploymentForestMode = (ConvertTo-DeploymentForestMode -Mode $forest.ForestMode) -as [System.String] + $deploymentDomainMode = (ConvertTo-DeploymentDomainMode -Mode $domain.DomainMode) -as [System.String] + $serviceNTDS = Get-ItemProperty -Path 'HKLM:\SYSTEM\CurrentControlSet\Services\NTDS\Parameters' + $serviceNETLOGON = Get-ItemProperty -Path 'HKLM:\SYSTEM\CurrentControlSet\Services\Netlogon\Parameters' + + $returnValue = @{ + DomainName = $DomainName + Credential = $Credential + SafeModeAdministratorPassword = $SafeModeAdministratorPassword + ParentDomainName = $domain.ParentDomain + DomainNetBiosName = $domain.NetBIOSName + DnsDelegationCredential = $null + DatabasePath = $serviceNTDS.'DSA Working Directory' + LogPath = $serviceNTDS.'Database log files path' + SysvolPath = $serviceNETLOGON.SysVol -replace '\\sysvol$', '' + ForestMode = $deploymentForestMode + DomainMode = $deploymentDomainMode + DomainExist = $true + Forest = $forest.Name + DnsRoot = $domain.DnsRoot + } + } + else + { + $returnValue = @{ + DomainName = $DomainName + Credential = $Credential + SafeModeAdministratorPassword = $SafeModeAdministratorPassword + ParentDomainName = $ParentDomainName + DomainNetBiosName = $null + DnsDelegationCredential = $null + DatabasePath = $null + LogPath = $null + SysvolPath = $null + ForestMode = $null + DomainMode = $null + DomainExist = $false + Forest = $null + DnsRoot = $null + } + } + + return $returnValue +} #end function Get-TargetResource + +<# + .SYNOPSIS + Tests the current state of the Domain. + + .PARAMETER DomainName + The fully qualified domain name (FQDN) of a new domain. If setting up a + child domain this must be set to a single-label DNS name. + + .PARAMETER Credential + Specifies the user name and password that corresponds to the account used to install + the domain controller. These are only used when adding a child domain and these credentials + need the correct permission in the parent domain. This will not be created as a user in the + new domain. The domain administrator password will be the same as the password of the local + Administrator of this node. + + .PARAMETER SafeModeAdministratorPassword + Password for the administrator account when the computer is started in Safe Mode. + + .PARAMETER ParentDomainName + Fully qualified domain name (FQDN) of the parent domain. + + .PARAMETER DomainNetBiosName + NetBIOS name for the new domain. + + .PARAMETER DnsDelegationCredential + Credential used for creating DNS delegation. + + .PARAMETER DatabasePath + Path to a directory that contains the domain database. + + .PARAMETER LogPath + Path to a directory for the log file that will be written. + + .PARAMETER SysvolPath + Path to a directory where the Sysvol file will be written. + + .PARAMETER ForestMode + The Forest Functional Level for the entire forest. + + .PARAMETER DomainMode + The Domain Functional Level for the entire domain. + + .NOTES + Used Functions: + Name | Module + -------------------|-------------------------- + Resolve-DomainFQDN | ActiveDirectoryDsc.Common +#> +function Test-TargetResource +{ + [OutputType([System.Boolean])] + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $DomainName, + + [Parameter(Mandatory = $true)] + [System.Management.Automation.PSCredential] + $Credential, + + [Parameter(Mandatory = $true)] + [System.Management.Automation.PSCredential] + $SafeModeAdministratorPassword, + + [Parameter()] + [ValidateNotNullOrEmpty()] + [System.String] + $ParentDomainName, + + [Parameter()] + [ValidateNotNullOrEmpty()] + [System.String] + $DomainNetBiosName, + + [Parameter()] + [ValidateNotNullOrEmpty()] + [System.Management.Automation.PSCredential] + $DnsDelegationCredential, + + [Parameter()] + [ValidateNotNullOrEmpty()] + [System.String] + $DatabasePath, + + [Parameter()] + [ValidateNotNullOrEmpty()] + [System.String] + $LogPath, + + [Parameter()] + [ValidateNotNullOrEmpty()] + [System.String] + $SysvolPath, + + [Parameter()] + [ValidateSet('Win2008', 'Win2008R2', 'Win2012', 'Win2012R2', 'WinThreshold')] + [System.String] + $ForestMode, + + [Parameter()] + [ValidateSet('Win2008', 'Win2008R2', 'Win2012', 'Win2012R2', 'WinThreshold')] + [System.String] + $DomainMode + ) + + $getTargetResourceParameters = @{ + DomainName = $DomainName + Credential = $Credential + SafeModeAdministratorPassword = $SafeModeAdministratorPassword + ParentDomainName = $ParentDomainName + } + + @($getTargetResourceParameters.Keys) | + ForEach-Object { + if (-not $PSBoundParameters.ContainsKey($_)) + { + $getTargetResourceParameters.Remove($_) + } + } + + $targetResource = Get-TargetResource @getTargetResourceParameters + + $domainFQDN = Resolve-DomainFQDN -DomainName $DomainName -ParentDomainName $ParentDomainName + + if ($targetResource.DomainExist) + { + Write-Verbose -Message ($script:localizedData.DomainInDesiredState -f + $domainFQDN) + $inDesiredState = $true + } + else + { + Write-Verbose -Message ($script:localizedData.DomainNotInDesiredState -f + $domainFQDN) + $inDesiredState = $false + } + + return $inDesiredState +} #end function Test-TargetResource + +<# + .SYNOPSIS + Sets the state of the Domain. + + .PARAMETER DomainName + The fully qualified domain name (FQDN) of a new domain. If setting up a + child domain this must be set to a single-label DNS name. + + .PARAMETER Credential + Specifies the user name and password that corresponds to the account used to install + the domain controller. These are only used when adding a child domain and these credentials + need the correct permission in the parent domain. This will not be created as a user in the + new domain. The domain administrator password will be the same as the password of the local + Administrator of this node. + + .PARAMETER SafeModeAdministratorPassword + Password for the administrator account when the computer is started in Safe Mode. + + .PARAMETER ParentDomainName + Fully qualified domain name (FQDN) of the parent domain. + + .PARAMETER DomainNetBiosName + NetBIOS name for the new domain. + + .PARAMETER DnsDelegationCredential + Credential used for creating DNS delegation. + + .PARAMETER DatabasePath + Path to a directory that contains the domain database. + + .PARAMETER LogPath + Path to a directory for the log file that will be written. + + .PARAMETER SysvolPath + Path to a directory where the Sysvol file will be written. + + .PARAMETER ForestMode + The Forest Functional Level for the entire forest. + + .PARAMETER DomainMode + The Domain Functional Level for the entire domain. + + .NOTES + Used Functions: + Name | Module + -------------------------------|-------------------------- + Install-ADDSDomain | ActiveDirectory + Install-ADDSForest | ActiveDirectory +#> +function Set-TargetResource +{ + <# + Suppressing this rule because $global:DSCMachineStatus is used to + trigger a reboot for the one that was suppressed when calling + Install-ADDSForest or Install-ADDSDomain. + #> + [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidGlobalVars', '')] + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $DomainName, + + [Parameter(Mandatory = $true)] + [System.Management.Automation.PSCredential] + $Credential, + + [Parameter(Mandatory = $true)] + [System.Management.Automation.PSCredential] + $SafeModeAdministratorPassword, + + [Parameter()] + [ValidateNotNullOrEmpty()] + [System.String] + $ParentDomainName, + + [Parameter()] + [ValidateNotNullOrEmpty()] + [System.String] + $DomainNetBiosName, + + [Parameter()] + [ValidateNotNullOrEmpty()] + [System.Management.Automation.PSCredential] + $DnsDelegationCredential, + + [Parameter()] + [ValidateNotNullOrEmpty()] + [System.String] + $DatabasePath, + + [Parameter()] + [ValidateNotNullOrEmpty()] + [System.String] + $LogPath, + + [Parameter()] + [ValidateNotNullOrEmpty()] + [System.String] + $SysvolPath, + + [Parameter()] + [ValidateSet('Win2008', 'Win2008R2', 'Win2012', 'Win2012R2', 'WinThreshold')] + [System.String] + $ForestMode, + + [Parameter()] + [ValidateSet('Win2008', 'Win2008R2', 'Win2012', 'Win2012R2', 'WinThreshold')] + [System.String] + $DomainMode + ) + + # Debug can pause Install-ADDSForest/Install-ADDSDomain, so we remove it. + $null = $PSBoundParameters.Remove('Debug') + + $getTargetResourceParameters = @{ + DomainName = $DomainName + Credential = $Credential + SafeModeAdministratorPassword = $SafeModeAdministratorPassword + ParentDomainName = $ParentDomainName + } + + @($getTargetResourceParameters.Keys) | + ForEach-Object { + if (-not $PSBoundParameters.ContainsKey($_)) + { + $getTargetResourceParameters.Remove($_) + } + } + + $targetResource = Get-TargetResource @getTargetResourceParameters + + if (-not $targetResource.DomainExist) + { + $installADDSParameters = @{ + SafeModeAdministratorPassword = $SafeModeAdministratorPassword.Password + NoRebootOnCompletion = $true + Force = $true + ErrorAction = 'Stop' + } + + if ($PSBoundParameters.ContainsKey('DnsDelegationCredential')) + { + $installADDSParameters['DnsDelegationCredential'] = $DnsDelegationCredential + $installADDSParameters['CreateDnsDelegation'] = $true + } + + if ($PSBoundParameters.ContainsKey('DatabasePath')) + { + $installADDSParameters['DatabasePath'] = $DatabasePath + } + + if ($PSBoundParameters.ContainsKey('LogPath')) + { + $installADDSParameters['LogPath'] = $LogPath + } + + if ($PSBoundParameters.ContainsKey('SysvolPath')) + { + $installADDSParameters['SysvolPath'] = $SysvolPath + } + + if ($PSBoundParameters.ContainsKey('DomainMode')) + { + $installADDSParameters['DomainMode'] = $DomainMode + } + + if ($PSBoundParameters.ContainsKey('ParentDomainName')) + { + Write-Verbose -Message ($script:localizedData.CreatingChildDomain -f $DomainName, $ParentDomainName) + $installADDSParameters['Credential'] = $Credential + $installADDSParameters['NewDomainName'] = $DomainName + $installADDSParameters['ParentDomainName'] = $ParentDomainName + $installADDSParameters['DomainType'] = 'ChildDomain' + + if ($PSBoundParameters.ContainsKey('DomainNetBiosName')) + { + $installADDSParameters['NewDomainNetBiosName'] = $DomainNetBiosName + } + + Install-ADDSDomain @installADDSParameters + + Write-Verbose -Message ($script:localizedData.CreatedChildDomain) + } + else + { + Write-Verbose -Message ($script:localizedData.CreatingForest -f $DomainName) + $installADDSParameters['DomainName'] = $DomainName + + if ($PSBoundParameters.ContainsKey('DomainNetBiosName')) + { + $installADDSParameters['DomainNetBiosName'] = $DomainNetBiosName + } + + if ($PSBoundParameters.ContainsKey('ForestMode')) + { + $installADDSParameters['ForestMode'] = $ForestMode + } + + Install-ADDSForest @installADDSParameters + + Write-Verbose -Message ($script:localizedData.CreatedForest -f $DomainName) + } + + <# + Signal to the LCM to reboot the node to compensate for the one we + suppressed from Install-ADDSForest/Install-ADDSDomain. + #> + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', '', + Justification = 'Set LCM DSCMachineStatus to indicate reboot required')] + $global:DSCMachineStatus = 1 + } +} #end function Set-TargetResource + +Export-ModuleMember -Function *-TargetResource diff --git a/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_ADDomain/MSFT_ADDomain.schema.mof b/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_ADDomain/MSFT_ADDomain.schema.mof new file mode 100644 index 0000000..542c9fe --- /dev/null +++ b/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_ADDomain/MSFT_ADDomain.schema.mof @@ -0,0 +1,18 @@ +[ClassVersion("1.0.1.0"), FriendlyName("ADDomain")] +class MSFT_ADDomain : OMI_BaseResource +{ + [Key, Description("The fully qualified domain name (FQDN) of a new domain. If setting up a child domain this must be set to a single-label DNS name.")] String DomainName; + [Required, Description("Specifies the user name and password that corresponds to the account used to install the domain controller. These are only used when adding a child domain and these credentials need the correct permission in the parent domain. This will not be created as a user in the new domain. The domain administrator password will be the same as the password of the local Administrator of this node."), EmbeddedInstance("MSFT_Credential")] String Credential; + [Required, Description("Password for the administrator account when the computer is started in Safe Mode."), EmbeddedInstance("MSFT_Credential")] String SafeModeAdministratorPassword; + [Write, Description("Fully qualified domain name (FQDN) of the parent domain.")] String ParentDomainName; + [Write, Description("NetBIOS name for the new domain.")] String DomainNetBiosName; + [Write, Description("Credential used for creating DNS delegation."), EmbeddedInstance("MSFT_Credential")] String DnsDelegationCredential; + [Write, Description("Path to a directory that contains the domain database.")] String DatabasePath; + [Write, Description("Path to a directory for the log file that will be written.")] String LogPath; + [Write, Description("Path to a directory where the Sysvol file will be written.")] String SysvolPath; + [Write, Description("The Forest Functional Level for the entire forest."), ValueMap{"Win2008", "Win2008R2", "Win2012", "Win2012R2", "WinThreshold"}, Values{"Win2008", "Win2008R2", "Win2012", "Win2012R2", "WinThreshold"}] String ForestMode; + [Write, Description("The Domain Functional Level for the entire domain."), ValueMap{"Win2008", "Win2008R2", "Win2012", "Win2012R2", "WinThreshold"}, Values{"Win2008", "Win2008R2", "Win2012", "Win2012R2", "WinThreshold"}] String DomainMode; + [Read, Description("Returns $true if the domain is available, or $false if the domain could not be found.")] Boolean DomainExist; + [Read, Description("Returns the fully qualified domain name (FQDN) DNS root of the domain.")] String DnsRoot; + [Read, Description("Returns the fully qualified domain name (FQDN) of the forest.")] String Forest; +}; diff --git a/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_ADDomain/README.md b/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_ADDomain/README.md new file mode 100644 index 0000000..9dc0c25 --- /dev/null +++ b/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_ADDomain/README.md @@ -0,0 +1,7 @@ +# Description + +The ADDomain resource creates a new domain in a new forest or a child domain in an existing forest. While it is possible to set the forest functional level and the domain functional level during deployment with this resource the common restrictions apply. For more information see [TechNet](https://docs.microsoft.com/en-us/windows-server/identity/ad-ds/active-directory-functional-levels). + +## Requirements + +* Target machine must be running Windows Server 2008 R2 or later. diff --git a/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_ADDomain/en-US/MSFT_ADDomain.strings.psd1 b/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_ADDomain/en-US/MSFT_ADDomain.strings.psd1 new file mode 100644 index 0000000..d744978 --- /dev/null +++ b/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_ADDomain/en-US/MSFT_ADDomain.strings.psd1 @@ -0,0 +1,17 @@ +# culture="en-US" +ConvertFrom-StringData @' + QueryDomain = Querying for domain '{0}'. (ADD0001) + ADServerNotReady = The AD Server for domain '{0}' is currently not ready. (ADD0002) + DomainFound = Active Directory domain '{0}' found. (ADD0003) + CreatingChildDomain = Creating domain '{0}' as a child of domain '{1}'. (ADD0004) + CreatedChildDomain = Child domain '{0}' created. (ADD0005) + CreatingForest = Creating AD forest '{0}'. (ADD0006) + CreatedForest = AD forest '{0}' created. (ADD0007) + DomainInDesiredState = The domain '{0}' is in the desired state. (ADD0008) + DomainNotInDesiredState = The domain '{0}' is NOT in the desired state. (ADD0009) + RetryingGetADDomain = Attempt {0} of {1} to call Get-ADDomain failed, retrying in {2} seconds. (ADD0010) + SysVolPathDoesNotExistError = The expected SysVol Path '{0}' does not exist. (ADD0011) + MaxDomainRetriesReachedError = Maximum Get-ADDomain retries reached and the domain did not respond. (ADD0012) + GetAdDomainUnexpectedError = Error getting AD domain '{0}'. (ADD0013) + GetAdForestUnexpectedError = Error getting AD forest '{0}'. (ADD0014) +'@ diff --git a/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_ADDomain/en-US/about_ADDomain.help.txt b/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_ADDomain/en-US/about_ADDomain.help.txt new file mode 100644 index 0000000..acf5188 --- /dev/null +++ b/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_ADDomain/en-US/about_ADDomain.help.txt @@ -0,0 +1,164 @@ +.NAME + ADDomain + +.DESCRIPTION + The ADDomain resource creates a new domain in a new forest or a child domain in an existing forest. While it is possible to set the forest functional level and the domain functional level during deployment with this resource the common restrictions apply. For more information see [TechNet](https://docs.microsoft.com/en-us/windows-server/identity/ad-ds/active-directory-functional-levels). + + ## Requirements + + * Target machine must be running Windows Server 2008 R2 or later. + +.PARAMETER DomainName + Key - String + The fully qualified domain name (FQDN) of a new domain. If setting up a child domain this must be set to a single-label DNS name. + +.PARAMETER Credential + Required - PSCredential + Specifies the user name and password that corresponds to the account used to install the domain controller. These are only used when adding a child domain and these credentials need the correct permission in the parent domain. This will not be created as a user in the new domain. The domain administrator password will be the same as the password of the local Administrator of this node. + +.PARAMETER SafeModeAdministratorPassword + Required - PSCredential + Password for the administrator account when the computer is started in Safe Mode. + +.PARAMETER ParentDomainName + Write - String + Fully qualified domain name (FQDN) of the parent domain. + +.PARAMETER DomainNetBiosName + Write - String + NetBIOS name for the new domain. + +.PARAMETER DnsDelegationCredential + Write - PSCredential + Credential used for creating DNS delegation. + +.PARAMETER DatabasePath + Write - String + Path to a directory that contains the domain database. + +.PARAMETER LogPath + Write - String + Path to a directory for the log file that will be written. + +.PARAMETER SysvolPath + Write - String + Path to a directory where the Sysvol file will be written. + +.PARAMETER ForestMode + Write - String + Allowed values: Win2008, Win2008R2, Win2012, Win2012R2, WinThreshold + The Forest Functional Level for the entire forest. + +.PARAMETER DomainMode + Write - String + Allowed values: Win2008, Win2008R2, Win2012, Win2012R2, WinThreshold + The Domain Functional Level for the entire domain. + +.PARAMETER DomainExist + Read - Boolean + Returns $true if the domain is available, or $false if the domain could not be found. + +.PARAMETER DnsRoot + Read - String + Returns the fully qualified domain name (FQDN) DNS root of the domain. + +.PARAMETER Forest + Read - String + Returns the fully qualified domain name (FQDN) of the forest. + +.EXAMPLE 1 + +This configuration will create a new domain with a new forest and a forest +functional level of Server 2016. + +Configuration ADDomain_NewForest_Config +{ + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.Management.Automation.PSCredential] + $Credential, + + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.Management.Automation.PSCredential] + $SafeModePassword + ) + + Import-DscResource -ModuleName PSDesiredStateConfiguration + Import-DscResource -ModuleName ActiveDirectoryDsc + + node 'localhost' + { + WindowsFeature 'ADDS' + { + Name = 'AD-Domain-Services' + Ensure = 'Present' + } + + WindowsFeature 'RSAT' + { + Name = 'RSAT-AD-PowerShell' + Ensure = 'Present' + } + + ADDomain 'contoso.com' + { + DomainName = 'contoso.com' + Credential = $Credential + SafemodeAdministratorPassword = $SafeModePassword + ForestMode = 'WinThreshold' + } + } +} + +.EXAMPLE 2 + +This configuration will create a new child domain in an existing forest with +a Domain Functional Level of Windows Server 2012R2. + +Configuration ADDomain_NewChildDomain_Config +{ + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.Management.Automation.PSCredential] + $Credential, + + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.Management.Automation.PSCredential] + $SafeModePassword + ) + + Import-DscResource -ModuleName PSDesiredStateConfiguration + Import-DscResource -ModuleName ActiveDirectoryDsc + + node 'localhost' + { + WindowsFeature 'ADDS' + { + Name = 'AD-Domain-Services' + Ensure = 'Present' + } + + WindowsFeature 'RSAT' + { + Name = 'RSAT-AD-PowerShell' + Ensure = 'Present' + } + + ADDomain 'child' + { + DomainName = 'child' + Credential = $Credential + SafemodeAdministratorPassword = $SafeModePassword + DomainMode = 'Win2012R2' + ParentDomainName = 'contoso.com' + } + } +} + + diff --git a/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_ADDomainController/MSFT_ADDomainController.psm1 b/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_ADDomainController/MSFT_ADDomainController.psm1 new file mode 100644 index 0000000..dafef40 --- /dev/null +++ b/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_ADDomainController/MSFT_ADDomainController.psm1 @@ -0,0 +1,854 @@ +$script:resourceModulePath = Split-Path -Path (Split-Path -Path $PSScriptRoot -Parent) -Parent +$script:modulesFolderPath = Join-Path -Path $script:resourceModulePath -ChildPath 'Modules' + +$script:localizationModulePath = Join-Path -Path $script:modulesFolderPath -ChildPath 'ActiveDirectoryDsc.Common' +Import-Module -Name (Join-Path -Path $script:localizationModulePath -ChildPath 'ActiveDirectoryDsc.Common.psm1') + +$script:localizedData = Get-LocalizedData -ResourceName 'MSFT_ADDomainController' + +<# + .SYNOPSIS + Returns the current state of the domain controller. + + .PARAMETER DomainName + Provide the FQDN of the domain the Domain Controller is being added to. + + .PARAMETER Credential + Specifies the credential for the account used to install the domain controller. + This account must have permission to access the other domain controllers + in the domain to be able replicate domain information. + + .PARAMETER SafemodeAdministratorPassword + Provide a password that will be used to set the DSRM password. This is a PSCredential. + + .NOTES + Used Functions: + Name | Module + ------------------------------------------------|-------------------------- + Get-ADDomain | ActiveDirectory + Get-ADDomainControllerPasswordReplicationPolicy | ActiveDirectory + Get-DomainControllerObject | ActiveDirectoryDsc.Common + Assert-Module | ActiveDirectoryDsc.Common +#> +function Get-TargetResource +{ + [CmdletBinding()] + [OutputType([System.Collections.Hashtable])] + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $DomainName, + + [Parameter(Mandatory = $true)] + [System.Management.Automation.PSCredential] + $Credential, + + [Parameter(Mandatory = $true)] + [System.Management.Automation.PSCredential] + $SafemodeAdministratorPassword + ) + + Assert-Module -ModuleName 'ActiveDirectory' + + Write-Verbose -Message ($script:localizedData.ResolveDomainName -f $DomainName) + + try + { + $domain = Get-ADDomain -Identity $DomainName -Credential $Credential + } + catch + { + $errorMessage = $script:localizedData.MissingDomain -f $DomainName + New-ObjectNotFoundException -Message $errorMessage -ErrorRecord $_ + } + + Write-Verbose -Message ($script:localizedData.DomainPresent -f $DomainName) + + $domainControllerObject = Get-DomainControllerObject ` + -DomainName $DomainName -ComputerName $env:COMPUTERNAME -Credential $Credential + + if ($domainControllerObject) + { + Write-Verbose -Message ($script:localizedData.IsDomainController -f + $domainControllerObject.Name, $domainControllerObject.Domain) + + $allowedPasswordReplicationAccountName = ( + Get-ADDomainControllerPasswordReplicationPolicy -Allowed -Identity $domainControllerObject | + ForEach-Object -MemberName sAMAccountName) + $deniedPasswordReplicationAccountName = ( + Get-ADDomainControllerPasswordReplicationPolicy -Denied -Identity $domainControllerObject | + ForEach-Object -MemberName sAMAccountName) + $serviceNTDS = Get-ItemProperty -Path 'HKLM:\SYSTEM\CurrentControlSet\Services\NTDS\Parameters' + $serviceNETLOGON = Get-ItemProperty -Path 'HKLM:\SYSTEM\CurrentControlSet\Services\Netlogon\Parameters' + $installDns = [System.Boolean](Get-Service -Name dns -ErrorAction SilentlyContinue) + + $targetResource = @{ + AllowPasswordReplicationAccountName = @($allowedPasswordReplicationAccountName) + Credential = $Credential + DatabasePath = $serviceNTDS.'DSA Working Directory' + DenyPasswordReplicationAccountName = @($deniedPasswordReplicationAccountName) + DomainName = $domainControllerObject.Domain + Ensure = $true + FlexibleSingleMasterOperationRole = @($domainControllerObject.OperationMasterRoles) + InstallationMediaPath = $null + InstallDns = $installDns + IsGlobalCatalog = $domainControllerObject.IsGlobalCatalog + LogPath = $serviceNTDS.'Database log files path' + ReadOnlyReplica = $domainControllerObject.IsReadOnly + SafemodeAdministratorPassword = $SafemodeAdministratorPassword + SiteName = $domainControllerObject.Site + SysvolPath = $serviceNETLOGON.SysVol -replace '\\sysvol$', '' + } + } + else + { + Write-Verbose -Message ($script:localizedData.NotDomainController -f $env:COMPUTERNAME) + + $targetResource = @{ + AllowPasswordReplicationAccountName = $null + Credential = $Credential + DatabasePath = $null + DenyPasswordReplicationAccountName = $null + DomainName = $DomainName + Ensure = $false + FlexibleSingleMasterOperationRole = $null + InstallationMediaPath = $null + InstallDns = $false + IsGlobalCatalog = $false + LogPath = $null + ReadOnlyReplica = $false + SafemodeAdministratorPassword = $SafemodeAdministratorPassword + SiteName = $null + SysvolPath = $null + } + } + + return $targetResource +} + +<# + .SYNOPSIS + Installs, or change properties on, a domain controller. + + .PARAMETER DomainName + Provide the FQDN of the domain the Domain Controller is being added to. + + .PARAMETER Credential + Specifies the credential for the account used to install the domain controller. + This account must have permission to access the other domain controllers + in the domain to be able replicate domain information. + + .PARAMETER SafemodeAdministratorPassword + Provide a password that will be used to set the DSRM password. This is a PSCredential. + + .PARAMETER DatabasePath + Provide the path where the NTDS.dit will be created and stored. + + .PARAMETER LogPath + Provide the path where the logs for the NTDS will be created and stored. + + .PARAMETER SysvolPath + Provide the path where the Sysvol will be created and stored. + + .PARAMETER SiteName + Provide the name of the site you want the Domain Controller to be added to. + + .PARAMETER InstallationMediaPath + Provide the path for the IFM folder that was created with ntdsutil. + This should not be on a share but locally to the Domain Controller being promoted. + + .PARAMETER IsGlobalCatalog + Specifies if the domain controller will be a Global Catalog (GC). + + .PARAMETER ReadOnlyReplica + Specifies if the domain controller should be provisioned as read-only domain controller + + .PARAMETER AllowPasswordReplicationAccountName + Provides a list of the users, computers, and groups to add to the password replication allowed list. + + .PARAMETER DenyPasswordReplicationAccountName + Provides a list of the users, computers, and groups to add to the password replication denied list. + + .PARAMETER FlexibleSingleMasterOperationRole + Specifies one or more Flexible Single Master Operation (FSMO) roles to + move to this domain controller. The current owner must be online and + responding for the move to be allowed. + + .PARAMETER InstallDns + Specifies if the DNS Server service should be installed and configured on + the domain controller. If this is not set the default value of the parameter + InstallDns of the cmdlet Install-ADDSDomainController is used. + The parameter `InstallDns` is only used during the provisioning of a domain + controller. The parameter cannot be used to install or uninstall the DNS + server on an already provisioned domain controller. + .NOTES + Used Functions: + Name | Module + ---------------------------------------------------|-------------------------- + Install-ADDSDomainController | ActiveDirectory + Get-ADDomain | ActiveDirectory + Get-ADForest | ActiveDirectory + Set-ADObject | ActiveDirectory + Move-ADDirectoryServer | ActiveDirectory + Move-ADDirectoryServerOperationMasterRole | ActiveDirectory + Remove-ADDomainControllerPasswordReplicationPolicy | ActiveDirectory + Add-ADDomainControllerPasswordReplicationPolicy | ActiveDirectory + Get-DomainControllerObject | ActiveDirectoryDsc.Common + New-InvalidOperationException | ActiveDirectoryDsc.Common +#> +function Set-TargetResource +{ + <# + Suppressing this rule because $global:DSCMachineStatus is used to + trigger a reboot for the one that was suppressed when calling + Install-ADDSDomainController. + #> + [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidGlobalVars', '')] + [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingPlainTextForPassword', '', + Justification = 'Read-Only Domain Controller (RODC) Creation support(AllowPasswordReplicationAccountName and DenyPasswordReplicationAccountName)')] + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $DomainName, + + [Parameter(Mandatory = $true)] + [System.Management.Automation.PSCredential] + $Credential, + + [Parameter(Mandatory = $true)] + [System.Management.Automation.PSCredential] + $SafemodeAdministratorPassword, + + [Parameter()] + [System.String] + $DatabasePath, + + [Parameter()] + [System.String] + $LogPath, + + [Parameter()] + [System.String] + $SysvolPath, + + [Parameter()] + [System.String] + $SiteName, + + [Parameter()] + [System.String] + $InstallationMediaPath, + + [Parameter()] + [System.Boolean] + $IsGlobalCatalog, + + [Parameter()] + [System.Boolean] + $ReadOnlyReplica, + + [Parameter()] + [System.String[]] + $AllowPasswordReplicationAccountName, + + [Parameter()] + [System.String[]] + $DenyPasswordReplicationAccountName, + + [Parameter()] + [ValidateSet('DomainNamingMaster', 'SchemaMaster', 'InfrastructureMaster', 'PDCEmulator', 'RIDMaster')] + [System.String[]] + $FlexibleSingleMasterOperationRole, + + [Parameter()] + [System.Boolean] + $InstallDns + ) + + $getTargetResourceParameters = @{ + DomainName = $DomainName + Credential = $Credential + SafeModeAdministratorPassword = $SafemodeAdministratorPassword + } + + $targetResource = Get-TargetResource @getTargetResourceParameters + + if ($targetResource.Ensure -eq $false) + { + Write-Verbose -Message ($script:localizedData.Promoting -f $env:COMPUTERNAME, $DomainName) + + # Node is not a domain controller so we promote it. + $installADDSDomainControllerParameters = @{ + DomainName = $DomainName + SafeModeAdministratorPassword = $SafemodeAdministratorPassword.Password + Credential = $Credential + NoRebootOnCompletion = $true + Force = $true + } + + if ($PSBoundParameters.ContainsKey('ReadOnlyReplica') -and $ReadOnlyReplica -eq $true) + { + if (-not $PSBoundParameters.ContainsKey('SiteName')) + { + New-InvalidOperationException -Message $script:localizedData.RODCMissingSite + } + + $installADDSDomainControllerParameters.Add('ReadOnlyReplica', $true) + } + + if ($PSBoundParameters.ContainsKey('AllowPasswordReplicationAccountName')) + { + $installADDSDomainControllerParameters.Add('AllowPasswordReplicationAccountName', + $AllowPasswordReplicationAccountName) + } + + if ($PSBoundParameters.ContainsKey('DenyPasswordReplicationAccountName')) + { + $installADDSDomainControllerParameters.Add('DenyPasswordReplicationAccountName', + $DenyPasswordReplicationAccountName) + } + + if ($PSBoundParameters.ContainsKey('DatabasePath')) + { + $installADDSDomainControllerParameters.Add('DatabasePath', $DatabasePath) + } + + if ($PSBoundParameters.ContainsKey('LogPath')) + { + $installADDSDomainControllerParameters.Add('LogPath', $LogPath) + } + + if ($PSBoundParameters.ContainsKey('SysvolPath')) + { + $installADDSDomainControllerParameters.Add('SysvolPath', $SysvolPath) + } + + if ($PSBoundParameters.ContainsKey('SiteName') -and $SiteName) + { + $installADDSDomainControllerParameters.Add('SiteName', $SiteName) + } + + if ($PSBoundParameters.ContainsKey('IsGlobalCatalog') -and $IsGlobalCatalog -eq $false) + { + $installADDSDomainControllerParameters.Add('NoGlobalCatalog', $true) + } + + if ($PSBoundParameters.ContainsKey('InstallDns')) + { + $installADDSDomainControllerParameters.Add('InstallDns', $InstallDns) + } + + if (-not [System.String]::IsNullOrWhiteSpace($InstallationMediaPath)) + { + $installADDSDomainControllerParameters.Add('InstallationMediaPath', $InstallationMediaPath) + } + + Install-ADDSDomainController @installADDSDomainControllerParameters + + Write-Verbose -Message ($script:localizedData.Promoted -f $env:COMPUTERNAME, $DomainName) + + <# + Signal to the LCM to reboot the node to compensate for the one we + suppressed from Install-ADDSDomainController + #> + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', '', + Justification = 'Set LCM DSCMachineStatus to indicate reboot required')] + $global:DSCMachineStatus = 1 + } + elseif ($targetResource.Ensure) + { + # Node is a domain controller. We check if other properties are in desired state + + Write-Verbose -Message ($script:localizedData.IsDomainController -f $env:COMPUTERNAME, $DomainName) + + $domainControllerObject = Get-DomainControllerObject ` + -DomainName $DomainName -ComputerName $env:COMPUTERNAME -Credential $Credential + + # Check if Node Global Catalog state is correct + if ($PSBoundParameters.ContainsKey('IsGlobalCatalog') -and + $targetResource.IsGlobalCatalog -ne $IsGlobalCatalog) + { + # DC is not in the expected Global Catalog state + if ($IsGlobalCatalog) + { + $globalCatalogOptionValue = 1 + + Write-Verbose -Message $script:localizedData.AddGlobalCatalog + } + else + { + $globalCatalogOptionValue = 0 + + Write-Verbose -Message $script:localizedData.RemoveGlobalCatalog + } + + Set-ADObject -Identity $domainControllerObject.NTDSSettingsObjectDN -Replace @{ + options = $globalCatalogOptionValue + } + } + + if ($PSBoundParameters.ContainsKey('SiteName') -and $targetResource.SiteName -ne $SiteName) + { + # DC is not in correct site. Move it. + Write-Verbose -Message ($script:localizedData.MovingDomainController -f + $targetResource.SiteName, $SiteName) + + Move-ADDirectoryServer -Identity $env:COMPUTERNAME -Site $SiteName -Credential $Credential + } + + if ($PSBoundParameters.ContainsKey('AllowPasswordReplicationAccountName')) + { + $testMembersParameters = @{ + ExistingMembers = $targetResource.AllowPasswordReplicationAccountName + Members = $AllowPasswordReplicationAccountName + } + + if (-not (Test-Members @testMembersParameters)) + { + Write-Verbose -Message ( + $script:localizedData.AllowedSyncAccountsMismatch -f + ($targetResource.AllowPasswordReplicationAccountName -join ';'), + ($AllowPasswordReplicationAccountName -join ';') + ) + + $getMembersToAddAndRemoveParameters = @{ + DesiredMembers = $AllowPasswordReplicationAccountName + CurrentMembers = $targetResource.AllowPasswordReplicationAccountName + } + + $getMembersToAddAndRemoveResult = Get-MembersToAddAndRemove @getMembersToAddAndRemoveParameters + + $adPrincipalsToRemove = $getMembersToAddAndRemoveResult.MembersToRemove + $adPrincipalsToAdd = $getMembersToAddAndRemoveResult.MembersToAdd + + if ($null -ne $adPrincipalsToRemove) + { + $removeADPasswordReplicationPolicy = @{ + Identity = $domainControllerObject + AllowedList = $adPrincipalsToRemove + } + + Remove-ADDomainControllerPasswordReplicationPolicy @removeADPasswordReplicationPolicy ` + -Confirm:$false + } + + if ($null -ne $adPrincipalsToAdd) + { + $addADPasswordReplicationPolicy = @{ + Identity = $domainControllerObject + AllowedList = $adPrincipalsToAdd + } + + Add-ADDomainControllerPasswordReplicationPolicy @addADPasswordReplicationPolicy + } + } + } + + if ($PSBoundParameters.ContainsKey('DenyPasswordReplicationAccountName')) + { + $testMembersParameters = @{ + ExistingMembers = $targetResource.DenyPasswordReplicationAccountName + Members = $DenyPasswordReplicationAccountName; + } + + if (-not (Test-Members @testMembersParameters)) + { + Write-Verbose -Message ( + $script:localizedData.DenySyncAccountsMismatch -f + ($targetResource.DenyPasswordReplicationAccountName -join ';'), + ($DenyPasswordReplicationAccountName -join ';') + ) + + $getMembersToAddAndRemoveParameters = @{ + DesiredMembers = $DenyPasswordReplicationAccountName + CurrentMembers = $targetResource.DenyPasswordReplicationAccountName + } + + $getMembersToAddAndRemoveResult = Get-MembersToAddAndRemove @getMembersToAddAndRemoveParameters + + $adPrincipalsToRemove = $getMembersToAddAndRemoveResult.MembersToRemove + $adPrincipalsToAdd = $getMembersToAddAndRemoveResult.MembersToAdd + + if ($null -ne $adPrincipalsToRemove) + { + $removeADPasswordReplicationPolicy = @{ + Identity = $domainControllerObject + DeniedList = $adPrincipalsToRemove + } + + Remove-ADDomainControllerPasswordReplicationPolicy @removeADPasswordReplicationPolicy ` + -Confirm:$false + } + + if ($null -ne $adPrincipalsToAdd) + { + $addADPasswordReplicationPolicy = @{ + Identity = $domainControllerObject + DeniedList = $adPrincipalsToAdd + } + + Add-ADDomainControllerPasswordReplicationPolicy @addADPasswordReplicationPolicy + } + + } + } + + if ($PSBoundParameters.ContainsKey('FlexibleSingleMasterOperationRole')) + { + foreach ($desiredFlexibleSingleMasterOperationRole in $FlexibleSingleMasterOperationRole) + { + if ($desiredFlexibleSingleMasterOperationRole -notin $targetResource.FlexibleSingleMasterOperationRole) + { + switch ($desiredFlexibleSingleMasterOperationRole) + { + <# + Connect to any available domain controller to get the + current owner for the specific role. + #> + { $_ -in @('DomainNamingMaster', 'SchemaMaster') } + { + $currentOwnerFullyQualifiedDomainName = (Get-ADForest).$_ + } + + { $_ -in @('InfrastructureMaster', 'PDCEmulator', 'RIDMaster') } + { + $currentOwnerFullyQualifiedDomainName = (Get-ADDomain).$_ + } + } + + Write-Verbose -Message ($script:localizedData.MovingFlexibleSingleMasterOperationRole -f + $desiredFlexibleSingleMasterOperationRole, $currentOwnerFullyQualifiedDomainName) + + <# + Using the object returned from Get-ADDomainController to handle + an issue with calling Move-ADDirectoryServerOperationMasterRole + with Fully Qualified Domain Name (FQDN) in the Identity parameter. + #> + $MoveADDirectoryServerOperationMasterRoleParameters = @{ + Identity = $domainControllerObject + OperationMasterRole = $desiredFlexibleSingleMasterOperationRole + Server = $currentOwnerFullyQualifiedDomainName + ErrorAction = 'Stop' + } + + Move-ADDirectoryServerOperationMasterRole @MoveADDirectoryServerOperationMasterRoleParameters + } + } + } + } +} + +<# + .SYNOPSIS + Determines if the domain controller is in desired state. + + .PARAMETER DomainName + Provide the FQDN of the domain the Domain Controller is being added to. + + .PARAMETER Credential + Specifies the credential for the account used to install the domain controller. + This account must have permission to access the other domain controllers + in the domain to be able replicate domain information. + + .PARAMETER SafemodeAdministratorPassword + Provide a password that will be used to set the DSRM password. This is a PSCredential. + + .PARAMETER DatabasePath + Provide the path where the NTDS.dit will be created and stored. + + .PARAMETER LogPath + Provide the path where the logs for the NTDS will be created and stored. + + .PARAMETER SysvolPath + Provide the path where the Sysvol will be created and stored. + + .PARAMETER SiteName + Provide the name of the site you want the Domain Controller to be added to. + + .PARAMETER InstallationMediaPath + Provide the path for the IFM folder that was created with ntdsutil. + This should not be on a share but locally to the Domain Controller being promoted. + + .PARAMETER IsGlobalCatalog + Specifies if the domain controller will be a Global Catalog (GC). + + .PARAMETER ReadOnlyReplica + Specifies if the domain controller should be provisioned as read-only domain controller + + .PARAMETER AllowPasswordReplicationAccountName + Provides a list of the users, computers, and groups to add to the password replication allowed list. + + .PARAMETER DenyPasswordReplicationAccountName + Provides a list of the users, computers, and groups to add to the password replication denied list. + + .PARAMETER FlexibleSingleMasterOperationRole + Specifies one or more Flexible Single Master Operation (FSMO) roles to + move to this domain controller. The current owner must be online and + responding for the move to be allowed. + + .PARAMETER InstallDns + Specifies if the DNS Server service should be installed and configured on + the domain controller. If this is not set the default value of the parameter + InstallDns of the cmdlet Install-ADDSDomainController is used. + The parameter `InstallDns` is only used during the provisioning of a domain + controller. The parameter cannot be used to install or uninstall the DNS + server on an already provisioned domain controller. + + Not used in Test-TargetResource. + + .NOTES + Used Functions: + Name | Module + ------------------------------|-------------------------- + Test-ADReplicationSite | ActiveDirectoryDsc.Common + New-InvalidOperationException | ActiveDirectoryDsc.Common + New-ObjectNotFoundException | ActiveDirectoryDsc.Common + Test-Members | ActiveDirectoryDsc.Common +#> +function Test-TargetResource +{ + [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingPlainTextForPassword", "", + Justification = 'Read-Only Domain Controller (RODC) Creation support($AllowPasswordReplicationAccountName and DenyPasswordReplicationAccountName)')] + [CmdletBinding()] + [OutputType([System.Boolean])] + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $DomainName, + + [Parameter(Mandatory = $true)] + [System.Management.Automation.PSCredential] + $Credential, + + [Parameter(Mandatory = $true)] + [System.Management.Automation.PSCredential] + $SafemodeAdministratorPassword, + + [Parameter()] + [System.String] + $DatabasePath, + + [Parameter()] + [System.String] + $LogPath, + + [Parameter()] + [System.String] + $SysvolPath, + + [Parameter()] + [System.String] + $SiteName, + + [Parameter()] + [System.String] + $InstallationMediaPath, + + [Parameter()] + [System.Boolean] + $IsGlobalCatalog, + + [Parameter()] + [System.Boolean] + $ReadOnlyReplica, + + [Parameter()] + [System.String[]] + $AllowPasswordReplicationAccountName, + + [Parameter()] + [System.String[]] + $DenyPasswordReplicationAccountName, + + [Parameter()] + [ValidateSet('DomainNamingMaster', 'SchemaMaster', 'InfrastructureMaster', 'PDCEmulator', 'RIDMaster')] + [System.String[]] + $FlexibleSingleMasterOperationRole, + + [Parameter()] + [System.Boolean] + $InstallDns + ) + + Write-Verbose -Message ($script:localizedData.TestingConfiguration -f $env:COMPUTERNAME, $DomainName) + + if ($PSBoundParameters.ContainsKey('ReadOnlyReplica') -and $ReadOnlyReplica -eq $true) + { + if (-not $PSBoundParameters.ContainsKey('SiteName')) + { + New-InvalidOperationException -Message $script:localizedData.RODCMissingSite + } + } + + if ($PSBoundParameters.ContainsKey('SiteName')) + { + if (-not (Test-ADReplicationSite -SiteName $SiteName -DomainName $DomainName -Credential $Credential)) + { + $errorMessage = $script:localizedData.FailedToFindSite -f $SiteName, $DomainName + New-ObjectNotFoundException -Message $errorMessage + } + } + + $getTargetResourceParameters = @{ + DomainName = $DomainName + Credential = $Credential + SafeModeAdministratorPassword = $SafemodeAdministratorPassword + } + + $existingResource = Get-TargetResource @getTargetResourceParameters + + $testTargetResourceReturnValue = $existingResource.Ensure + + if ($PSBoundParameters.ContainsKey('ReadOnlyReplica') -and $ReadOnlyReplica) + { + if ($testTargetResourceReturnValue -and -not $testTargetResourceReturnValue.ReadOnlyReplica) + { + New-InvalidOperationException -Message $script:localizedData.CannotConvertToRODC + } + } + + if ($PSBoundParameters.ContainsKey('SiteName') -and $existingResource.SiteName -ne $SiteName) + { + Write-Verbose -Message ($script:localizedData.WrongSite -f $existingResource.SiteName, $SiteName) + + $testTargetResourceReturnValue = $false + } + + # Check Global Catalog Config + if ($PSBoundParameters.ContainsKey('IsGlobalCatalog') -and $existingResource.IsGlobalCatalog -ne $IsGlobalCatalog) + { + if ($IsGlobalCatalog) + { + Write-Verbose -Message ($script:localizedData.ExpectedGlobalCatalogEnabled -f + $existingResource.SiteName, $SiteName) + } + else + { + Write-Verbose -Message ($script:localizedData.ExpectedGlobalCatalogDisabled -f + $existingResource.SiteName, $SiteName) + } + + $testTargetResourceReturnValue = $false + } + + if ($PSBoundParameters.ContainsKey('AllowPasswordReplicationAccountName') -and + $null -ne $existingResource.AllowPasswordReplicationAccountName) + { + $testMembersParameters = @{ + ExistingMembers = $existingResource.AllowPasswordReplicationAccountName + Members = $AllowPasswordReplicationAccountName + } + + if (-not (Test-Members @testMembersParameters)) + { + Write-Verbose -Message ( + $script:localizedData.AllowedSyncAccountsMismatch -f + ($existingResource.AllowPasswordReplicationAccountName -join ';'), + ($AllowPasswordReplicationAccountName -join ';') + ) + + $testTargetResourceReturnValue = $false + } + } + + if ($PSBoundParameters.ContainsKey('DenyPasswordReplicationAccountName') -and + $null -ne $existingResource.DenyPasswordReplicationAccountName) + { + $testMembersParameters = @{ + ExistingMembers = $existingResource.DenyPasswordReplicationAccountName + Members = $DenyPasswordReplicationAccountName; + } + + if (-not (Test-Members @testMembersParameters)) + { + Write-Verbose -Message ( + $script:localizedData.DenySyncAccountsMismatch -f + ($existingResource.DenyPasswordReplicationAccountName -join ';'), + ($DenyPasswordReplicationAccountName -join ';') + ) + + $testTargetResourceReturnValue = $false + } + } + + <# + Only evaluate Flexible Single Master Operation (FSMO) roles if the + node is already a domain controller. + #> + if ($PSBoundParameters.ContainsKey('FlexibleSingleMasterOperationRole') -and $existingResource.Ensure -eq $true) + { + $FlexibleSingleMasterOperationRole | ForEach-Object -Process { + if ($_ -notin $existingResource.FlexibleSingleMasterOperationRole) + { + Write-Verbose -Message ($script:localizedData.NotOwnerOfFlexibleSingleMasterOperationRole -f $_ ) + + $testTargetResourceReturnValue = $false + } + } + } + + return $testTargetResourceReturnValue +} + +<# + .SYNOPSIS + Return a hashtable with members that are not present in CurrentMembers, + and members that are present add should not be present. + + .PARAMETER DesiredMembers + Specifies the list of desired members in the hashtable. + + .PARAMETER CurrentMembers + Specifies the list of current members in the hashtable. + + .OUTPUTS + Returns a hashtable with two properties. The property MembersToAdd contains the + members as ADPrincipal objects that are not members in the collection + provided in $CurrentMembers. The property MembersToRemove contains the + unwanted members as ADPrincipal objects in the collection provided + in $CurrentMembers. +#> +function Get-MembersToAddAndRemove +{ + param + ( + [Parameter(Mandatory = $true)] + [AllowNull()] + [AllowEmptyCollection()] + [System.String[]] + $DesiredMembers, + + [Parameter(Mandatory = $true)] + [AllowNull()] + [AllowEmptyCollection()] + [System.String[]] + $CurrentMembers + ) + + $principalsToRemove = foreach ($memberName in $CurrentMembers) + { + if ($memberName -notin $DesiredMembers) + { + New-Object -TypeName Microsoft.ActiveDirectory.Management.ADPrincipal -ArgumentList $memberName + } + } + + $principalsToAdd = foreach ($memberName in $DesiredMembers) + { + if ($memberName -notin $CurrentMembers) + { + New-Object -TypeName Microsoft.ActiveDirectory.Management.ADPrincipal -ArgumentList $memberName + } + } + + return @{ + MembersToAdd = [Microsoft.ActiveDirectory.Management.ADPrincipal[]] $principalsToAdd + MembersToRemove = [Microsoft.ActiveDirectory.Management.ADPrincipal[]] $principalsToRemove + } +} + +Export-ModuleMember -Function *-TargetResource diff --git a/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_ADDomainController/MSFT_ADDomainController.schema.mof b/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_ADDomainController/MSFT_ADDomainController.schema.mof new file mode 100644 index 0000000..e235028 --- /dev/null +++ b/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_ADDomainController/MSFT_ADDomainController.schema.mof @@ -0,0 +1,19 @@ +[ClassVersion("1.0.1.0"), FriendlyName("ADDomainController")] +class MSFT_ADDomainController : OMI_BaseResource +{ + [Key, Description("The fully qualified domain name (FQDN) of the domain the Domain Controller will be joining.")] String DomainName; + [Required, Description("The credentials (as a 'PSCredential' object) of a user that has Domain Administrator rights to add the Domain Controller to the domain."), EmbeddedInstance("MSFT_Credential")] String Credential; + [Required, Description("The 'PSCredential' object containing the password to use for Directory Services Restore Mode (DSRM)."), EmbeddedInstance("MSFT_Credential")] String SafemodeAdministratorPassword; + [Write, Description("The path where the database will be stored.")] String DatabasePath; + [Write, Description("The path where the logs will be stored.")] String LogPath; + [Write, Description("The path where the Sysvol will be stored.")] String SysvolPath; + [Write, Description("The name of the site this Domain Controller will be added to.")] String SiteName; + [Write, Description("The path of the media you want to use install the Domain Controller.")] String InstallationMediaPath; + [Write, Description("Specifies if the domain controller will be a Global Catalog (GC).")] Boolean IsGlobalCatalog; + [Read, Description("Returns the state of the Domain Controller.")] String Ensure; + [Write, Description("Indicates that the cmdlet installs the domain controller as an Read-Only Domain Controller (RODC) for an existing domain.")] Boolean ReadOnlyReplica; + [Write, Description("Specifies an array of names of user accounts, group accounts, and computer accounts whose passwords can be replicated to this Read-Only Domain Controller (RODC).")] String AllowPasswordReplicationAccountName[]; + [Write, Description("Specifies the names of user accounts, group accounts, and computer accounts whose passwords are not to be replicated to this Read-Only Domain Controller (RODC).")] String DenyPasswordReplicationAccountName[]; + [Write, Description("Specifies one or more Flexible Single Master Operation (FSMO) roles to move to this domain controller. The current owner must be online and responding for the move to be allowed."), ValueMap{"DomainNamingMaster", "SchemaMaster", "InfrastructureMaster", "PDCEmulator", "RIDMaster"}, Values{"DomainNamingMaster", "SchemaMaster", "InfrastructureMaster", "PDCEmulator", "RIDMaster"}] String FlexibleSingleMasterOperationRole[]; + [Write, Description("Specifies if the DNS Server service should be installed and configured on the Domain Controller. If this is not set the default value of the parameter `InstallDns` of the cmdlet Install-ADDSDomainController is used. This parameter is only used during the provisioning of a domain controller. The parameter cannot be used to install or uninstall the DNS server on an already provisioned domain controller.")] Boolean InstallDns; +}; diff --git a/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_ADDomainController/README.md b/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_ADDomainController/README.md new file mode 100644 index 0000000..e82abc5 --- /dev/null +++ b/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_ADDomainController/README.md @@ -0,0 +1,35 @@ +# Description + +The ADDomainController DSC resource will install and configure domain +controllers in Active Directory. Installation of Read-Only Domain Controllers +(RODC) is also supported. + +Promotion of a Domain Controller using an existing DNS is available using +the `InstallDns` parameter. The parameter specifies if the DNS Server service +should be installed and configured on the domain controller. If this is +not set the default value of the parameter `InstallDns` of the cmdlet +[`Install-ADDSDomainController`](https://docs.microsoft.com/en-us/powershell/module/addsdeployment/install-addsdomaincontroller) +is used. The parameter `InstallDns` is only used during the provisioning +of a domain controller. The parameter cannot be used to install or uninstall +the DNS server on an already provisioned domain controller. + +>**Note:** If the account used for the parameter `Credential` +>cannot connect to another domain controller, for example using a credential +>without the domain name, then the cmdlet `Install-ADDSDomainController` will +>seemingly halt (without reporting an error) when trying to replicate +>information from another domain controller. +>Make sure to use a correct domain account with the correct permission as +>the account for the parameter `Credential`. + +The parameter `FlexibleSingleMasterOperationRole` is ignored until +the node has been provisioned as a domain controller. Take extra care +to make sure the Flexible Single Master Operation (FSMO) roles are moved +accordingly to avoid that two domain controller try to get to be the +owner of the same role (potential "ping-pong"-behavior). + +>The resource does not support seizing of Flexible Single Master Operation +>(FSMO) roles + +## Requirements + +* Target machine must be running Windows Server 2008 R2 or later. diff --git a/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_ADDomainController/en-US/MSFT_ADDomainController.strings.psd1 b/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_ADDomainController/en-US/MSFT_ADDomainController.strings.psd1 new file mode 100644 index 0000000..278c5b5 --- /dev/null +++ b/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_ADDomainController/en-US/MSFT_ADDomainController.strings.psd1 @@ -0,0 +1,23 @@ +ConvertFrom-StringData @' + ResolveDomainName = Resolving the domain name '{0}'. (ADDC0001) + DomainPresent = The domain '{0}' is present. Looking for domain controllers. (ADDC0002) + NotDomainController = The current node '{0}' is not a domain controller. (ADDC0006) + IsDomainController = The current node '{0}' is a domain controller for the domain '{1}'. (ADDC0007) + MissingDomain = Current node could not find the domain '{0}'. (ADDC0008) + Promoting = Promoting the current node to be a domain controller for the domain '{1}'. (ADDC0009) + Promoted = The current node '{0}' has been promoted to a domain controller for the domain '{1}'. (ADDC0010) + AddGlobalCatalog = Adding Global Catalog to the domain controller. (ADDC0011) + RemoveGlobalCatalog = Removing Global Catalog from the domain controller. (ADDC0012) + MovingDomainController = Moving Domain Controller from site '{0}' to site '{1}'. (ADDC0013) + FailedToFindSite = The site '{0}' could not be found in the domain '{1}'. (ADDC0014) + TestingConfiguration = Determine the state of the domain controller on the current node '{0}' in the domain '{1}'. (ADDC0015) + WrongSite = The domain controller is in the site '{0}', but expected it to be in the site '{1}'. (ADDC0016) + ExpectedGlobalCatalogEnabled = The domain controller does not contain a Global Catalog, but it was expected to have a Global Catalog. (ADDC0018) + ExpectedGlobalCatalogDisabled = The domain controller have a Global Catalog, but it was expected to not have a Global Catalog. (ADDC0019) + AllowedSyncAccountsMismatch = There is a mismatch in AllowPasswordReplicationAccountName list. Got {0}, expected was {1}. (ADDC0020) + DenySyncAccountsMismatch = There is a mismatch in DenyPasswordReplicationAccountName list. Got {0}, expected was {1}. (ADDC0021) + RODCMissingSite = You have specified 'ReadOnlyReplica', but did not provide a site name. (ADDC0022) + CannotConvertToRODC = Cannot convert a existing domain controller to a Read-Only Domain Controller (RODC). (ADDC0023) + NotOwnerOfFlexibleSingleMasterOperationRole = The domain controller was expected to be the owner of the Flexible Single Master Operation (FSMO) role '{0}', but it is not. (ADDC0024) + MovingFlexibleSingleMasterOperationRole = The Flexible Single Master Operation (FSMO) role '{0}' is being moved from domain controller '{1}' to this domain controller. (ADDC0025) +'@ diff --git a/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_ADDomainController/en-US/about_ADDomainController.help.txt b/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_ADDomainController/en-US/about_ADDomainController.help.txt new file mode 100644 index 0000000..cea22f9 --- /dev/null +++ b/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_ADDomainController/en-US/about_ADDomainController.help.txt @@ -0,0 +1,457 @@ +.NAME + ADDomainController + +.DESCRIPTION + The ADDomainController DSC resource will install and configure domain + controllers in Active Directory. Installation of Read-Only Domain Controllers + (RODC) is also supported. + + Promotion of a Domain Controller using an existing DNS is available using + the `InstallDns` parameter. The parameter specifies if the DNS Server service + should be installed and configured on the domain controller. If this is + not set the default value of the parameter `InstallDns` of the cmdlet + [`Install-ADDSDomainController`](https://docs.microsoft.com/en-us/powershell/module/addsdeployment/install-addsdomaincontroller) + is used. The parameter `InstallDns` is only used during the provisioning + of a domain controller. The parameter cannot be used to install or uninstall + the DNS server on an already provisioned domain controller. + + >**Note:** If the account used for the parameter `Credential` + >cannot connect to another domain controller, for example using a credential + >without the domain name, then the cmdlet `Install-ADDSDomainController` will + >seemingly halt (without reporting an error) when trying to replicate + >information from another domain controller. + >Make sure to use a correct domain account with the correct permission as + >the account for the parameter `Credential`. + + The parameter `FlexibleSingleMasterOperationRole` is ignored until + the node has been provisioned as a domain controller. Take extra care + to make sure the Flexible Single Master Operation (FSMO) roles are moved + accordingly to avoid that two domain controller try to get to be the + owner of the same role (potential "ping-pong"-behavior). + + >The resource does not support seizing of Flexible Single Master Operation + >(FSMO) roles + + ## Requirements + + * Target machine must be running Windows Server 2008 R2 or later. + +.PARAMETER DomainName + Key - String + The fully qualified domain name (FQDN) of the domain the Domain Controller will be joining. + +.PARAMETER Credential + Required - PSCredential + The credentials (as a 'PSCredential' object) of a user that has Domain Administrator rights to add the Domain Controller to the domain. + +.PARAMETER SafemodeAdministratorPassword + Required - PSCredential + The 'PSCredential' object containing the password to use for Directory Services Restore Mode (DSRM). + +.PARAMETER DatabasePath + Write - String + The path where the database will be stored. + +.PARAMETER LogPath + Write - String + The path where the logs will be stored. + +.PARAMETER SysvolPath + Write - String + The path where the Sysvol will be stored. + +.PARAMETER SiteName + Write - String + The name of the site this Domain Controller will be added to. + +.PARAMETER InstallationMediaPath + Write - String + The path of the media you want to use install the Domain Controller. + +.PARAMETER IsGlobalCatalog + Write - Boolean + Specifies if the domain controller will be a Global Catalog (GC). + +.PARAMETER Ensure + Read - String + Returns the state of the Domain Controller. + +.PARAMETER ReadOnlyReplica + Write - Boolean + Indicates that the cmdlet installs the domain controller as an Read-Only Domain Controller (RODC) for an existing domain. + +.PARAMETER AllowPasswordReplicationAccountName + Write - StringArray + Specifies an array of names of user accounts, group accounts, and computer accounts whose passwords can be replicated to this Read-Only Domain Controller (RODC). + +.PARAMETER DenyPasswordReplicationAccountName + Write - StringArray + Specifies the names of user accounts, group accounts, and computer accounts whose passwords are not to be replicated to this Read-Only Domain Controller (RODC). + +.PARAMETER FlexibleSingleMasterOperationRole + Write - StringArray + Allowed values: DomainNamingMaster, SchemaMaster, InfrastructureMaster, PDCEmulator, RIDMaster + Specifies one or more Flexible Single Master Operation (FSMO) roles to move to this domain controller. The current owner must be online and responding for the move to be allowed. + +.PARAMETER InstallDns + Write - Boolean + Specifies if the DNS Server service should be installed and configured on the Domain Controller. If this is not set the default value of the parameter `InstallDns` of the cmdlet Install-ADDSDomainController is used. This parameter is only used during the provisioning of a domain controller. The parameter cannot be used to install or uninstall the DNS server on an already provisioned domain controller. + +.EXAMPLE 1 + +This configuration will add a domain controller to the domain +contoso.com. + +Configuration ADDomainController_AddDomainControllerToDomainMinimal_Config +{ + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.Management.Automation.PSCredential] + $Credential, + + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.Management.Automation.PSCredential] + $SafeModePassword + ) + + Import-DscResource -ModuleName PSDesiredStateConfiguration + Import-DscResource -ModuleName ActiveDirectoryDsc + + node localhost + { + WindowsFeature 'InstallADDomainServicesFeature' + { + Ensure = 'Present' + Name = 'AD-Domain-Services' + } + + WindowsFeature 'RSATADPowerShell' + { + Ensure = 'Present' + Name = 'RSAT-AD-PowerShell' + + DependsOn = '[WindowsFeature]InstallADDomainServicesFeature' + } + + WaitForADDomain 'WaitForestAvailability' + { + DomainName = 'contoso.com' + Credential = $Credential + + DependsOn = '[WindowsFeature]RSATADPowerShell' + } + + ADDomainController 'DomainControllerMinimal' + { + DomainName = 'contoso.com' + Credential = $Credential + SafeModeAdministratorPassword = $SafeModePassword + + DependsOn = '[WaitForADDomain]WaitForestAvailability' + } + } +} + +.EXAMPLE 2 + +This configuration will add a domain controller to the domain +contoso.com, specifying all properties of the resource. + +Configuration ADDomainController_AddDomainControllerToDomainAllProperties_Config +{ + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.Management.Automation.PSCredential] + $Credential, + + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.Management.Automation.PSCredential] + $SafeModePassword + ) + + Import-DscResource -ModuleName PSDesiredStateConfiguration + Import-DscResource -ModuleName ActiveDirectoryDsc + + node localhost + { + WindowsFeature 'InstallADDomainServicesFeature' + { + Ensure = 'Present' + Name = 'AD-Domain-Services' + } + + WindowsFeature 'RSATADPowerShell' + { + Ensure = 'Present' + Name = 'RSAT-AD-PowerShell' + + DependsOn = '[WindowsFeature]InstallADDomainServicesFeature' + } + + WaitForADDomain 'WaitForestAvailability' + { + DomainName = 'contoso.com' + Credential = $Credential + + DependsOn = '[WindowsFeature]RSATADPowerShell' + } + + ADDomainController 'DomainControllerAllProperties' + { + DomainName = 'contoso.com' + Credential = $Credential + SafeModeAdministratorPassword = $SafeModePassword + DatabasePath = 'C:\Windows\NTDS' + LogPath = 'C:\Windows\Logs' + SysvolPath = 'C:\Windows\SYSVOL' + SiteName = 'Europe' + IsGlobalCatalog = $true + + DependsOn = '[WaitForADDomain]WaitForestAvailability' + } + } +} + +.EXAMPLE 3 + +This configuration will add a domain controller to the domain +contoso.com using the information from media. + +Configuration ADDomainController_AddDomainControllerToDomainUsingIFM_Config +{ + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.Management.Automation.PSCredential] + $Credential, + + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.Management.Automation.PSCredential] + $SafeModePassword + ) + + Import-DscResource -ModuleName PSDesiredStateConfiguration + Import-DscResource -ModuleName ActiveDirectoryDsc + + node localhost + { + WindowsFeature 'InstallADDomainServicesFeature' + { + Ensure = 'Present' + Name = 'AD-Domain-Services' + } + + WindowsFeature 'RSATADPowerShell' + { + Ensure = 'Present' + Name = 'RSAT-AD-PowerShell' + + DependsOn = '[WindowsFeature]InstallADDomainServicesFeature' + } + + WaitForADDomain 'WaitForestAvailability' + { + DomainName = 'contoso.com' + Credential = $Credential + + DependsOn = '[WindowsFeature]RSATADPowerShell' + } + + ADDomainController 'DomainControllerWithIFM' + { + DomainName = 'contoso.com' + Credential = $Credential + SafeModeAdministratorPassword = $SafeModePassword + InstallationMediaPath = 'F:\IFM' + + DependsOn = '[WaitForADDomain]WaitForestAvailability' + } + } +} + +.EXAMPLE 4 + +This configuration will add a read-only domain controller to the domain contoso.com +and specify a list of account, whose passwords are allowed/denied for synchronisation. + +Configuration ADDomainController_AddReadOnlyDomainController_Config +{ + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.Management.Automation.PSCredential] + $Credential, + + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.Management.Automation.PSCredential] + $SafeModePassword + ) + + Import-DscResource -ModuleName PSDesiredStateConfiguration + Import-DscResource -ModuleName ActiveDirectoryDsc + + node localhost + { + WindowsFeature 'InstallADDomainServicesFeature' + { + Ensure = 'Present' + Name = 'AD-Domain-Services' + } + + WindowsFeature 'RSATADPowerShell' + { + Ensure = 'Present' + Name = 'RSAT-AD-PowerShell' + + DependsOn = '[WindowsFeature]InstallADDomainServicesFeature' + } + + WaitForADDomain 'WaitForestAvailability' + { + DomainName = 'contoso.com' + Credential = $Credential + + DependsOn = '[WindowsFeature]RSATADPowerShell' + } + + ADDomainController 'Read-OnlyDomainController(RODC)' + { + DomainName = 'contoso.com' + Credential = $Credential + SafeModeAdministratorPassword = $SafeModePassword + ReadOnlyReplica = $true + SiteName = 'Default-First-Site-Name' + AllowPasswordReplicationAccountName = @('pvdi.test1', 'pvdi.test') + DenyPasswordReplicationAccountName = @('SVC_PVS', 'TA2SCVMM') + + DependsOn = '[WaitForADDomain]WaitForestAvailability' + } + } +} + +.EXAMPLE 5 + +This configuration will add a domain controller to the domain +contoso.com, and when the configuration is enforced it will +move the Flexible Single Master Operation (FSMO) role +'RIDMaster' from the current owner to this domain controller. + +Configuration ADDomainController_AddDomainControllerAndMoveRole_Config +{ + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.Management.Automation.PSCredential] + $Credential, + + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.Management.Automation.PSCredential] + $SafeModePassword + ) + + Import-DscResource -ModuleName PSDesiredStateConfiguration + Import-DscResource -ModuleName ActiveDirectoryDsc + + node localhost + { + WindowsFeature 'InstallADDomainServicesFeature' + { + Ensure = 'Present' + Name = 'AD-Domain-Services' + } + + WindowsFeature 'RSATADPowerShell' + { + Ensure = 'Present' + Name = 'RSAT-AD-PowerShell' + + DependsOn = '[WindowsFeature]InstallADDomainServicesFeature' + } + + WaitForADDomain 'WaitForestAvailability' + { + DomainName = 'contoso.com' + Credential = $Credential + + DependsOn = '[WindowsFeature]RSATADPowerShell' + } + + ADDomainController 'DomainControllerMinimal' + { + DomainName = 'contoso.com' + Credential = $Credential + SafeModeAdministratorPassword = $SafeModePassword + FlexibleSingleMasterOperationRole = @('RIDMaster') + + DependsOn = '[WaitForADDomain]WaitForestAvailability' + } + } +} + +.EXAMPLE 6 + +This configuration will add a domain controller to the domain contoso.com +without installing the local DNS server service and using the one in the existing domain. + +Configuration ADDomainController_AddDomainControllerUsingInstallDns_Config +{ + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.Management.Automation.PSCredential] + $Credential + ) + + Import-DscResource -ModuleName PSDesiredStateConfiguration + Import-DscResource -ModuleName ActiveDirectoryDsc + + node localhost + { + WindowsFeature 'InstallADDomainServicesFeature' + { + Ensure = 'Present' + Name = 'AD-Domain-Services' + } + + WindowsFeature 'RSATADPowerShell' + { + Ensure = 'Present' + Name = 'RSAT-AD-PowerShell' + + DependsOn = '[WindowsFeature]InstallADDomainServicesFeature' + } + + WaitForADDomain 'WaitForestAvailability' + { + DomainName = 'contoso.com' + Credential = $Credential + + DependsOn = '[WindowsFeature]RSATADPowerShell' + } + + ADDomainController 'DomainControllerUsingExistingDNSServer' + { + DomainName = 'contoso.com' + Credential = $Credential + SafeModeAdministratorPassword = $Credential + InstallDns = $false + + DependsOn = '[WaitForADDomain]WaitForestAvailability' + } + } +} + + diff --git a/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_ADDomainControllerProperties/MSFT_ADDomainControllerProperties.psm1 b/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_ADDomainControllerProperties/MSFT_ADDomainControllerProperties.psm1 new file mode 100644 index 0000000..12f984a --- /dev/null +++ b/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_ADDomainControllerProperties/MSFT_ADDomainControllerProperties.psm1 @@ -0,0 +1,197 @@ +$script:resourceModulePath = Split-Path -Path (Split-Path -Path $PSScriptRoot -Parent) -Parent +$script:modulesFolderPath = Join-Path -Path $script:resourceModulePath -ChildPath 'Modules' + +$script:localizationModulePath = Join-Path -Path $script:modulesFolderPath -ChildPath 'ActiveDirectoryDsc.Common' +Import-Module -Name (Join-Path -Path $script:localizationModulePath -ChildPath 'ActiveDirectoryDsc.Common.psm1') + +$script:localizedData = Get-LocalizedData -ResourceName 'MSFT_ADDomainControllerProperties' + +<# + .SYNOPSIS + Returns the current state of the properties of the domain controller. + + .PARAMETER IsSingleInstance + Specifies the resource is a single instance, the value must be 'Yes'. +#> +function Get-TargetResource +{ + [CmdletBinding()] + [OutputType([System.Collections.Hashtable])] + param + ( + [Parameter(Mandatory = $true)] + [ValidateSet('Yes')] + [System.String] + $IsSingleInstance + ) + + Write-Verbose -Message ( + $script:localizedData.RetrievingProperties -f $env:COMPUTERNAME + ) + + $getTargetResourceReturnValue = @{ + IsSingleInstance = $IsSingleInstance + ContentFreshness = 0 + } + + $getCimInstanceParameters = @{ + Namespace = 'ROOT/MicrosoftDfs' + Query = 'select MaxOfflineTimeInDays from DfsrMachineConfig' + } + + $getTargetResourceReturnValue['ContentFreshness'] = (Get-CimInstance @getCimInstanceParameters).MaxOfflineTimeInDays + + return $getTargetResourceReturnValue +} + +<# + .SYNOPSIS + Determines if the properties are in the desired state. + + .PARAMETER IsSingleInstance + Specifies the resource is a single instance, the value must be 'Yes'. + + .PARAMETER ContentFreshness + Specifies the Distributed File System Replication (DFSR) server threshold + after the number of days its content is considered stale (MaxOfflineTimeInDays) + Once the content is considered stale, the Distributed File System Replication + (DFSR) server will no longer be able to replicate. +#> +function Test-TargetResource +{ + [CmdletBinding()] + [OutputType([System.Boolean])] + param + ( + [Parameter(Mandatory = $true)] + [ValidateSet('Yes')] + [System.String] + $IsSingleInstance, + + [Parameter()] + [ValidateNotNullOrEmpty()] + [System.UInt32] + $ContentFreshness + ) + + Write-Verbose -Message ( + $script:localizedData.TestConfiguration -f $env:COMPUTERNAME + ) + + $compareTargetResourceStateResult = Compare-TargetResourceState @PSBoundParameters + + if ($false -in $compareTargetResourceStateResult.InDesiredState) + { + Write-Verbose -Message $script:localizedData.DomainControllerNotInDesiredState + + $testTargetResourceReturnValue = $false + } + else + { + Write-Verbose -Message $script:localizedData.DomainControllerInDesiredState + + $testTargetResourceReturnValue = $true + } + + return $testTargetResourceReturnValue +} + +<# + .SYNOPSIS + Sets the properties on the Active Directory domain controller. + + .PARAMETER IsSingleInstance + Specifies the resource is a single instance, the value must be 'Yes'. + + .PARAMETER ContentFreshness + Specifies the Distributed File System Replication (DFSR) server threshold + after the number of days its content is considered stale (MaxOfflineTimeInDays) + Once the content is considered stale, the Distributed File System Replication + (DFSR) server will no longer be able to replicate. +#> +function Set-TargetResource +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [ValidateSet('Yes')] + [System.String] + $IsSingleInstance, + + [Parameter()] + [ValidateNotNullOrEmpty()] + [System.UInt32] + $ContentFreshness + ) + + $compareTargetResourceStateResult = Compare-TargetResourceState @PSBoundParameters + + # Get all properties that are not in desired state. + $propertiesNotInDesiredState = $compareTargetResourceStateResult | Where-Object -FilterScript { + -not $_.InDesiredState + } + + if ($propertiesNotInDesiredState.Where( { $_.ParameterName -eq 'ContentFreshness' })) + { + Write-Verbose -Message ( + $script:localizedData.ContentFreshnessUpdated -f $ContentFreshness + ) + + $setCimInstanceParameters = @{ + Namespace = 'ROOT/MicrosoftDfs' + Query = 'select MaxOfflineTimeInDays from DfsrMachineConfig' + Property = @{ + MaxOfflineTimeInDays = $ContentFreshness + } + + } + + $null = Set-CimInstance @setCimInstanceParameters + } +} + +<# + .SYNOPSIS + Compares the properties in the current state with the properties of the + desired state and returns a hashtable with the comparison result. + + .PARAMETER IsSingleInstance + Specifies the resource is a single instance, the value must be 'Yes'. + + .PARAMETER ContentFreshness + Specifies the Distributed File System Replication (DFSR) server threshold + after the number of days its content is considered stale (MaxOfflineTimeInDays) + Once the content is considered stale, the Distributed File System Replication + (DFSR) server will no longer be able to replicate. +#> +function Compare-TargetResourceState +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [ValidateSet('Yes')] + [System.String] + $IsSingleInstance, + + [Parameter()] + [ValidateNotNullOrEmpty()] + [System.UInt32] + $ContentFreshness + ) + + $getTargetResourceParameters = @{ + IsSingleInstance = $IsSingleInstance + } + + $getTargetResourceResult = Get-TargetResource @getTargetResourceParameters + + $compareTargetResourceStateParameters = @{ + CurrentValues = $getTargetResourceResult + DesiredValues = $PSBoundParameters + Properties = @('ContentFreshness') + } + + return Compare-ResourcePropertyState @compareTargetResourceStateParameters +} diff --git a/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_ADDomainControllerProperties/MSFT_ADDomainControllerProperties.schema.mof b/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_ADDomainControllerProperties/MSFT_ADDomainControllerProperties.schema.mof new file mode 100644 index 0000000..b358aa4 --- /dev/null +++ b/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_ADDomainControllerProperties/MSFT_ADDomainControllerProperties.schema.mof @@ -0,0 +1,6 @@ +[ClassVersion("1.0.0.0"), FriendlyName("ADDomainControllerProperties")] +class MSFT_ADDomainControllerProperties : OMI_BaseResource +{ + [Key, Description("Specifies the resource is a single instance, the value must be 'Yes'."), ValueMap{"Yes"}, Values{"Yes"}] String IsSingleInstance; + [Write, Description("Specifies the Distributed File System Replication (DFSR) server threshold after the number of days its content is considered stale (MaxOfflineTimeInDays). Once the content is considered stale, the Distributed File System Replication (DFSR) server will no longer be able to replicate.")] UInt32 ContentFreshness; +}; diff --git a/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_ADDomainControllerProperties/README.md b/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_ADDomainControllerProperties/README.md new file mode 100644 index 0000000..8c24edc --- /dev/null +++ b/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_ADDomainControllerProperties/README.md @@ -0,0 +1,8 @@ +# Description + +This resource enforces the single instance properties of a domain controller. +*Properties that must always have a value, but the value can be changed.* + +## Requirements + +* Target machine must be running Windows Server 2008 R2 or later. diff --git a/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_ADDomainControllerProperties/en-US/MSFT_ADDomainControllerProperties.strings.psd1 b/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_ADDomainControllerProperties/en-US/MSFT_ADDomainControllerProperties.strings.psd1 new file mode 100644 index 0000000..5a4971d --- /dev/null +++ b/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_ADDomainControllerProperties/en-US/MSFT_ADDomainControllerProperties.strings.psd1 @@ -0,0 +1,8 @@ +# culture="en-US" +ConvertFrom-StringData @' + RetrievingProperties = Retrieving the properties for the domain controller '{0}'. (ADDCP0001) + TestConfiguration = Determining the current state of the properties on the domain controller '{0}'. (ADDCP0002) + DomainControllerInDesiredState = The domain controller is in the desired state. (ADDCP0003) + DomainControllerNotInDesiredState = The domain controller is not in the desired state. (ADDCP0004) + ContentFreshnessUpdated = The content freshness property (MaxOfflineTimeInDays) will be updated to {0} days. (ADDCP0005) +'@ diff --git a/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_ADDomainControllerProperties/en-US/about_ADDomainControllerProperties.help.txt b/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_ADDomainControllerProperties/en-US/about_ADDomainControllerProperties.help.txt new file mode 100644 index 0000000..3b6ecf3 --- /dev/null +++ b/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_ADDomainControllerProperties/en-US/about_ADDomainControllerProperties.help.txt @@ -0,0 +1,39 @@ +.NAME + ADDomainControllerProperties + +.DESCRIPTION + This resource enforces the single instance properties of a domain controller. + *Properties that must always have a value, but the value can be changed.* + + ## Requirements + + * Target machine must be running Windows Server 2008 R2 or later. + +.PARAMETER IsSingleInstance + Key - String + Allowed values: Yes + Specifies the resource is a single instance, the value must be 'Yes'. + +.PARAMETER ContentFreshness + Write - UInt32 + Specifies the Distributed File System Replication (DFSR) server threshold after the number of days its content is considered stale (MaxOfflineTimeInDays). Once the content is considered stale, the Distributed File System Replication (DFSR) server will no longer be able to replicate. + +.EXAMPLE 1 + +This configuration will set the content freshness to 100 days. + +Configuration ADDomainControllerProperties_SetContentFreshness_Config +{ + Import-DscResource -ModuleName ActiveDirectoryDsc + + node localhost + { + ADDomainControllerProperties 'ContentFreshness' + { + IsSingleInstance = 'Yes' + ContentFreshness = 100 + } + } +} + + diff --git a/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_ADDomainDefaultPasswordPolicy/MSFT_ADDomainDefaultPasswordPolicy.psm1 b/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_ADDomainDefaultPasswordPolicy/MSFT_ADDomainDefaultPasswordPolicy.psm1 new file mode 100644 index 0000000..7ed0a90 --- /dev/null +++ b/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_ADDomainDefaultPasswordPolicy/MSFT_ADDomainDefaultPasswordPolicy.psm1 @@ -0,0 +1,374 @@ +$script:resourceModulePath = Split-Path -Path (Split-Path -Path $PSScriptRoot -Parent) -Parent +$script:modulesFolderPath = Join-Path -Path $script:resourceModulePath -ChildPath 'Modules' + +$script:localizationModulePath = Join-Path -Path $script:modulesFolderPath -ChildPath 'ActiveDirectoryDsc.Common' +Import-Module -Name (Join-Path -Path $script:localizationModulePath -ChildPath 'ActiveDirectoryDsc.Common.psm1') + +$script:localizedData = Get-LocalizedData -ResourceName 'MSFT_ADDomainDefaultPasswordPolicy' + +# List of changeable policy properties +$mutablePropertyMap = @( + @{ + Name = 'ComplexityEnabled' + } + @{ + Name = 'LockoutDuration' + IsTimeSpan = $true + } + @{ + Name = 'LockoutObservationWindow' + IsTimeSpan = $true + } + @{ + Name = 'LockoutThreshold' + } + @{ + Name = 'MinPasswordAge' + IsTimeSpan = $true + } + @{ + Name = 'MaxPasswordAge' + IsTimeSpan = $true + } + @{ + Name = 'MinPasswordLength' + } + @{ + Name = 'PasswordHistoryCount' + } + @{ + Name = 'ReversibleEncryptionEnabled' + } +) + +<# + .SYNOPSIS + Returns the current state of the Active Directory default domain password + policy. + + .PARAMETER DomainName + Name of the domain to which the password policy will be applied. + + .PARAMETER DomainController + Active Directory domain controller to enact the change upon. + + .PARAMETER Credential + Credentials used to access the domain. +#> +function Get-TargetResource +{ + [CmdletBinding()] + [OutputType([System.Collections.Hashtable])] + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $DomainName, + + [Parameter()] + [ValidateNotNullOrEmpty()] + [System.String] + $DomainController, + + [Parameter()] + [System.Management.Automation.PSCredential] + [System.Management.Automation.CredentialAttribute()] + $Credential + ) + + Assert-Module -ModuleName 'ActiveDirectory' + + $PSBoundParameters['Identity'] = $DomainName + + $getADDefaultDomainPasswordPolicyParams = Get-ADCommonParameters @PSBoundParameters + + Write-Verbose -Message ($script:localizedData.QueryingDomainPasswordPolicy -f $DomainName) + + $policy = Get-ADDefaultDomainPasswordPolicy @getADDefaultDomainPasswordPolicyParams + + return @{ + DomainName = $DomainName + ComplexityEnabled = $policy.ComplexityEnabled + LockoutDuration = ConvertFrom-Timespan -Timespan $policy.LockoutDuration -TimeSpanType Minutes + LockoutObservationWindow = ConvertFrom-Timespan -Timespan $policy.LockoutObservationWindow -TimeSpanType Minutes + LockoutThreshold = $policy.LockoutThreshold + MinPasswordAge = ConvertFrom-Timespan -Timespan $policy.MinPasswordAge -TimeSpanType Minutes + MaxPasswordAge = ConvertFrom-Timespan -Timespan $policy.MaxPasswordAge -TimeSpanType Minutes + MinPasswordLength = $policy.MinPasswordLength + PasswordHistoryCount = $policy.PasswordHistoryCount + ReversibleEncryptionEnabled = $policy.ReversibleEncryptionEnabled + } +} #end Get-TargetResource + +<# + .SYNOPSIS + Determines if the Active Directory default domain password policy is in + the desired state + + .PARAMETER DomainName + Name of the domain to which the password policy will be applied. + + .PARAMETER ComplexityEnabled + Whether password complexity is enabled for the default password policy. + + .PARAMETER LockoutDuration + Length of time that an account is locked after the number of failed login attempts (minutes). + + .PARAMETER LockoutObservationWindow + Maximum time between two unsuccessful login attempts before the counter is reset to 0 (minutes). + + .PARAMETER LockoutThreshold + Number of unsuccessful login attempts that are permitted before an account is locked out. + + .PARAMETER MinPasswordAge + Minimum length of time that you can have the same password (minutes). + + .PARAMETER MaxPasswordAge + Maximum length of time that you can have the same password (minutes). + + .PARAMETER MinPasswordLength + Minimum number of characters that a password must contain. + + .PARAMETER PasswordHistoryCount + Number of previous passwords to remember. + + .PARAMETER ReversibleEncryptionEnabled + Whether the directory must store passwords using reversible encryption. + + .PARAMETER DomainController + Active Directory domain controller to enact the change upon. + + .PARAMETER Credential + Credentials used to access the domain. +#> +function Test-TargetResource +{ + [CmdletBinding()] + [OutputType([System.Boolean])] + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $DomainName, + + [Parameter()] + [System.Boolean] + $ComplexityEnabled, + + [Parameter()] + [System.UInt32] + $LockoutDuration, + + [Parameter()] + [System.UInt32] + $LockoutObservationWindow, + + [Parameter()] + [System.UInt32] + $LockoutThreshold, + + [Parameter()] + [System.UInt32] + $MinPasswordAge, + + [Parameter()] + [System.UInt32] + $MaxPasswordAge, + + [Parameter()] + [System.UInt32] + $MinPasswordLength, + + [Parameter()] + [System.UInt32] + $PasswordHistoryCount, + + [Parameter()] + [System.Boolean] + $ReversibleEncryptionEnabled, + + [Parameter()] + [ValidateNotNullOrEmpty()] + [System.String] + $DomainController, + + [Parameter()] + [System.Management.Automation.PSCredential] + [System.Management.Automation.CredentialAttribute()] + $Credential + ) + + $getTargetResourceParams = @{ + DomainName = $DomainName + } + + if ($PSBoundParameters.ContainsKey('Credential')) + { + $getTargetResourceParams['Credential'] = $Credential + } + + if ($PSBoundParameters.ContainsKey('DomainController')) + { + $getTargetResourceParams['DomainController'] = $DomainController + } + + $targetResource = Get-TargetResource @getTargetResourceParams + + $inDesiredState = $true + foreach ($property in $mutablePropertyMap) + { + $propertyName = $property.Name + + if ($PSBoundParameters.ContainsKey($propertyName)) + { + $expectedValue = $PSBoundParameters[$propertyName] + $actualValue = $targetResource[$propertyName] + + if ($expectedValue -ne $actualValue) + { + $valueIncorrectMessage = $script:localizedData.ResourcePropertyValueIncorrect -f $propertyName, $expectedValue, $actualValue + Write-Verbose -Message $valueIncorrectMessage + $inDesiredState = $false + } + } + } + + if ($inDesiredState) + { + Write-Verbose -Message ($script:localizedData.ResourceInDesiredState -f $DomainName) + return $true + } + else + { + Write-Verbose -Message ($script:localizedData.ResourceNotInDesiredState -f $DomainName) + return $false + } +} #end Test-TargetResource + +<# + .SYNOPSIS + Modifies the Active Directory default domain password policy. + + .PARAMETER DomainName + Name of the domain to which the password policy will be applied. + + .PARAMETER ComplexityEnabled + Whether password complexity is enabled for the default password policy. + + .PARAMETER LockoutDuration + Length of time that an account is locked after the number of failed login attempts (minutes). + + .PARAMETER LockoutObservationWindow + Maximum time between two unsuccessful login attempts before the counter is reset to 0 (minutes). + + .PARAMETER LockoutThreshold + Number of unsuccessful login attempts that are permitted before an account is locked out. + + .PARAMETER MinPasswordAge + Minimum length of time that you can have the same password (minutes). + + .PARAMETER MaxPasswordAge + Maximum length of time that you can have the same password (minutes). + + .PARAMETER MinPasswordLength + Minimum number of characters that a password must contain. + + .PARAMETER PasswordHistoryCount + Number of previous passwords to remember. + + .PARAMETER ReversibleEncryptionEnabled + Whether the directory must store passwords using reversible encryption. + + .PARAMETER DomainController + Active Directory domain controller to enact the change upon. + + .PARAMETER Credential + Credentials used to access the domain. +#> +function Set-TargetResource +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $DomainName, + + [Parameter()] + [System.Boolean] + $ComplexityEnabled, + + [Parameter()] + [System.UInt32] + $LockoutDuration, + + [Parameter()] + [System.UInt32] + $LockoutObservationWindow, + + [Parameter()] + [System.UInt32] + $LockoutThreshold, + + [Parameter()] + [System.UInt32] + $MinPasswordAge, + + [Parameter()] + [System.UInt32] + $MaxPasswordAge, + + [Parameter()] + [System.UInt32] + $MinPasswordLength, + + [Parameter()] + [System.UInt32] + $PasswordHistoryCount, + + [Parameter()] + [System.Boolean] + $ReversibleEncryptionEnabled, + + [Parameter()] + [ValidateNotNullOrEmpty()] + [System.String] + $DomainController, + + [Parameter()] + [System.Management.Automation.PSCredential] + [System.Management.Automation.CredentialAttribute()] + $Credential + ) + + Assert-Module -ModuleName 'ActiveDirectory' + + $PSBoundParameters['Identity'] = $DomainName + + $setADDefaultDomainPasswordPolicyParams = Get-ADCommonParameters @PSBoundParameters + + foreach ($property in $mutablePropertyMap) + { + $propertyName = $property.Name + + if ($PSBoundParameters.ContainsKey($propertyName)) + { + $propertyValue = $PSBoundParameters[$propertyName] + + if ($property.IsTimeSpan -eq $true) + { + $propertyValue = ConvertTo-TimeSpan -TimeSpan $propertyValue -TimeSpanType Minutes + } + + $setADDefaultDomainPasswordPolicyParams[$propertyName] = $propertyValue + + Write-Verbose -Message ($script:localizedData.SettingPasswordPolicyValue -f $propertyName, $propertyValue) + } + } + + Write-Verbose -Message ($script:localizedData.UpdatingDomainPasswordPolicy -f $DomainName) + + [ref] $null = Set-ADDefaultDomainPasswordPolicy @setADDefaultDomainPasswordPolicyParams +} #end Set-TargetResource + +Export-ModuleMember -Function *-TargetResource diff --git a/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_ADDomainDefaultPasswordPolicy/MSFT_ADDomainDefaultPasswordPolicy.schema.mof b/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_ADDomainDefaultPasswordPolicy/MSFT_ADDomainDefaultPasswordPolicy.schema.mof new file mode 100644 index 0000000..d34e317 --- /dev/null +++ b/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_ADDomainDefaultPasswordPolicy/MSFT_ADDomainDefaultPasswordPolicy.schema.mof @@ -0,0 +1,17 @@ +[ClassVersion("1.0.0"), FriendlyName("ADDomainDefaultPasswordPolicy")] +class MSFT_ADDomainDefaultPasswordPolicy : OMI_BaseResource +{ + [Key, Description("Name of the domain to which the password policy will be applied.")] String DomainName; + [Write, Description("Whether password complexity is enabled for the default password policy.")] Boolean ComplexityEnabled; + [Write, Description("Length of time that an account is locked after the number of failed login attempts (minutes).")] UInt32 LockoutDuration; + [Write, Description("Maximum time between two unsuccessful login attempts before the counter is reset to 0 (minutes).")] UInt32 LockoutObservationWindow; + [Write, Description("Number of unsuccessful login attempts that are permitted before an account is locked out.")] UInt32 LockoutThreshold; + [Write, Description("Minimum length of time that you can have the same password (minutes).")] UInt32 MinPasswordAge; + [Write, Description("Maximum length of time that you can have the same password (minutes).")] UInt32 MaxPasswordAge; + [Write, Description("Minimum number of characters that a password must contain.")] UInt32 MinPasswordLength; + [Write, Description("Number of previous passwords to remember.")] UInt32 PasswordHistoryCount; + [Write, Description("Whether the directory must store passwords using reversible encryption.")] Boolean ReversibleEncryptionEnabled; + [Write, Description("Active Directory domain controller to enact the change upon.")] String DomainController; + [Write, Description("Credentials used to access the domain."), EmbeddedInstance("MSFT_Credential")] String Credential; +}; + diff --git a/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_ADDomainDefaultPasswordPolicy/README.md b/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_ADDomainDefaultPasswordPolicy/README.md new file mode 100644 index 0000000..117554b --- /dev/null +++ b/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_ADDomainDefaultPasswordPolicy/README.md @@ -0,0 +1,7 @@ +# Description + +The ADDomainDefaultPasswordPolicy DSC resource will manage an Active Directory domain's default password policy. + +## Requirements + +* Target machine must be running Windows Server 2008 R2 or later. diff --git a/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_ADDomainDefaultPasswordPolicy/en-US/MSFT_ADDomainDefaultPasswordPolicy.strings.psd1 b/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_ADDomainDefaultPasswordPolicy/en-US/MSFT_ADDomainDefaultPasswordPolicy.strings.psd1 new file mode 100644 index 0000000..ee44acc --- /dev/null +++ b/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_ADDomainDefaultPasswordPolicy/en-US/MSFT_ADDomainDefaultPasswordPolicy.strings.psd1 @@ -0,0 +1,9 @@ +# culture="en-US" +ConvertFrom-StringData @' + QueryingDomainPasswordPolicy = Querying Active Directory domain '{0}' default password policy. (ADDDPP0001) + UpdatingDomainPasswordPolicy = Updating Active Directory domain '{0}' default password policy. (ADDDPP0002) + SettingPasswordPolicyValue = Setting password policy '{0}' property to '{1}'. (ADDDPP0003) + ResourcePropertyValueIncorrect = Property '{0}' value is incorrect; expected '{1}', actual '{2}'. (ADDDPP0004) + ResourceInDesiredState = Resource '{0}' is in the desired state. (ADDDPP0005) + ResourceNotInDesiredState = Resource '{0}' is NOT in the desired state. (ADDDPP0006) +'@ diff --git a/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_ADDomainDefaultPasswordPolicy/en-US/about_ADDomainDefaultPasswordPolicy.help.txt b/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_ADDomainDefaultPasswordPolicy/en-US/about_ADDomainDefaultPasswordPolicy.help.txt new file mode 100644 index 0000000..9e7f687 --- /dev/null +++ b/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_ADDomainDefaultPasswordPolicy/en-US/about_ADDomainDefaultPasswordPolicy.help.txt @@ -0,0 +1,94 @@ +.NAME + ADDomainDefaultPasswordPolicy + +.DESCRIPTION + The ADDomainDefaultPasswordPolicy DSC resource will manage an Active Directory domain's default password policy. + + ## Requirements + + * Target machine must be running Windows Server 2008 R2 or later. + +.PARAMETER DomainName + Key - String + Name of the domain to which the password policy will be applied. + +.PARAMETER ComplexityEnabled + Write - Boolean + Whether password complexity is enabled for the default password policy. + +.PARAMETER LockoutDuration + Write - UInt32 + Length of time that an account is locked after the number of failed login attempts (minutes). + +.PARAMETER LockoutObservationWindow + Write - UInt32 + Maximum time between two unsuccessful login attempts before the counter is reset to 0 (minutes). + +.PARAMETER LockoutThreshold + Write - UInt32 + Number of unsuccessful login attempts that are permitted before an account is locked out. + +.PARAMETER MinPasswordAge + Write - UInt32 + Minimum length of time that you can have the same password (minutes). + +.PARAMETER MaxPasswordAge + Write - UInt32 + Maximum length of time that you can have the same password (minutes). + +.PARAMETER MinPasswordLength + Write - UInt32 + Minimum number of characters that a password must contain. + +.PARAMETER PasswordHistoryCount + Write - UInt32 + Number of previous passwords to remember. + +.PARAMETER ReversibleEncryptionEnabled + Write - Boolean + Whether the directory must store passwords using reversible encryption. + +.PARAMETER DomainController + Write - String + Active Directory domain controller to enact the change upon. + +.PARAMETER Credential + Write - PSCredential + Credentials used to access the domain. + +.EXAMPLE 1 + +This configuration will set an Active Directory domain's default password +policy to set the minimum password length and complexity. + +Configuration ADDomainDefaultPasswordPolicy_ConfigureDefaultPasswordPolicy_Config +{ + Param + ( + [Parameter(Mandatory = $true)] + [System.String] + $DomainName, + + [Parameter(Mandatory = $true)] + [System.Boolean] + $ComplexityEnabled, + + [Parameter(Mandatory = $true)] + [System.Int32] + $MinPasswordLength + ) + + Import-DscResource -Module ActiveDirectoryDsc + + Node localhost + { + ADDomainDefaultPasswordPolicy 'DefaultPasswordPolicy' + { + DomainName = $DomainName + ComplexityEnabled = $ComplexityEnabled + MinPasswordLength = $MinPasswordLength + } + } +} + + diff --git a/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_ADDomainFunctionalLevel/MSFT_ADDomainFunctionalLevel.psm1 b/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_ADDomainFunctionalLevel/MSFT_ADDomainFunctionalLevel.psm1 new file mode 100644 index 0000000..b71b04f --- /dev/null +++ b/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_ADDomainFunctionalLevel/MSFT_ADDomainFunctionalLevel.psm1 @@ -0,0 +1,194 @@ +$script:resourceModulePath = Split-Path -Path (Split-Path -Path $PSScriptRoot -Parent) -Parent +$script:modulesFolderPath = Join-Path -Path $script:resourceModulePath -ChildPath 'Modules' + +$script:localizationModulePath = Join-Path -Path $script:modulesFolderPath -ChildPath 'ActiveDirectoryDsc.Common' +Import-Module -Name (Join-Path -Path $script:localizationModulePath -ChildPath 'ActiveDirectoryDsc.Common.psm1') + +$script:localizedData = Get-LocalizedData -ResourceName 'MSFT_ADDomainFunctionalLevel' + +<# + .SYNOPSIS + Returns the current functional level of the domain. + + .PARAMETER DomainIdentity + Specifies the Active Directory domain to modify. You can identify a + domain by its distinguished name, GUID, security identifier, DNS domain + name, or NetBIOS domain name. + + .PARAMETER DomainMode + Specifies the functional level for the Active Directory domain. + + Not used in Get-TargetResource. +#> +function Get-TargetResource +{ + [CmdletBinding()] + [OutputType([System.Collections.Hashtable])] + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $DomainIdentity, + + [Parameter(Mandatory = $true)] + [ValidateSet('Windows2008R2Domain', 'Windows2012Domain', 'Windows2012R2Domain', 'Windows2016Domain')] + [System.String] + $DomainMode + ) + + Write-Verbose -Message ( + $script:localizedData.RetrievingDomainMode -f $DomainIdentity + ) + + $getTargetResourceReturnValue = @{ + DomainIdentity = $DomainIdentity + DomainMode = $null + } + + $domainObject = Get-ADDomain -Identity $DomainIdentity -ErrorAction 'Stop' + + $getTargetResourceReturnValue['DomainMode'] = $domainObject.DomainMode + + return $getTargetResourceReturnValue +} + +<# + .SYNOPSIS + Determines if the functional level is in the desired state. + + .PARAMETER DomainIdentity + Specifies the Active Directory domain to modify. You can identify a + domain by its distinguished name, GUID, security identifier, DNS domain + name, or NetBIOS domain name. + + .PARAMETER DomainMode + Specifies the functional level for the Active Directory domain. +#> +function Test-TargetResource +{ + [CmdletBinding()] + [OutputType([System.Boolean])] + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $DomainIdentity, + + [Parameter(Mandatory = $true)] + [ValidateSet('Windows2008R2Domain', 'Windows2012Domain', 'Windows2012R2Domain', 'Windows2016Domain')] + [System.String] + $DomainMode + ) + + Write-Verbose -Message ( + $script:localizedData.TestConfiguration -f $DomainIdentity + ) + + $compareTargetResourceStateResult = Compare-TargetResourceState @PSBoundParameters + + if ($false -in $compareTargetResourceStateResult.InDesiredState) + { + Write-Verbose -Message $script:localizedData.LevelNotInDesiredState + + $testTargetResourceReturnValue = $false + } + else + { + Write-Verbose -Message $script:localizedData.LevelInDesiredState + + $testTargetResourceReturnValue = $true + } + + return $testTargetResourceReturnValue +} + +<# + .SYNOPSIS + Sets the functional level on the Active Directory domain. + + .PARAMETER DomainIdentity + Specifies the Active Directory domain to modify. You can identify a + domain by its distinguished name, GUID, security identifier, DNS domain + name, or NetBIOS domain name. + + .PARAMETER DomainMode + Specifies the functional level for the Active Directory domain. +#> +function Set-TargetResource +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $DomainIdentity, + + [Parameter(Mandatory = $true)] + [ValidateSet('Windows2008R2Domain', 'Windows2012Domain', 'Windows2012R2Domain', 'Windows2016Domain')] + [System.String] + $DomainMode + ) + + $compareTargetResourceStateResult = Compare-TargetResourceState @PSBoundParameters + + # Get all properties that are not in desired state. + $propertiesNotInDesiredState = $compareTargetResourceStateResult | Where-Object -FilterScript { + -not $_.InDesiredState + } + + $domainModeProperty = $propertiesNotInDesiredState.Where( { $_.ParameterName -eq 'DomainMode' }) + + if ($domainModeProperty) + { + Write-Verbose -Message ( + $script:localizedData.DomainModeUpdating -f $domainModeProperty.Actual, $DomainMode + ) + + $setADDomainModeParameters = @{ + Identity = $DomainIdentity + DomainMode = [Microsoft.ActiveDirectory.Management.ADDomainMode]::$DomainMode + Confirm = $false + } + + Set-ADDomainMode @setADDomainModeParameters + } +} + +<# + .SYNOPSIS + Compares the properties in the current state with the properties of the + desired state and returns a hashtable with the comparison result. + + .PARAMETER DomainIdentity + Specifies the Active Directory domain to modify. You can identify a + domain by its distinguished name, GUID, security identifier, DNS domain + name, or NetBIOS domain name. + + .PARAMETER DomainMode + Specifies the functional level for the Active Directory domain. +#> +function Compare-TargetResourceState +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $DomainIdentity, + + [Parameter(Mandatory = $true)] + [ValidateSet('Windows2008R2Domain', 'Windows2012Domain', 'Windows2012R2Domain', 'Windows2016Domain')] + [System.String] + $DomainMode + ) + + $getTargetResourceResult = Get-TargetResource @PSBoundParameters + + $compareTargetResourceStateParameters = @{ + CurrentValues = $getTargetResourceResult + DesiredValues = $PSBoundParameters + Properties = @('DomainMode') + } + + return Compare-ResourcePropertyState @compareTargetResourceStateParameters +} diff --git a/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_ADDomainFunctionalLevel/MSFT_ADDomainFunctionalLevel.schema.mof b/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_ADDomainFunctionalLevel/MSFT_ADDomainFunctionalLevel.schema.mof new file mode 100644 index 0000000..1eb8c7b --- /dev/null +++ b/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_ADDomainFunctionalLevel/MSFT_ADDomainFunctionalLevel.schema.mof @@ -0,0 +1,6 @@ +[ClassVersion("1.0.0.0"), FriendlyName("ADDomainFunctionalLevel")] +class MSFT_ADDomainFunctionalLevel : OMI_BaseResource +{ + [Key, Description("Specifies the Active Directory domain to modify. You can identify a domain by its distinguished name, GUID, security identifier, DNS domain name, or NetBIOS domain name.")] String DomainIdentity; + [Required, Description("Specifies the functional level for the Active Directory domain."), ValueMap{"Windows2008R2Domain", "Windows2012Domain", "Windows2012R2Domain", "Windows2016Domain"}, Values{"Windows2008R2Domain", "Windows2012Domain", "Windows2012R2Domain", "Windows2016Domain"}] String DomainMode; +}; diff --git a/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_ADDomainFunctionalLevel/README.md b/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_ADDomainFunctionalLevel/README.md new file mode 100644 index 0000000..c8f260e --- /dev/null +++ b/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_ADDomainFunctionalLevel/README.md @@ -0,0 +1,15 @@ +# Description + +This resource changes the domain functional level. For further details, see [Forest and Domain Functional Levels](https://docs.microsoft.com/en-us/windows-server/identity/ad-ds/active-directory-functional-levels). + +**WARNING: This action might be irreversible!** Make sure you understand +the consequences of changing the domain functional level. + +Read more about raising function levels and potential roll back +scenarios in the Active Directory documentation. For example: [Upgrade Domain Controllers to Windows Server 2016](https://docs.microsoft.com/en-us/windows-server/identity/ad-ds/deploy/upgrade-domain-controllers). + +## Requirements + +* Target machine must be running Windows Server 2008 R2 or later. +* Target machine must be running the minimum required operating system + version for the domain functional level to set. diff --git a/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_ADDomainFunctionalLevel/en-US/MSFT_ADDomainFunctionalLevel.strings.psd1 b/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_ADDomainFunctionalLevel/en-US/MSFT_ADDomainFunctionalLevel.strings.psd1 new file mode 100644 index 0000000..f9c0c52 --- /dev/null +++ b/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_ADDomainFunctionalLevel/en-US/MSFT_ADDomainFunctionalLevel.strings.psd1 @@ -0,0 +1,8 @@ +# culture="en-US" +ConvertFrom-StringData @' + RetrievingDomainMode = Retrieving the domain functional level for the domain '{0}'. (ADDFL0001) + TestConfiguration = Determining the current domain functional level in the domain '{0}'. (ADDFL0002) + LevelInDesiredState = The domain functional level is in the desired state. (ADDFL0003) + LevelNotInDesiredState = The domain functional level is not in the desired state. (ADDFL0004) + DomainModeUpdating = The domain functional level will change from '{0}' to '{1}'. (ADDFL0005) +'@ diff --git a/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_ADDomainFunctionalLevel/en-US/about_ADDomainFunctionalLevel.help.txt b/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_ADDomainFunctionalLevel/en-US/about_ADDomainFunctionalLevel.help.txt new file mode 100644 index 0000000..cbd21de --- /dev/null +++ b/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_ADDomainFunctionalLevel/en-US/about_ADDomainFunctionalLevel.help.txt @@ -0,0 +1,47 @@ +.NAME + ADDomainFunctionalLevel + +.Description + This resource changes the domain functional level. For further details, see [Forest and Domain Functional Levels](https://docs.microsoft.com/en-us/windows-server/identity/ad-ds/active-directory-functional-levels). + + **WARNING: This action might be irreversible!** Make sure you understand + the consequences of changing the domain functional level. + + Read more about raising function levels and potential roll back + scenarios in the Active Directory documentation. For example: [Upgrade Domain Controllers to Windows Server 2016](https://docs.microsoft.com/en-us/windows-server/identity/ad-ds/deploy/upgrade-domain-controllers). + + ## Requirements + + * Target machine must be running Windows Server 2008 R2 or later. + * Target machine must be running the minimum required operating system + version for the domain functional level to set. + +.PARAMETER DomainIdentity + Key - String + Specifies the Active Directory domain to modify. You can identify a domain by its distinguished name, GUID, security identifier, DNS domain name, or NetBIOS domain name. + +.PARAMETER DomainMode + Required - String + Allowed values: Windows2008R2Domain, Windows2012Domain, Windows2012R2Domain, Windows2016Domain + Specifies the functional level for the Active Directory domain. + +.EXAMPLE 1 + +This configuration will change the domain functional level to +a Windows Server 2012 R2 Domain. + +Configuration ADDomainFunctionalLevel_SetLevel_Config +{ + Import-DscResource -ModuleName ActiveDirectoryDsc + + node localhost + { + ADDomainFunctionalLevel 'ChangeDomainFunctionalLevel' + { + DomainIdentity = 'contoso.com' + DomainMode = 'Windows2012R2Domain' + } + } +} + + diff --git a/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_ADDomainTrust/MSFT_ADDomainTrust.psm1 b/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_ADDomainTrust/MSFT_ADDomainTrust.psm1 new file mode 100644 index 0000000..9e66274 --- /dev/null +++ b/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_ADDomainTrust/MSFT_ADDomainTrust.psm1 @@ -0,0 +1,714 @@ +$script:resourceModulePath = Split-Path -Path (Split-Path -Path $PSScriptRoot -Parent) -Parent +$script:modulesFolderPath = Join-Path -Path $script:resourceModulePath -ChildPath 'Modules' + +$script:localizationModulePath = Join-Path -Path $script:modulesFolderPath -ChildPath 'ActiveDirectoryDsc.Common' +Import-Module -Name (Join-Path -Path $script:localizationModulePath -ChildPath 'ActiveDirectoryDsc.Common.psm1') + +$script:localizedData = Get-LocalizedData -ResourceName 'MSFT_ADDomainTrust' + +<# + .SYNOPSIS + Returns the current state of the Active Directory trust. + + .PARAMETER SourceDomainName + Specifies the name of the Active Directory domain that is requesting the + trust. + + .PARAMETER TargetDomainName + Specifies the name of the Active Directory domain that is being trusted. + + .PARAMETER TargetCredential + Specifies the credentials to authenticate to the target domain. + + .PARAMETER TrustType + Specifies the type of trust. The value 'External' means the context Domain, + while the value 'Forest' means the context 'Forest'. + + .PARAMETER TrustDirection + Specifies the direction of the trust. + + .PARAMETER AllowTrustRecreation + Specifies if the is allowed to be recreated if required. Default value is + $false. +#> +function Get-TargetResource +{ + [CmdletBinding()] + [OutputType([System.Collections.Hashtable])] + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $SourceDomainName, + + [Parameter(Mandatory = $true)] + [System.String] + $TargetDomainName, + + [Parameter(Mandatory = $true)] + [System.Management.Automation.PSCredential] + $TargetCredential, + + [Parameter(Mandatory = $true)] + [ValidateSet('External', 'Forest')] + [System.String] + $TrustType, + + [Parameter(Mandatory = $true)] + [ValidateSet('Bidirectional', 'Inbound', 'Outbound')] + [System.String] + $TrustDirection, + + [Parameter()] + [System.Boolean] + $AllowTrustRecreation = $false + ) + + # Return a credential object without the password. + $cimCredentialInstance = New-CimCredentialInstance -Credential $TargetCredential + + $returnValue = @{ + SourceDomainName = $SourceDomainName + TargetDomainName = $TargetDomainName + TargetCredential = $cimCredentialInstance + AllowTrustRecreation = $AllowTrustRecreation + } + + $getTrustTargetAndSourceObject = @{ + SourceDomainName = $SourceDomainName + TargetDomainName = $TargetDomainName + TargetCredential = $TargetCredential + TrustType = $TrustType + } + + $trustSource, $trustTarget = Get-TrustSourceAndTargetObject @getTrustTargetAndSourceObject + + try + { + # Find trust between source & destination. + Write-Verbose -Message ( + $script:localizedData.CheckingTrustMessage -f $SourceDomainName, $TargetDomainName, $directoryContextTyp + ) + + $trust = $trustSource.GetTrustRelationship($trustTarget) + + $returnValue['TrustDirection'] = $trust.TrustDirection + $returnValue['TrustType'] = ConvertFrom-DirectoryContextType -DirectoryContextType $trust.TrustType + + Write-Verbose -Message ($script:localizedData.TrustPresentMessage -f $SourceDomainName, $TargetDomainName, $directoryContextType) + + $returnValue['Ensure'] = 'Present' + } + catch + { + Write-Verbose -Message ($script:localizedData.TrustAbsentMessage -f $SourceDomainName, $TargetDomainName, $directoryContextType) + + $returnValue['Ensure'] = 'Absent' + $returnValue['TrustDirection'] = $null + $returnValue['TrustType'] = $null + } + + return $returnValue +} + +<# + .SYNOPSIS + Creates, removes, or updates the Active Directory trust so it is in the + desired state. + + .PARAMETER SourceDomainName + Specifies the name of the Active Directory domain that is requesting the + trust. + + .PARAMETER TargetDomainName + Specifies the name of the Active Directory domain that is being trusted. + + .PARAMETER TargetCredential + Specifies the credentials to authenticate to the target domain. + + .PARAMETER TrustType + Specifies the type of trust. The value 'External' means the context Domain, + while the value 'Forest' means the context 'Forest'. + + .PARAMETER TrustDirection + Specifies the direction of the trust. + + .PARAMETER Ensure + Specifies whether the computer account is present or absent. Default + value is 'Present'. + + .PARAMETER AllowTrustRecreation + Specifies if the is allowed to be recreated if required. Default value is + $false. +#> +function Set-TargetResource +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $SourceDomainName, + + [Parameter(Mandatory = $true)] + [System.String] + $TargetDomainName, + + [Parameter(Mandatory = $true)] + [System.Management.Automation.PSCredential] + $TargetCredential, + + [Parameter(Mandatory = $true)] + [ValidateSet('External', 'Forest')] + [System.String] + $TrustType, + + [Parameter(Mandatory = $true)] + [ValidateSet('Bidirectional', 'Inbound', 'Outbound')] + [System.String] + $TrustDirection, + + [Parameter()] + [ValidateSet('Present', 'Absent')] + [System.String] + $Ensure = 'Present', + + [Parameter()] + [System.Boolean] + $AllowTrustRecreation = $false + ) + + $getTrustTargetAndSourceObject = @{ + SourceDomainName = $SourceDomainName + TargetDomainName = $TargetDomainName + TargetCredential = $TargetCredential + TrustType = $TrustType + } + + $trustSource, $trustTarget = Get-TrustSourceAndTargetObject @getTrustTargetAndSourceObject + + # Only pass those properties that should be evaluated. + $compareTargetResourceStateParameters = @{} + $PSBoundParameters + $compareTargetResourceStateParameters.Remove('AllowTrustRecreation') + + $compareTargetResourceStateResult = Compare-TargetResourceState @compareTargetResourceStateParameters + + # Get all properties that are not in desired state. + $propertiesNotInDesiredState = $compareTargetResourceStateResult | + Where-Object -FilterScript { -not $_.InDesiredState } + + if ($propertiesNotInDesiredState.Where( { $_.ParameterName -eq 'Ensure' })) + { + if ($Ensure -eq 'Present') + { + # Create trust. + $trustSource.CreateTrustRelationship($trustTarget, $TrustDirection) + + Write-Verbose -Message ( + $script:localizedData.AddedTrust -f @( + $SourceDomainName, + $TargetDomainName, + $TrustType, + $TrustDirection + ) + ) + } + else + { + # Remove trust. + $trustSource.DeleteTrustRelationship($trustTarget) + + Write-Verbose -Message ( + $script:localizedData.RemovedTrust -f @( + $SourceDomainName, + $TargetDomainName, + $TrustType, + $TrustDirection + ) + ) + } + } + else + { + if ($Ensure -eq 'Present') + { + $trustRecreated = $false + + # Check properties. + $trustTypeProperty = $propertiesNotInDesiredState.Where( { $_.ParameterName -eq 'TrustType' }) + + if ($trustTypeProperty) + { + Write-Verbose -Message ( + $script:localizedData.NeedToRecreateTrust -f @( + $SourceDomainName, + $TargetDomainName, + (ConvertFrom-DirectoryContextType -DirectoryContextType $trustTypeProperty.Actual), + $TrustType + ) + ) + + if ($AllowTrustRecreation) + { + $trustSource.DeleteTrustRelationship($trustTarget) + $trustSource.CreateTrustRelationship($trustTarget, $TrustDirection) + + Write-Verbose -Message ( + $script:localizedData.RecreatedTrustType -f @( + $SourceDomainName, + $TargetDomainName, + $TrustType, + $TrustDirection + ) + ) + + $trustRecreated = $true + } + else + { + throw $script:localizedData.NotOptInToRecreateTrust + } + } + + <# + In case the trust direction property should be wrong, there + is no need to update that property twice since it was set + to the correct value when the trust was recreated. + #> + if (-not $trustRecreated) + { + if ($propertiesNotInDesiredState.Where( { $_.ParameterName -eq 'TrustDirection' })) + { + $trustSource.UpdateTrustRelationship($trustTarget, $TrustDirection) + + Write-Verbose -Message ( + $script:localizedData.SetTrustDirection -f $TrustDirection + ) + } + } + + Write-Verbose -Message $script:localizedData.InDesiredState + } + else + { + # The trust is already absent, so in desired state. + Write-Verbose -Message $script:localizedData.InDesiredState + } + } +} + +<# + .SYNOPSIS + Determines if the properties of the Active Directory trust is in + the desired state. + + .PARAMETER SourceDomainName + Specifies the name of the Active Directory domain that is requesting the + trust. + + .PARAMETER TargetDomainName + Specifies the name of the Active Directory domain that is being trusted. + + .PARAMETER TargetCredential + Specifies the credentials to authenticate to the target domain. + + .PARAMETER TrustType + Specifies the type of trust. The value 'External' means the context Domain, + while the value 'Forest' means the context 'Forest'. + + .PARAMETER TrustDirection + Specifies the direction of the trust. + + .PARAMETER Ensure + Specifies whether the computer account is present or absent. Default + value is 'Present'. + + .PARAMETER AllowTrustRecreation + Specifies if the is allowed to be recreated if required. Default value is + $false. +#> +function Test-TargetResource +{ + [CmdletBinding()] + [OutputType([System.Boolean])] + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $SourceDomainName, + + [Parameter(Mandatory = $true)] + [System.String] + $TargetDomainName, + + [Parameter(Mandatory = $true)] + [System.Management.Automation.PSCredential] + $TargetCredential, + + [Parameter(Mandatory = $true)] + [ValidateSet('External', 'Forest')] + [System.String] + $TrustType, + + [Parameter(Mandatory = $true)] + [ValidateSet('Bidirectional', 'Inbound', 'Outbound')] + [System.String] + $TrustDirection, + + [Parameter()] + [ValidateSet('Present', 'Absent')] + [System.String] + $Ensure = 'Present', + + [Parameter()] + [System.Boolean] + $AllowTrustRecreation = $false + ) + + Write-Verbose -Message ( + $script:localizedData.TestConfiguration -f $SourceDomainName, $TargetDomainName, $TrustType + ) + + # Only pass those properties that should be evaluated. + $compareTargetResourceStateParameters = @{} + $PSBoundParameters + $compareTargetResourceStateParameters.Remove('AllowTrustRecreation') + + <# + This returns array of hashtables which contain the properties ParameterName, + Expected, Actual, and InDesiredState. + #> + $compareTargetResourceStateResult = Compare-TargetResourceState @compareTargetResourceStateParameters + + if ($false -in $compareTargetResourceStateResult.InDesiredState) + { + $testTargetResourceReturnValue = $false + + Write-Verbose -Message $script:localizedData.NotInDesiredState + } + else + { + $testTargetResourceReturnValue = $true + + Write-Verbose -Message $script:localizedData.InDesiredState + } + + return $testTargetResourceReturnValue +} + +<# + .SYNOPSIS + Compares the properties in the current state with the properties of the + desired state and returns a hashtable with the comparison result. + + .PARAMETER SourceDomainName + Specifies the name of the Active Directory domain that is requesting the + trust. + + .PARAMETER TargetDomainName + Specifies the name of the Active Directory domain that is being trusted. + + .PARAMETER TargetCredential + Specifies the credentials to authenticate to the target domain. + + .PARAMETER TrustType + Specifies the type of trust. The value 'External' means the context Domain, + while the value 'Forest' means the context 'Forest'. + + .PARAMETER TrustDirection + Specifies the direction of the trust. + + .PARAMETER Ensure + Specifies whether the computer account is present or absent. Default + value is 'Present'. +#> +function Compare-TargetResourceState +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $SourceDomainName, + + [Parameter(Mandatory = $true)] + [System.String] + $TargetDomainName, + + [Parameter(Mandatory = $true)] + [System.Management.Automation.PSCredential] + $TargetCredential, + + [Parameter(Mandatory = $true)] + [ValidateSet('External', 'Forest')] + [System.String] + $TrustType, + + [Parameter(Mandatory = $true)] + [ValidateSet('Bidirectional', 'Inbound', 'Outbound')] + [System.String] + $TrustDirection, + + [Parameter()] + [ValidateSet('Present', 'Absent')] + [System.String] + $Ensure = 'Present' + ) + + $getTargetResourceParameters = @{ + SourceDomainName = $SourceDomainName + TargetDomainName = $TargetDomainName + TargetCredential = $TargetCredential + TrustType = $TrustType + TrustDirection = $TrustDirection + } + + $getTargetResourceResult = Get-TargetResource @getTargetResourceParameters + + <# + If the desired state should be Absent, then there is no need to + compare properties other than 'Ensure'. If the other properties + would be compared, they would return a false negative during test. + #> + if ($Ensure -eq 'Present') + { + $propertiesToEvaluate = @( + 'Ensure' + 'TrustType' + 'TrustDirection' + ) + } + else + { + $propertiesToEvaluate = @( + 'Ensure' + ) + } + + <# + If the user did not specify Ensure property, then it is not part of + the $PSBoundParameters, but it still needs to be compared. + Copy the hashtable $PSBoundParameters and add 'Ensure' property to make + sure it is part of the DesiredValues. + #> + $desiredValues = @{} + $PSBoundParameters + $desiredValues['Ensure'] = $Ensure + + $compareResourcePropertyStateParameters = @{ + CurrentValues = $getTargetResourceResult + DesiredValues = $desiredValues + Properties = $propertiesToEvaluate + } + + return Compare-ResourcePropertyState @compareResourcePropertyStateParameters +} + +<# + .SYNOPSIS + This returns a new object of the type System.DirectoryServices.ActiveDirectory.Domain + which is a class that represents an Active Directory Domain Services domain. + + .PARAMETER DirectoryContext + The Active Directory context from which the domain object is returned. + Calling the Get-ADDirectoryContext gets a value that can be provided in + this parameter. + + .NOTES + This is a wrapper to enable unit testing of this resource. + see issue https://github.com/PowerShell/ActiveDirectoryDsc/issues/324 + for more information. +#> +function Get-ActiveDirectoryDomain +{ + [CmdletBinding()] + [OutputType([System.DirectoryServices.ActiveDirectory.Domain])] + param + ( + [Parameter(Mandatory = $true)] + [System.DirectoryServices.ActiveDirectory.DirectoryContext] + $DirectoryContext + ) + + return [System.DirectoryServices.ActiveDirectory.Domain]::GetDomain($DirectoryContext) +} + +<# + .SYNOPSIS + This returns a new object of the type System.DirectoryServices.ActiveDirectory.Forest + which is a class that represents an Active Directory Domain Services forest. + + .PARAMETER DirectoryContext + The Active Directory context from which the forest object is returned. + Calling the Get-ADDirectoryContext gets a value that can be provided in + this parameter. + + .NOTES + This is a wrapper to enable unit testing of this resource. + see issue https://github.com/PowerShell/ActiveDirectoryDsc/issues/324 + for more information. +#> +function Get-ActiveDirectoryForest +{ + [CmdletBinding()] + [OutputType([System.DirectoryServices.ActiveDirectory.Forest])] + param + ( + [Parameter(Mandatory = $true)] + [System.DirectoryServices.ActiveDirectory.DirectoryContext] + $DirectoryContext + ) + + return [System.DirectoryServices.ActiveDirectory.Forest]::GetForest($DirectoryContext) +} + +<# + .SYNOPSIS + This returns the converted value from a Trust Type value to the correct + Directory Context Type value. + + .PARAMETER TrustType + The trust type value to convert. +#> +function ConvertTo-DirectoryContextType +{ + [CmdletBinding()] + [OutputType([System.String])] + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $TrustType + ) + + switch ($TrustType) + { + 'External' + { + $directoryContextType = 'Domain' + } + + 'Forest' + { + $directoryContextType = 'Forest' + } + } + + return $directoryContextType +} + +<# + .SYNOPSIS + This returns the converted value from a Directory Context Type value to + the correct Trust Type value. + + .PARAMETER DirectoryContextType + The Directory Context Type value to convert. +#> +function ConvertFrom-DirectoryContextType +{ + [CmdletBinding()] + [OutputType([System.String])] + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $DirectoryContextType + ) + + switch ($DirectoryContextType) + { + 'Domain' + { + $trustType = 'External' + } + + 'Forest' + { + $trustType = 'Forest' + } + } + + return $trustType +} + +<# + .SYNOPSIS + Returns two objects where the first object is for the source domain and + the second object is for the target domain. + + .PARAMETER SourceDomainName + Specifies the name of the Active Directory domain that is requesting the + trust. + + .PARAMETER TargetDomainName + Specifies the name of the Active Directory domain that is being trusted. + + .PARAMETER TargetCredential + Specifies the credentials to authenticate to the target domain. + + .PARAMETER TrustType + Specifies the type of trust. The value 'External' means the context Domain, + while the value 'Forest' means the context 'Forest'. + + .OUTPUTS + For both objects the type returned is either of the type + System.DirectoryServices.ActiveDirectory.Domain or of the type + System.DirectoryServices.ActiveDirectory.Forest. +#> +function Get-TrustSourceAndTargetObject +{ + [CmdletBinding()] + [OutputType([System.Object[]])] + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $SourceDomainName, + + [Parameter(Mandatory = $true)] + [System.String] + $TargetDomainName, + + [Parameter(Mandatory = $true)] + [System.Management.Automation.PSCredential] + $TargetCredential, + + [Parameter(Mandatory = $true)] + [ValidateSet('External', 'Forest')] + [System.String] + $TrustType + ) + + $directoryContextType = ConvertTo-DirectoryContextType -TrustType $TrustType + + # Create the target object. + $getADDirectoryContextParameters = @{ + DirectoryContextType = $directoryContextType + Name = $TargetDomainName + Credential = $TargetCredential + } + + $targetDirectoryContext = Get-ADDirectoryContext @getADDirectoryContextParameters + + # Create the source object. + $getADDirectoryContextParameters = @{ + DirectoryContextType = $directoryContextType + Name = $SourceDomainName + } + + $sourceDirectoryContext = Get-ADDirectoryContext @getADDirectoryContextParameters + + if ($directoryContextType -eq 'Domain') + { + $trustSource = Get-ActiveDirectoryDomain -DirectoryContext $sourceDirectoryContext + $trustTarget = Get-ActiveDirectoryDomain -DirectoryContext $targetDirectoryContext + } + else + { + $trustSource = Get-ActiveDirectoryForest -DirectoryContext $sourceDirectoryContext + $trustTarget = Get-ActiveDirectoryForest -DirectoryContext $targetDirectoryContext + } + + return $trustSource, $trustTarget +} + +Export-ModuleMember -Function *-TargetResource diff --git a/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_ADDomainTrust/MSFT_ADDomainTrust.schema.mof b/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_ADDomainTrust/MSFT_ADDomainTrust.schema.mof new file mode 100644 index 0000000..1043d91 --- /dev/null +++ b/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_ADDomainTrust/MSFT_ADDomainTrust.schema.mof @@ -0,0 +1,11 @@ +[ClassVersion("1.0.1.0"), FriendlyName("ADDomainTrust")] +class MSFT_ADDomainTrust : OMI_BaseResource +{ + [Write, Description("Specifies whether the computer account is present or absent. Default value is 'Present'."), ValueMap{"Present","Absent"}, Values{"Present","Absent"}] String Ensure; + [Required, Description("Specifies the credentials to authenticate to the target domain."), EmbeddedInstance("MSFT_Credential")] String TargetCredential; + [Key, Description("Specifies the name of the Active Directory domain that is being trusted.")] String TargetDomainName; + [Required, Description("Specifies the type of trust. The value 'External' means the context Domain, while the value 'Forest' means the context 'Forest'."), ValueMap{"External","Forest"}, Values{"External","Forest"}] String TrustType; + [Required, Description("Specifies the direction of the trust."), ValueMap{"Bidirectional","Inbound","Outbound"}, Values{"Bidirectional","Inbound","Outbound"}] String TrustDirection; + [Key, Description("Specifies the name of the Active Directory domain that is requesting the trust.")] String SourceDomainName; + [Write, Description("Specifies if the is allowed to be recreated if required. Default value is $false.")] Boolean AllowTrustRecreation; +}; diff --git a/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_ADDomainTrust/README.md b/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_ADDomainTrust/README.md new file mode 100644 index 0000000..f874320 --- /dev/null +++ b/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_ADDomainTrust/README.md @@ -0,0 +1,7 @@ +# Description + +The ADDomainTrust DSC resource will manage Domain Trusts within Active Directory. A trust is a relationship, which you establish between domains or forests. To understand more about trusts in Active Directory, please see the article [Forest Design Models](https://docs.microsoft.com/en-us/windows-server/identity/ad-ds/plan/forest-design-models) for more information. + +## Requirements + +* Target machine must be running Windows Server 2008 R2 or later. diff --git a/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_ADDomainTrust/en-US/MSFT_ADDomainTrust.strings.psd1 b/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_ADDomainTrust/en-US/MSFT_ADDomainTrust.strings.psd1 new file mode 100644 index 0000000..291635f --- /dev/null +++ b/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_ADDomainTrust/en-US/MSFT_ADDomainTrust.strings.psd1 @@ -0,0 +1,15 @@ +# culture="en-US" +ConvertFrom-StringData @' +CheckingTrustMessage = Determining if the trust between domains '{0}' and '{1}' with the context type '{2}' exists. (ADDT0001) +RemovedTrust = Trust between between domains '{0}' and '{1}' with the context type '{2}' has been removed. (ADDT0002) +AddedTrust = Created the trust between domains '{0}' and '{1}' with the context type '{2}' and direction '{3}'. (ADDT0003) +SetTrustDirection = The trust direction has been changed to '{0}'. (ADDT0004) +TrustPresentMessage = The trust between domains '{0}' and '{1}' with the context type '{2}' exist. (ADDT0005) +TrustAbsentMessage = There is no trust between domains '{0}' and '{1}' with the context type '{2}'. (ADDT0006) +TestConfiguration = Determining the current state of the Active Directory trust with source domain '{0}', target domain '{1}' and context type '{2}'. (ADDT0007) +InDesiredState = The Active Directory trust is in the desired state. (ADDT0008) +NotInDesiredState = The Active Directory trust is not in the desired state. (ADDT0009) +NeedToRecreateTrust = The trust type is not in desired state, removing the trust between the domains '{0}' and '{1}' with the context type '{2}' to be able to recreate the trust with the correct context type '{3}'. (ADDT0010) +RecreatedTrustType = Recreated the trust between domains '{0}' and '{1}' with the context type '{2}' and direction '{3}'. (ADDT0011) +NotOptInToRecreateTrust = Not opt-in to recreate trust. To opt-in set the parameter AllowTrustRecreation to $true. +'@ diff --git a/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_ADDomainTrust/en-US/about_ADDomainTrust.help.txt b/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_ADDomainTrust/en-US/about_ADDomainTrust.help.txt new file mode 100644 index 0000000..d6d1870 --- /dev/null +++ b/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_ADDomainTrust/en-US/about_ADDomainTrust.help.txt @@ -0,0 +1,120 @@ +.NAME + ADDomainTrust + +.DESCRIPTION + The ADDomainTrust DSC resource will manage Domain Trusts within Active Directory. A trust is a relationship, which you establish between domains or forests. To understand more about trusts in Active Directory, please see the article [Forest Design Models](https://docs.microsoft.com/en-us/windows-server/identity/ad-ds/plan/forest-design-models) for more information. + + ## Requirements + + * Target machine must be running Windows Server 2008 R2 or later. + +.PARAMETER Ensure + Write - String + Allowed values: Present, Absent + Specifies whether the computer account is present or absent. Default value is 'Present'. + +.PARAMETER TargetCredential + Required - PSCredential + Specifies the credentials to authenticate to the target domain. + +.PARAMETER TargetDomainName + Key - String + Specifies the name of the Active Directory domain that is being trusted. + +.PARAMETER TrustType + Required - String + Allowed values: External, Forest + Specifies the type of trust. The value 'External' means the context Domain, while the value 'Forest' means the context 'Forest'. + +.PARAMETER TrustDirection + Required - String + Allowed values: Bidirectional, Inbound, Outbound + Specifies the direction of the trust. + +.PARAMETER SourceDomainName + Key - String + Specifies the name of the Active Directory domain that is requesting the trust. + +.PARAMETER AllowTrustRecreation + Write - Boolean + Specifies if the is allowed to be recreated if required. Default value is $false. + +.EXAMPLE 1 + +This configuration will create a new one way inbound trust between two +domains. + +Configuration ADDomainTrust_ExternalInboundTrust_Config +{ + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $SourceDomain, + + [Parameter(Mandatory = $true)] + [System.String] + $TargetDomain, + + [Parameter(Mandatory = $true)] + [System.Management.Automation.PSCredential] + $TargetDomainAdminCred + ) + + Import-DscResource -module ActiveDirectoryDsc + + node localhost + { + ADDomainTrust 'Trust' + { + Ensure = 'Present' + SourceDomainName = $SourceDomain + TargetDomainName = $TargetDomain + TargetCredential = $TargetDomainAdminCred + TrustDirection = 'Inbound' + TrustType = 'External' + } + } +} + +.EXAMPLE 2 + +This configuration will create a new one way inbound trust between two +domains, and allows the trust to recreated if it should have the wrong +trust type. + +Configuration ADDomainTrust_ExternalInboundTrustWithOptInToRecreate_Config +{ + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $SourceDomain, + + [Parameter(Mandatory = $true)] + [System.String] + $TargetDomain, + + [Parameter(Mandatory = $true)] + [System.Management.Automation.PSCredential] + $TargetDomainAdminCred + ) + + Import-DscResource -module ActiveDirectoryDsc + + node localhost + { + ADDomainTrust 'Trust' + { + Ensure = 'Present' + SourceDomainName = $SourceDomain + TargetDomainName = $TargetDomain + TargetCredential = $TargetDomainAdminCred + TrustDirection = 'Inbound' + TrustType = 'External' + AllowTrustRecreation = $true + } + } +} + + diff --git a/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_ADForestFunctionalLevel/MSFT_ADForestFunctionalLevel.psm1 b/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_ADForestFunctionalLevel/MSFT_ADForestFunctionalLevel.psm1 new file mode 100644 index 0000000..40ce7ac --- /dev/null +++ b/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_ADForestFunctionalLevel/MSFT_ADForestFunctionalLevel.psm1 @@ -0,0 +1,193 @@ +$script:resourceModulePath = Split-Path -Path (Split-Path -Path $PSScriptRoot -Parent) -Parent +$script:modulesFolderPath = Join-Path -Path $script:resourceModulePath -ChildPath 'Modules' + +$script:localizationModulePath = Join-Path -Path $script:modulesFolderPath -ChildPath 'ActiveDirectoryDsc.Common' +Import-Module -Name (Join-Path -Path $script:localizationModulePath -ChildPath 'ActiveDirectoryDsc.Common.psm1') + +$script:localizedData = Get-LocalizedData -ResourceName 'MSFT_ADForestFunctionalLevel' + +<# + .SYNOPSIS + Returns the current functional level of the forest. + + .PARAMETER ForestIdentity + Specifies the Active Directory forest to modify. You can identify a + forest by its fully qualified domain name (FQDN), GUID, DNS host name, + or NetBIOS name. + + .PARAMETER ForestMode + Specifies the the functional level for the Active Directory forest. + + Not used in Get-TargetResource. +#> +function Get-TargetResource +{ + [CmdletBinding()] + [OutputType([System.Collections.Hashtable])] + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $ForestIdentity, + + [Parameter(Mandatory = $true)] + [ValidateSet('Windows2008R2Forest', 'Windows2012Forest', 'Windows2012R2Forest', 'Windows2016Forest')] + [System.String] + $ForestMode + ) + + Write-Verbose -Message ( + $script:localizedData.RetrievingForestMode -f $ForestIdentity + ) + + $getTargetResourceReturnValue = @{ + ForestIdentity = $ForestIdentity + ForestMode = $null + } + + $forestObject = Get-ADForest -Identity $ForestIdentity -ErrorAction 'Stop' + $getTargetResourceReturnValue['ForestMode'] = $forestObject.ForestMode + + return $getTargetResourceReturnValue +} + +<# + .SYNOPSIS + Determines if the functional level is in the desired state. + + .PARAMETER ForestIdentity + Specifies the Active Directory forest to modify. You can identify a + forest by its fully qualified domain name (FQDN), GUID, DNS host name, + or NetBIOS name. + + .PARAMETER ForestMode + Specifies the the functional level for the Active Directory forest. +#> +function Test-TargetResource +{ + [CmdletBinding()] + [OutputType([System.Boolean])] + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $ForestIdentity, + + [Parameter(Mandatory = $true)] + [ValidateSet('Windows2008R2Forest', 'Windows2012Forest', 'Windows2012R2Forest', 'Windows2016Forest')] + [System.String] + $ForestMode + ) + + Write-Verbose -Message ( + $script:localizedData.TestConfiguration -f $ForestIdentity + ) + + $compareTargetResourceStateResult = Compare-TargetResourceState @PSBoundParameters + + if ($false -in $compareTargetResourceStateResult.InDesiredState) + { + Write-Verbose -Message $script:localizedData.LevelNotInDesiredState + + $testTargetResourceReturnValue = $false + } + else + { + Write-Verbose -Message $script:localizedData.LevelInDesiredState + + $testTargetResourceReturnValue = $true + } + + return $testTargetResourceReturnValue +} + +<# + .SYNOPSIS + Sets the functional level on the Active Directory forest. + + .PARAMETER ForestIdentity + Specifies the Active Directory forest to modify. You can identify a + forest by its fully qualified domain name (FQDN), GUID, DNS host name, + or NetBIOS name. + + .PARAMETER ForestMode + Specifies the the functional level for the Active Directory forest. +#> +function Set-TargetResource +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $ForestIdentity, + + [Parameter(Mandatory = $true)] + [ValidateSet('Windows2008R2Forest', 'Windows2012Forest', 'Windows2012R2Forest', 'Windows2016Forest')] + [System.String] + $ForestMode + ) + + $compareTargetResourceStateResult = Compare-TargetResourceState @PSBoundParameters + + # Get all properties that are not in desired state. + $propertiesNotInDesiredState = $compareTargetResourceStateResult | Where-Object -FilterScript { + -not $_.InDesiredState + } + + $forestModeProperty = $propertiesNotInDesiredState.Where( { $_.ParameterName -eq 'ForestMode' }) + + if ($forestModeProperty) + { + Write-Verbose -Message ( + $script:localizedData.ForestModeUpdating -f $forestModeProperty.Actual, $ForestMode + ) + + $setADForestModeParameters = @{ + Identity = $ForestIdentity + ForestMode = [Microsoft.ActiveDirectory.Management.ADForestMode]::$ForestMode + Confirm = $false + } + + Set-ADForestMode @setADForestModeParameters + } +} + +<# + .SYNOPSIS + Compares the properties in the current state with the properties of the + desired state and returns a hashtable with the comparison result. + + .PARAMETER ForestIdentity + Specifies the Active Directory forest to modify. You can identify a + forest by its fully qualified domain name (FQDN), GUID, DNS host name, + or NetBIOS name. + + .PARAMETER ForestMode + Specifies the the functional level for the Active Directory forest. +#> +function Compare-TargetResourceState +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $ForestIdentity, + + [Parameter(Mandatory = $true)] + [ValidateSet('Windows2008R2Forest', 'Windows2012Forest', 'Windows2012R2Forest', 'Windows2016Forest')] + [System.String] + $ForestMode + ) + + $getTargetResourceResult = Get-TargetResource @PSBoundParameters + + $compareTargetResourceStateParameters = @{ + CurrentValues = $getTargetResourceResult + DesiredValues = $PSBoundParameters + Properties = @('ForestMode') + } + + return Compare-ResourcePropertyState @compareTargetResourceStateParameters +} diff --git a/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_ADForestFunctionalLevel/MSFT_ADForestFunctionalLevel.schema.mof b/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_ADForestFunctionalLevel/MSFT_ADForestFunctionalLevel.schema.mof new file mode 100644 index 0000000..8ac2fd0 --- /dev/null +++ b/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_ADForestFunctionalLevel/MSFT_ADForestFunctionalLevel.schema.mof @@ -0,0 +1,6 @@ +[ClassVersion("1.0.0.0"), FriendlyName("ADForestFunctionalLevel")] +class MSFT_ADForestFunctionalLevel : OMI_BaseResource +{ + [Key, Description("Specifies the Active Directory forest to modify. You can identify a forest by its fully qualified domain name (FQDN), GUID, DNS host name, or NetBIOS name.")] String ForestIdentity; + [Required, Description("Specifies the the functional level for the Active Directory forest."), ValueMap{"Windows2008R2Forest", "Windows2012Forest", "Windows2012R2Forest", "Windows2016Forest"}, Values{"Windows2008R2Forest", "Windows2012Forest", "Windows2012R2Forest", "Windows2016Forest"}] String ForestMode; +}; diff --git a/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_ADForestFunctionalLevel/README.md b/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_ADForestFunctionalLevel/README.md new file mode 100644 index 0000000..21d32ff --- /dev/null +++ b/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_ADForestFunctionalLevel/README.md @@ -0,0 +1,15 @@ +# Description + +This resource changes the forest functional level. For further details, see [Forest and Domain Functional Levels](https://docs.microsoft.com/en-us/windows-server/identity/ad-ds/active-directory-functional-levels). + +**WARNING: This action might be irreversible!** Make sure you understand +the consequences of changing the forest functional level. + +Read more about raising function levels and potential roll back +scenarios in the Active Directory documentation, for example: [Upgrade Domain Controllers to Windows Server 2016](https://docs.microsoft.com/en-us/windows-server/identity/ad-ds/deploy/upgrade-domain-controllers). + +## Requirements + +* Target machine must be running Windows Server 2008 R2 or later. +* Target machine must be running the minimum required operating system + version for the forest functional level to set. diff --git a/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_ADForestFunctionalLevel/en-US/MSFT_ADForestFunctionalLevel.strings.psd1 b/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_ADForestFunctionalLevel/en-US/MSFT_ADForestFunctionalLevel.strings.psd1 new file mode 100644 index 0000000..1191387 --- /dev/null +++ b/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_ADForestFunctionalLevel/en-US/MSFT_ADForestFunctionalLevel.strings.psd1 @@ -0,0 +1,8 @@ +# culture="en-US" +ConvertFrom-StringData @' + RetrievingForestMode = Retrieving the forest functional level for the forest '{0}'. (ADFFL0001) + TestConfiguration = Determining the current forest functional level in the forest '{0}'. (ADFFL0002) + LevelInDesiredState = The forest functional level is in the desired state. (ADFFL0003) + LevelNotInDesiredState = The forest functional level is not in the desired state. (ADFFL0004) + ForestModeUpdating = The forest functional level will change from '{0}' to '{1}'. (ADFFL0005) +'@ diff --git a/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_ADForestFunctionalLevel/en-US/about_ADForestFunctionalLevel.help.txt b/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_ADForestFunctionalLevel/en-US/about_ADForestFunctionalLevel.help.txt new file mode 100644 index 0000000..2068176 --- /dev/null +++ b/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_ADForestFunctionalLevel/en-US/about_ADForestFunctionalLevel.help.txt @@ -0,0 +1,47 @@ +.NAME + ADForestFunctionalLevel + +.DESCRIPTION + This resource changes the forest functional level. For further details, see [Forest and Domain Functional Levels](https://docs.microsoft.com/en-us/windows-server/identity/ad-ds/active-directory-functional-levels). + + **WARNING: This action might be irreversible!** Make sure you understand + the consequences of changing the forest functional level. + + Read more about raising function levels and potential roll back + scenarios in the Active Directory documentation, for example: [Upgrade Domain Controllers to Windows Server 2016](https://docs.microsoft.com/en-us/windows-server/identity/ad-ds/deploy/upgrade-domain-controllers). + + ## Requirements + + * Target machine must be running Windows Server 2008 R2 or later. + * Target machine must be running the minimum required operating system + version for the forest functional level to set. + +.PARAMETER ForestIdentity + Key - String + Specifies the Active Directory forest to modify. You can identify a forest by its fully qualified domain name (FQDN), GUID, DNS host name, or NetBIOS name. + +.PARAMETER ForestMode + Required - String + Allowed values: Windows2008R2Forest, Windows2012Forest, Windows2012R2Forest, Windows2016Forest + Specifies the the functional level for the Active Directory forest. + +.EXAMPLE 1 + +This configuration will change the forest functional level to +a Windows Server 2012 R2 Forest. + +Configuration ADForestFunctionalLevel_SetLevel_Config +{ + Import-DscResource -ModuleName ActiveDirectoryDsc + + node localhost + { + ADForestFunctionalLevel 'ChangeForestFunctionalLevel' + { + ForestIdentity = 'contoso.com' + ForestMode = 'Windows2012R2Forest' + } + } +} + + diff --git a/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_ADForestProperties/MSFT_ADForestProperties.psm1 b/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_ADForestProperties/MSFT_ADForestProperties.psm1 new file mode 100644 index 0000000..879d800 --- /dev/null +++ b/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_ADForestProperties/MSFT_ADForestProperties.psm1 @@ -0,0 +1,509 @@ +$script:resourceModulePath = Split-Path -Path (Split-Path -Path $PSScriptRoot -Parent) -Parent +$script:modulesFolderPath = Join-Path -Path $script:resourceModulePath -ChildPath 'Modules' + +$script:localizationModulePath = Join-Path -Path $script:modulesFolderPath -ChildPath 'ActiveDirectoryDsc.Common' +Import-Module -Name (Join-Path -Path $script:localizationModulePath -ChildPath 'ActiveDirectoryDsc.Common.psm1') + +$script:localizedData = Get-LocalizedData -ResourceName 'MSFT_ADForestProperties' +$script:psModuleName = 'ActiveDirectory' + +<# + .SYNOPSIS + Gets the current state of user principal name and service principal name suffixes in the forest. + + .PARAMETER Credential + The user account credentials to use to perform this task. + + .PARAMETER ForestName + The target Active Directory forest for the change. + + .PARAMETER ServicePrincipalNameSuffixToAdd + The Service Principal Name Suffix(es) to add in the forest. Cannot be used with ServicePrincipalNameSuffix. + + .PARAMETER ServicePrincipalNameSuffixToRemove + The Service Principal Name Suffix(es) to remove in the forest. Cannot be used with ServicePrincipalNameSuffix. + + .PARAMETER UserPrincipalNameSuffixToAdd + The User Principal Name Suffix(es) to add in the forest. Cannot be used with UserPrincipalNameSuffix. + + .PARAMETER UserPrincipalNameSuffixToRemove + The User Principal Name Suffix(es) to remove in the forest. Cannot be used with UserPrincipalNameSuffix. + + .NOTES + Used Functions: + Name | Module + ------------------------------|-------------------------- + Assert-Module | ActiveDirectoryDsc.Common + Get-ADForest | ActiveDirectory + Get-ADObject | ActiveDirectory + Get-ADRootDSE | ActiveDirectory +#> +function Get-TargetResource +{ + [CmdletBinding()] + [OutputType([System.Collections.Hashtable])] + param + ( + [Parameter()] + [System.Management.Automation.PSCredential] + $Credential, + + [Parameter(Mandatory = $true)] + [System.String] + $ForestName, + + [Parameter()] + [System.String[]] + $ServicePrincipalNameSuffix, + + [Parameter()] + [System.String[]] + $ServicePrincipalNameSuffixToAdd, + + [Parameter()] + [System.String[]] + $ServicePrincipalNameSuffixToRemove, + + [Parameter()] + [System.String[]] + $UserPrincipalNameSuffix, + + [Parameter()] + [System.String[]] + $UserPrincipalNameSuffixToAdd, + + [Parameter()] + [System.String[]] + $UserPrincipalNameSuffixToRemove + ) + + Assert-Module -ModuleName $script:psModuleName + + Write-Verbose -Message ($script:localizedData.GetForest -f $ForestName) + $forest = Get-ADForest -Identity $ForestName + + $configurationNamingContext = (Get-ADRootDSE).configurationNamingContext + $identity = "CN=Directory Service,CN=Windows NT,CN=Services,$configurationNamingContext" + $tombstoneLifetime = (Get-ADObject -Identity $identity -Partition $configurationNamingContext ` + -Properties 'tombstonelifetime').tombstonelifetime + + if ($PSBoundParameters.ContainsKey('Credential')) + { + $cimCredential = New-CimCredentialInstance -Credential $Credential + } + else + { + $cimCredential = $null + } + + return @{ + Credential = $cimCredential + ForestName = $forest.Name + ServicePrincipalNameSuffix = [System.Array] $forest.SpnSuffixes + ServicePrincipalNameSuffixToAdd = [System.Array] $ServicePrincipalNameSuffixToAdd + ServicePrincipalNameSuffixToRemove = [System.Array] $ServicePrincipalNameSuffixToRemove + TombstoneLifetime = $tombstoneLifetime + UserPrincipalNameSuffix = [System.Array] $forest.UpnSuffixes + UserPrincipalNameSuffixToAdd = [System.Array] $UserPrincipalNameSuffixToAdd + UserPrincipalNameSuffixToRemove = [System.Array] $UserPrincipalNameSuffixToRemove + } +} + +<# + .SYNOPSIS + Tests the current state of user principal name and service principal name suffixes in the forest. + + .PARAMETER Credential + The user account credentials to use to perform this task. + + .PARAMETER ForestName + The target Active Directory forest for the change. + + .PARAMETER ServicePrincipalNameSuffix + The Service Principal Name Suffix(es) to be explicitly defined in the forest and replace existing + members. Cannot be used with ServicePrincipalNameSuffixToAdd or ServicePrincipalNameSuffixToRemove. + + .PARAMETER ServicePrincipalNameSuffixToAdd + The Service Principal Name Suffix(es) to add in the forest. Cannot be used with ServicePrincipalNameSuffix. + + .PARAMETER ServicePrincipalNameSuffixToRemove + The Service Principal Name Suffix(es) to remove in the forest. Cannot be used with ServicePrincipalNameSuffix. + + .PARAMETER TombstoneLifetime + Specifies the AD Tombstone lifetime which determines how long deleted items exist in Active Directory before + they are purged. + + .PARAMETER UserPrincipalNameSuffix + The User Principal Name Suffix(es) to be explicitly defined in the forest and replace existing + members. Cannot be used with UserPrincipalNameSuffixToAdd or UserPrincipalNameSuffixToRemove. + + .PARAMETER UserPrincipalNameSuffixToAdd + The User Principal Name Suffix(es) to add in the forest. Cannot be used with UserPrincipalNameSuffix. + + .PARAMETER UserPrincipalNameSuffixToRemove + The User Principal Name Suffix(es) to remove in the forest. Cannot be used with UserPrincipalNameSuffix. + + .NOTES + Used Functions: + Name | Module + ------------------------------|-------------------------- + Assert-MemberParameters | ActiveDirectoryDsc.Common + Assert-Module | ActiveDirectoryDsc.Common + Test-Members | ActiveDirectoryDsc.Common +#> +function Test-TargetResource +{ + [CmdletBinding()] + [OutputType([System.Boolean])] + param + ( + [Parameter()] + [System.Management.Automation.PSCredential] + $Credential, + + [Parameter(Mandatory = $true)] + [System.String] + $ForestName, + + [Parameter()] + [System.String[]] + $ServicePrincipalNameSuffix, + + [Parameter()] + [System.String[]] + $ServicePrincipalNameSuffixToAdd, + + [Parameter()] + [System.String[]] + $ServicePrincipalNameSuffixToRemove, + + [Parameter()] + [System.Int32] + $TombstoneLifetime, + + [Parameter()] + [System.String[]] + $UserPrincipalNameSuffix, + + [Parameter()] + [System.String[]] + $UserPrincipalNameSuffixToAdd, + + [Parameter()] + [System.String[]] + $UserPrincipalNameSuffixToRemove + ) + + Assert-Module -ModuleName $script:psModuleName + + $inDesiredState = $true + + $targetResource = Get-TargetResource -ForestName $ForestName + + # Validate parameters before we even attempt to retrieve anything + $assertMemberParameters = @{} + + if ($PSBoundParameters.ContainsKey('ServicePrincipalNameSuffix') -and + -not [system.string]::IsNullOrEmpty($ServicePrincipalNameSuffix)) + { + $assertMemberParameters['Members'] = $ServicePrincipalNameSuffix + } + + if ($PSBoundParameters.ContainsKey('ServicePrincipalNameSuffixToAdd') -and + -not [system.string]::IsNullOrEmpty($ServicePrincipalNameSuffixToAdd)) + { + $assertMemberParameters['MembersToInclude'] = $ServicePrincipalNameSuffixToAdd + } + + if ($PSBoundParameters.ContainsKey('ServicePrincipalNameSuffixToRemove') -and + -not [system.string]::IsNullOrEmpty($ServicePrincipalNameSuffixToRemove)) + { + $assertMemberParameters['MembersToExclude'] = $ServicePrincipalNameSuffixToRemove + } + + Assert-MemberParameters @assertMemberParameters -ErrorAction Stop + + if (-not ( Test-Members @assertMemberParameters -ExistingMembers ($targetResource.ServicePrincipalNameSuffix -split ',') )) + { + Write-Verbose -Message ($script:localizedData.ForestSpnSuffixNotInDesiredState -f $ForestName) + $inDesiredState = $false + } + + $assertMemberParameters = @{} + + if ($PSBoundParameters.ContainsKey('UserPrincipalNameSuffix') -and + -not [system.string]::IsNullOrEmpty($UserPrincipalNameSuffix)) + { + $assertMemberParameters['Members'] = $UserPrincipalNameSuffix + } + + if ($PSBoundParameters.ContainsKey('UserPrincipalNameSuffixToAdd') -and + -not [system.string]::IsNullOrEmpty($UserPrincipalNameSuffixToAdd)) + { + $assertMemberParameters['MembersToInclude'] = $UserPrincipalNameSuffixToAdd + } + + if ($PSBoundParameters.ContainsKey('UserPrincipalNameSuffixToRemove') -and + -not [system.string]::IsNullOrEmpty($UserPrincipalNameSuffixToRemove)) + { + $assertMemberParameters['MembersToExclude'] = $UserPrincipalNameSuffixToRemove + } + + Assert-MemberParameters @assertMemberParameters -ErrorAction Stop + + if (-not ( Test-Members @assertMemberParameters -ExistingMembers ($targetResource.UserPrincipalNameSuffix -split ',') )) + { + Write-Verbose -Message ($script:localizedData.ForestUpnSuffixNotInDesiredState -f $ForestName) + + $inDesiredState = $false + } + + if ($PSBoundParameters.ContainsKey('TombstoneLifetime')) + { + if ($TombstoneLifetime -ne $targetResource.TombstoneLifetime) + { + Write-Verbose -Message ($script:localizedData.TombstoneLifetimeNotInDesiredState -f + $ForestName, $targetResource.TombstoneLifetime, $TombstoneLifetime) + + $inDesiredState = $false + } + } + + return $inDesiredState +} + +<# + .SYNOPSIS + Sets the user principal name and service principal name suffixes in the forest. + + .PARAMETER Credential + The user account credentials to use to perform this task. + + .PARAMETER ForestName + The target Active Directory forest for the change. + + .PARAMETER ServicePrincipalNameSuffix + The Service Principal Name Suffix(es) to be explicitly defined in the forest and replace existing + members. Cannot be used with ServicePrincipalNameSuffixToAdd or ServicePrincipalNameSuffixToRemove. + + .PARAMETER ServicePrincipalNameSuffixToAdd + The Service Principal Name Suffix(es) to add in the forest. Cannot be used with ServicePrincipalNameSuffix. + + .PARAMETER ServicePrincipalNameSuffixToRemove + The Service Principal Name Suffix(es) to remove in the forest. Cannot be used with ServicePrincipalNameSuffix. + + .PARAMETER TombstoneLifetime + Specifies the AD Tombstone lifetime which determines how long deleted items exist in Active Directory before + they are purged. + + .PARAMETER UserPrincipalNameSuffix + The User Principal Name Suffix(es) to be explicitly defined in the forest and replace existing + members. Cannot be used with UserPrincipalNameSuffixToAdd or UserPrincipalNameSuffixToRemove. + + .PARAMETER UserPrincipalNameSuffixToAdd + The User Principal Name Suffix(es) to add in the forest. Cannot be used with UserPrincipalNameSuffix. + + .PARAMETER UserPrincipalNameSuffixToRemove + The User Principal Name Suffix(es) to remove in the forest. Cannot be used with UserPrincipalNameSuffix. + + .NOTES + Used Functions: + Name | Module + ------------------------------|-------------------------- + Assert-Module | ActiveDirectoryDsc.Common + Get-ADRootDSE | ActiveDirectory + New-InvalidOperationException | ActiveDirectoryDsc.Common + Set-ADForest | ActiveDirectory + Set-ODObject | ActiveDirectory +#> +function Set-TargetResource +{ + [CmdletBinding()] + param + ( + [Parameter()] + [System.Management.Automation.PSCredential] + $Credential, + + [Parameter(Mandatory = $true)] + [System.String] + $ForestName, + + [Parameter()] + [System.String[]] + $ServicePrincipalNameSuffix, + + [Parameter()] + [System.String[]] + $ServicePrincipalNameSuffixToAdd, + + [Parameter()] + [System.String[]] + $ServicePrincipalNameSuffixToRemove, + + [Parameter()] + [System.Int32] + $TombstoneLifetime, + + [Parameter()] + [System.String[]] + $UserPrincipalNameSuffix, + + [Parameter()] + [System.String[]] + $UserPrincipalNameSuffixToAdd, + + [Parameter()] + [System.String[]] + $UserPrincipalNameSuffixToRemove + ) + + Assert-Module -ModuleName $script:psModuleName + + $targetResource = Get-TargetResource -ForestName $ForestName + + $setADForestParameters = @{} + + # add ServicePrincipalName parameter + if ($PSBoundParameters.ContainsKey('ServicePrincipalNameSuffix')) + { + if (-not [system.string]::IsNullOrEmpty($ServicePrincipalNameSuffix)) + { + $setADForestParameters['SpnSuffixes'] = @{ + Replace = $($ServicePrincipalNameSuffix) + } + + Write-Verbose -Message ($script:localizedData.ReplaceSpnSuffix -f + ($ServicePrincipalNameSuffix -join ', '), $ForestName) + } + else + { + $setADForestParameters['SpnSuffixes'] = $null + Write-Verbose -Message ($script:localizedData.ClearSpnSuffix -f $ForestName) + } + } + + if ($PSBoundParameters.ContainsKey('ServicePrincipalNameSuffixToAdd') -and + -not [system.string]::IsNullOrEmpty($ServicePrincipalNameSuffixToAdd)) + { + $setADForestParameters['SpnSuffixes'] = @{ + Add = $($ServicePrincipalNameSuffixToAdd) + } + + Write-Verbose -Message ($script:localizedData.AddSpnSuffix -f + ($ServicePrincipalNameSuffixToAdd -join ', '), $ForestName) + } + + if ($PSBoundParameters.ContainsKey('ServicePrincipalNameSuffixToRemove') -and + -not [system.string]::IsNullOrEmpty($ServicePrincipalNameSuffixToRemove)) + { + if ($setADForestParameters['SpnSuffixes']) + { + $setADForestParameters['SpnSuffixes']['Remove'] = $($ServicePrincipalNameSuffixToRemove) + } + else + { + $setADForestParameters['SpnSuffixes'] = @{ + Remove = $($ServicePrincipalNameSuffixToRemove) + } + } + + Write-Verbose -Message ($script:localizedData.RemoveSpnSuffix -f + ($ServicePrincipalNameSuffixToRemove -join ', '), $ForestName) + } + + # add UserPrincipalName parameter + if ($PSBoundParameters.ContainsKey('UserPrincipalNameSuffix')) + { + if (-not [system.string]::IsNullOrEmpty($UserPrincipalNameSuffix)) + { + $setADForestParameters['UpnSuffixes'] = @{ + Replace = $($UserPrincipalNameSuffix) + } + + Write-Verbose -Message ($script:localizedData.ReplaceUpnSuffix -f + ($UserPrincipalNameSuffix -join ', '), $ForestName) + } + else + { + $setADForestParameters['UpnSuffixes'] = $null + Write-Verbose -Message ($script:localizedData.ClearUpnSuffix -f $ForestName) + } + } + + if ($PSBoundParameters.ContainsKey('UserPrincipalNameSuffixToAdd') -and + -not [system.string]::IsNullOrEmpty($UserPrincipalNameSuffixToAdd)) + { + $setADForestParameters['UpnSuffixes'] = @{ + Add = $($UserPrincipalNameSuffixToAdd) + } + + Write-Verbose -Message ($script:localizedData.AddUpnSuffix -f + ($UserPrincipalNameSuffixToAdd -join ', '), $ForestName) + } + + if ($PSBoundParameters.ContainsKey('UserPrincipalNameSuffixToRemove') -and + -not [system.string]::IsNullOrEmpty($UserPrincipalNameSuffixToRemove)) + { + if ($setADForestParameters['UpnSuffixes']) + { + $setADForestParameters['UpnSuffixes']['Remove'] = $($UserPrincipalNameSuffixToRemove) + } + else + { + $setADForestParameters['UpnSuffixes'] = @{ + Remove = $($UserPrincipalNameSuffixToRemove) + } + } + + Write-Verbose -Message ($script:localizedData.RemoveUpnSuffix -f + ($UserPrincipalNameSuffixToRemove -join ', '), $ForestName) + } + + # Only run Set-ADForest if a value needs updating + if ($setADForestParameters.count -gt 0) + { + if ($PSBoundParameters.ContainsKey('Credential')) + { + $setADForestParameters['Credential'] = $Credential + } + + $setADForestParameters['Identity'] = $ForestName + + Set-ADForest @setADForestParameters + } + + if ($PSBoundParameters.ContainsKey('TombstoneLifetime') -and + $TombstoneLifetime -ne $targetResource.TombstoneLifetime) + { + Write-Verbose -Message ($script:localizedData.SetTombstoneLifetime -f + $TombstoneLifetime, $ForestName) + + $configurationNamingContext = (Get-ADRootDSE).configurationNamingContext + $identity = "CN=Directory Service,CN=Windows NT,CN=Services,$configurationNamingContext" + + $setADObjectParameters = @{ + Identity = $identity + Partition = $configurationNamingContext + Replace = @{ + tombstonelifetime = $TombstoneLifetime + } + } + + if ($PSBoundParameters.ContainsKey('Credential')) + { + $setADObjectParameters['Credential'] = $Credential + } + + try + { + Set-ADObject @setADObjectParameters + } + catch + { + $errorMessage = ($script:localizedData.SetTombstoneLifetimeError -f + $TombstoneLifetime, $ForestName) + New-InvalidOperationException -Message $errorMessage -ErrorRecord $_ + } + } +} diff --git a/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_ADForestProperties/MSFT_ADForestProperties.schema.mof b/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_ADForestProperties/MSFT_ADForestProperties.schema.mof new file mode 100644 index 0000000..649954e --- /dev/null +++ b/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_ADForestProperties/MSFT_ADForestProperties.schema.mof @@ -0,0 +1,13 @@ +[ClassVersion("1.0.0.0"), FriendlyName("ADForestProperties")] +class MSFT_ADForestProperties : OMI_BaseResource +{ + [Write, Description("Specifies the user account credentials to use to perform this task."), EmbeddedInstance("MSFT_Credential")] String Credential; + [Key, Description("Specifies the target Active Directory forest for the change.")] String ForestName; + [Write, Description("Specifies the Service Principal Name (SPN) Suffix(es) to be explicitly defined in the forest and replace existing Service Principal Names. Cannot be used with ServicePrincipalNameSuffixToAdd or ServicePrincipalNameSuffixToRemove.")] String ServicePrincipalNameSuffix[]; + [Write, Description("Specifies the Service Principal Name (SPN) Suffix(es) to be added to the forest. Cannot be used with ServicePrincipalNameSuffix.")] String ServicePrincipalNameSuffixToAdd[]; + [Write, Description("Specifies the Service Principal Name (SPN) Suffix(es) to be removed from the forest. Cannot be used with ServicePrincipalNameSuffix.")] String ServicePrincipalNameSuffixToRemove[]; + [Write, Description("Specifies the AD Tombstone lifetime which determines how long deleted items exist in Active Directory before they are purged.")] Sint32 TombStoneLifetime; + [Write, Description("Specifies the User Principal Name (UPN) Suffix(es) to be explicitly defined in the forest and replace existing User Principal Names. Cannot be used with UserPrincipalNameSuffixToAdd or UserPrincipalNameSuffixToRemove.")] String UserPrincipalNameSuffix[]; + [Write, Description("Specifies the User Principal Name (UPN) Suffix(es) to be added to the forest. Cannot be used with UserPrincipalNameSuffix.")] String UserPrincipalNameSuffixToAdd[]; + [Write, Description("Specifies the User Principal Name (UPN) Suffix(es) to be removed from the forest. Cannot be used with UserPrincipalNameSuffix.")] String UserPrincipalNameSuffixToRemove[]; +}; diff --git a/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_ADForestProperties/README.md b/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_ADForestProperties/README.md new file mode 100644 index 0000000..68e767b --- /dev/null +++ b/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_ADForestProperties/README.md @@ -0,0 +1,8 @@ +# Description + +The ADForestProperties DSC resource will manage forest wide settings within an Active Directory forest. +These include User Principal Name (UPN) suffixes, Service Principal Name (SPN) suffixes and the tombstone lifetime. + +## Requirements + +* Target machine must be running Windows Server 2008 R2 or later. diff --git a/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_ADForestProperties/en-US/MSFT_ADForestProperties.strings.psd1 b/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_ADForestProperties/en-US/MSFT_ADForestProperties.strings.psd1 new file mode 100644 index 0000000..547d589 --- /dev/null +++ b/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_ADForestProperties/en-US/MSFT_ADForestProperties.strings.psd1 @@ -0,0 +1,17 @@ +# culture="en-US" +ConvertFrom-StringData @' + GetForest = Getting properties for forest '{0}'. (ADFP0001) + ForestUpnSuffixNotInDesiredState = User Principal Name Suffix for forest '{0}' not in the desired state. (ADFP0002) + ForestSpnSuffixNotInDesiredState = Service Principal Name Suffix for forest '{0}' not in the desired state. (ADFP0003) + AddSpnSuffix = Adding Service Principal Name Suffix: '{0}' for forest '{1}'. (ADFP0004) + RemoveSpnSuffix = Removing Service Principal Name Suffix: '{0}' for forest '{1}'. (ADFP0005) + ReplaceSpnSuffix = Replacing Service Principal Name Suffix with: '{0}' for forest '{1}'. (ADFP0006) + ClearSpnSuffix = Clearing Service Principal Name Suffix for forest '{0}'. (ADFP0007) + AddUpnSuffix = Adding User Principal Name Suffix: '{0}' for forest '{1}'. (ADFP0008) + RemoveUpnSuffix = Removing User Principal Name Suffix: '{0}' for forest '{1}'. (ADFP0009) + ReplaceUpnSuffix = Replacing User Principal Name Suffix with: '{0}' for forest '{1}'. (ADFP0010) + ClearUpnSuffix = Clearing User Principal Name Suffix for forest '{0}'. (ADFP0011) + TombstoneLifetimeNotInDesiredState = Tombstone lifetime for forest '{0}' not in the desired state. Current: '{1}', Expected: '{2}'. (ADFP0012) + SetTombstoneLifetime = Setting tombstone lifetime to '{0}' for forest '{1}. (ADFP0013) + SetTombstoneLifetimeError = Error setting tombstone lifetime to '{0}' for forest '{1}. (ADFP0014) +'@ diff --git a/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_ADForestProperties/en-US/about_ADForestProperties.help.txt b/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_ADForestProperties/en-US/about_ADForestProperties.help.txt new file mode 100644 index 0000000..8f2bb7b --- /dev/null +++ b/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_ADForestProperties/en-US/about_ADForestProperties.help.txt @@ -0,0 +1,111 @@ +.NAME + ADForestProperties + +.DESCRIPTION + The ADForestProperties DSC resource will manage forest wide settings within an Active Directory forest. + These include User Principal Name (UPN) suffixes, Service Principal Name (SPN) suffixes and the tombstone lifetime. + + ## Requirements + + * Target machine must be running Windows Server 2008 R2 or later. + +.PARAMETER Credential + Write - PSCredential + Specifies the user account credentials to use to perform this task. + +.PARAMETER ForestName + Key - String + Specifies the target Active Directory forest for the change. + +.PARAMETER ServicePrincipalNameSuffix + Write - StringArray + Specifies the Service Principal Name (SPN) Suffix(es) to be explicitly defined in the forest and replace existing Service Principal Names. Cannot be used with ServicePrincipalNameSuffixToAdd or ServicePrincipalNameSuffixToRemove. + +.PARAMETER ServicePrincipalNameSuffixToAdd + Write - StringArray + Specifies the Service Principal Name (SPN) Suffix(es) to be added to the forest. Cannot be used with ServicePrincipalNameSuffix. + +.PARAMETER ServicePrincipalNameSuffixToRemove + Write - StringArray + Specifies the Service Principal Name (SPN) Suffix(es) to be removed from the forest. Cannot be used with ServicePrincipalNameSuffix. + +.PARAMETER TombStoneLifetime + Write - SInt32 + Specifies the AD Tombstone lifetime which determines how long deleted items exist in Active Directory before they are purged. + +.PARAMETER UserPrincipalNameSuffix + Write - StringArray + Specifies the User Principal Name (UPN) Suffix(es) to be explicitly defined in the forest and replace existing User Principal Names. Cannot be used with UserPrincipalNameSuffixToAdd or UserPrincipalNameSuffixToRemove. + +.PARAMETER UserPrincipalNameSuffixToAdd + Write - StringArray + Specifies the User Principal Name (UPN) Suffix(es) to be added to the forest. Cannot be used with UserPrincipalNameSuffix. + +.PARAMETER UserPrincipalNameSuffixToRemove + Write - StringArray + Specifies the User Principal Name (UPN) Suffix(es) to be removed from the forest. Cannot be used with UserPrincipalNameSuffix. + +.EXAMPLE 1 + +This configuration will manage the Service and User Principal name suffixes +in the forest by replacing any existing suffixes with the ones specified +in the configuration. + +Configuration ADForestProperties_ReplaceForestProperties_Config +{ + Import-DscResource -ModuleName ActiveDirectoryDsc + + node 'localhost' + { + ADForestProperties 'contoso.com' + { + ForestName = 'contoso.com' + UserPrincipalNameSuffix = 'fabrikam.com', 'industry.com' + ServicePrincipalNameSuffix = 'corporate.com' + } + } +} + +.EXAMPLE 2 + +This configuration will manage the Service and User Principal name suffixes in +the forest by adding and removing the desired suffixes. This will not overwrite +existing suffixes in the forest. + +Configuration ADForestProperties_AddRemoveForestProperties_Config +{ + Import-DscResource -ModuleName ActiveDirectoryDsc + + node localhost + { + ADForestProperties 'ContosoProperties' + { + ForestName = 'contoso.com' + ServicePrincipalNameSuffixToAdd = 'test.net' + ServicePrincipalNameSuffixToRemove = 'test.com' + UserPrincipalNameSuffixToAdd = 'cloudapp.net', 'fabrikam.com' + UserPrincipalNameSuffixToRemove = 'pester.net' + } + } +} + +.EXAMPLE 3 + +This configuration will manage the Tombstone Lifetime setting of the +Active Directory forest. + +Configuration ADForestProperties_AddRemoveForestProperties_Config +{ + Import-DscResource -ModuleName ActiveDirectoryDsc + + node localhost + { + ADForestProperties 'ContosoProperties' + { + ForestName = 'contoso.com' + TombstoneLifetime = 200 + } + } +} + + diff --git a/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_ADGroup/MSFT_ADGroup.psm1 b/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_ADGroup/MSFT_ADGroup.psm1 new file mode 100644 index 0000000..723f0dc --- /dev/null +++ b/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_ADGroup/MSFT_ADGroup.psm1 @@ -0,0 +1,853 @@ +$script:resourceModulePath = Split-Path -Path (Split-Path -Path $PSScriptRoot -Parent) -Parent +$script:modulesFolderPath = Join-Path -Path $script:resourceModulePath -ChildPath 'Modules' + +$script:localizationModulePath = Join-Path -Path $script:modulesFolderPath -ChildPath 'ActiveDirectoryDsc.Common' +Import-Module -Name (Join-Path -Path $script:localizationModulePath -ChildPath 'ActiveDirectoryDsc.Common.psm1') + +$script:localizedData = Get-LocalizedData -ResourceName 'MSFT_ADGroup' + +<# + .SYNOPSIS + Returns the current state of the Active Directory group. + + .PARAMETER GroupName + Name of the Active Directory group. + + .PARAMETER GroupScope + Active Directory group scope. Default value is 'Global'. + + .PARAMETER Category + Active Directory group category. Default value is 'Security'. + + .PARAMETER Path + Location of the group within Active Directory expressed as a Distinguished Name. + + .PARAMETER Ensure + Specifies if this Active Directory group should be present or absent. + Default value is 'Present'. + + .PARAMETER Description + Description of the Active Directory group. + + .PARAMETER DisplayName + Display name of the Active Directory group. + + .PARAMETER Credential + Credentials used to enact the change upon. + + .PARAMETER DomainController + Active Directory domain controller to enact the change upon. + + .PARAMETER Members + Active Directory group membership should match membership exactly. + + .PARAMETER MembersToInclude + Active Directory group should include these members. + + .PARAMETER MembersToExclude + Active Directory group should NOT include these members. + + .PARAMETER MembershipAttribute + Active Directory attribute used to perform membership operations. + Default value is 'SamAccountName'. + + .PARAMETER ManagedBy + Active Directory managed by attribute specified as a DistinguishedName. + + .PARAMETER Notes + Active Directory group notes field. + + .PARAMETER RestoreFromRecycleBin + Try to restore the group from the recycle bin before creating a new one. +#> +function Get-TargetResource +{ + [CmdletBinding()] + [OutputType([System.Collections.Hashtable])] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $GroupName, + + [Parameter()] + [ValidateSet('DomainLocal', 'Global', 'Universal')] + [System.String] + $GroupScope = 'Global', + + [Parameter()] + [ValidateSet('Security', 'Distribution')] + [System.String] + $Category = 'Security', + + [Parameter()] + [ValidateNotNullOrEmpty()] + [System.String] + $Path, + + [Parameter()] + [ValidateSet('Present', 'Absent')] + [System.String] + $Ensure = 'Present', + + [Parameter()] + [ValidateNotNullOrEmpty()] + [System.String] + $Description, + + [Parameter()] + [ValidateNotNullOrEmpty()] + [System.String] + $DisplayName, + + [Parameter()] + [ValidateNotNull()] + [System.Management.Automation.PSCredential] + [System.Management.Automation.CredentialAttribute()] + $Credential, + + [Parameter()] + [ValidateNotNullOrEmpty()] + [System.String] + $DomainController, + + [Parameter()] + [System.String[]] + $Members, + + [Parameter()] + [System.String[]] + $MembersToInclude, + + [Parameter()] + [System.String[]] + $MembersToExclude, + + [Parameter()] + [ValidateSet('SamAccountName', 'DistinguishedName', 'SID', 'ObjectGUID')] + [System.String] + $MembershipAttribute = 'SamAccountName', + + # This must be the user's DN + [Parameter()] + [ValidateNotNullOrEmpty()] + [System.String] + $ManagedBy, + + [Parameter()] + [ValidateNotNullOrEmpty()] + [System.String] + $Notes, + + [Parameter()] + [ValidateNotNull()] + [System.Boolean] + $RestoreFromRecycleBin + ) + + Assert-Module -ModuleName 'ActiveDirectory' + + $getTargetResourceReturnValue = @{ + Ensure = 'Absent' + GroupName = $GroupName + GroupScope = $null + Category = $null + Path = $null + Description = $null + DisplayName = $null + Members = @() + MembersToInclude = $MembersToInclude + MembersToExclude = $MembersToExclude + MembershipAttribute = $MembershipAttribute + ManagedBy = $null + Notes = $null + DistinguishedName = $null + } + + $commonParameters = Get-ADCommonParameters @PSBoundParameters + + try + { + $adGroup = Get-ADGroup @commonParameters -Properties @( + 'Name', + 'GroupScope', + 'GroupCategory', + 'DistinguishedName', + 'Description', + 'DisplayName', + 'ManagedBy', + 'Info' + ) + + Write-Verbose -Message ($script:localizedData.RetrievingGroupMembers -f $MembershipAttribute) + + if ($adGroup) + { + # Retrieve the current list of members, returning the specified membership attribute + [System.Array] $adGroupMembers = (Get-ADGroupMember @commonParameters).$MembershipAttribute + + $getTargetResourceReturnValue['Ensure'] = 'Present' + $getTargetResourceReturnValue['GroupName'] = $adGroup.Name + $getTargetResourceReturnValue['GroupScope'] = $adGroup.GroupScope + $getTargetResourceReturnValue['Category'] = $adGroup.GroupCategory + $getTargetResourceReturnValue['DistinguishedName'] = $adGroup.DistinguishedName + $getTargetResourceReturnValue['Path'] = Get-ADObjectParentDN -DN $adGroup.DistinguishedName + $getTargetResourceReturnValue['Description'] = $adGroup.Description + $getTargetResourceReturnValue['DisplayName'] = $adGroup.DisplayName + $getTargetResourceReturnValue['Members'] = $adGroupMembers + $getTargetResourceReturnValue['ManagedBy'] = $adGroup.ManagedBy + $getTargetResourceReturnValue['Notes'] = $adGroup.Info + } + } + catch [Microsoft.ActiveDirectory.Management.ADIdentityNotFoundException] + { + Write-Verbose -Message ($script:localizedData.GroupNotFound -f $GroupName) + } + + return $getTargetResourceReturnValue +} #end function Get-TargetResource + +<# + .SYNOPSIS + Determines if the Active Directory group is in the desired state. + + .PARAMETER GroupName + Name of the Active Directory group. + + .PARAMETER GroupScope + Active Directory group scope. Default value is 'Global'. + + .PARAMETER Category + Active Directory group category. Default value is 'Security'. + + .PARAMETER Path + Location of the group within Active Directory expressed as a Distinguished Name. + + .PARAMETER Ensure + Specifies if this Active Directory group should be present or absent. + Default value is 'Present'. + + .PARAMETER Description + Description of the Active Directory group. + + .PARAMETER DisplayName + Display name of the Active Directory group. + + .PARAMETER Credential + Credentials used to enact the change upon. + + .PARAMETER DomainController + Active Directory domain controller to enact the change upon. + + .PARAMETER Members + Active Directory group membership should match membership exactly. + + .PARAMETER MembersToInclude + Active Directory group should include these members. + + .PARAMETER MembersToExclude + Active Directory group should NOT include these members. + + .PARAMETER MembershipAttribute + Active Directory attribute used to perform membership operations. + Default value is 'SamAccountName'. + + .PARAMETER ManagedBy + Active Directory managed by attribute specified as a DistinguishedName. + + .PARAMETER Notes + Active Directory group notes field. + + .PARAMETER RestoreFromRecycleBin + Try to restore the group from the recycle bin before creating a new one. +#> +function Test-TargetResource +{ + [CmdletBinding()] + [OutputType([System.Boolean])] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $GroupName, + + [Parameter()] + [ValidateSet('DomainLocal', 'Global', 'Universal')] + [System.String] + $GroupScope = 'Global', + + [Parameter()] + [ValidateSet('Security', 'Distribution')] + [System.String] + $Category = 'Security', + + [Parameter()] + [ValidateNotNullOrEmpty()] + [System.String] + $Path, + + [Parameter()] + [ValidateSet('Present', 'Absent')] + [System.String] + $Ensure = 'Present', + + [Parameter()] + [ValidateNotNullOrEmpty()] + [System.String] + $Description, + + [Parameter()] + [ValidateNotNullOrEmpty()] + [System.String] + $DisplayName, + + [Parameter()] + [ValidateNotNull()] + [System.Management.Automation.PSCredential] + [System.Management.Automation.CredentialAttribute()] + $Credential, + + [Parameter()] + [ValidateNotNullOrEmpty()] + [System.String] + $DomainController, + + [Parameter()] + [System.String[]] + $Members, + + [Parameter()] + [System.String[]] + $MembersToInclude, + + [Parameter()] + [System.String[]] + $MembersToExclude, + + [Parameter()] + [ValidateSet('SamAccountName', 'DistinguishedName', 'SID', 'ObjectGUID')] + [System.String] + $MembershipAttribute = 'SamAccountName', + + # This must be the user's DN + [Parameter()] + [ValidateNotNullOrEmpty()] + [System.String] + $ManagedBy, + + [Parameter()] + [ValidateNotNullOrEmpty()] + [System.String] + $Notes, + + [Parameter()] + [ValidateNotNull()] + [System.Boolean] + $RestoreFromRecycleBin + ) + + # Validate parameters before we even attempt to retrieve anything + $assertMemberParameters = @{} + + # Members parameter should always be tested to enforce an empty group (issue #189) + if ($PSBoundParameters.ContainsKey('Members')) + { + $assertMemberParameters['Members'] = $Members + } + + if ($PSBoundParameters.ContainsKey('MembersToInclude') -and -not [System.String]::IsNullOrEmpty($MembersToInclude)) + { + $assertMemberParameters['MembersToInclude'] = $MembersToInclude + } + + if ($PSBoundParameters.ContainsKey('MembersToExclude') -and -not [System.String]::IsNullOrEmpty($MembersToExclude)) + { + $assertMemberParameters['MembersToExclude'] = $MembersToExclude + } + + Assert-MemberParameters @assertMemberParameters + + $targetResource = Get-TargetResource @PSBoundParameters + + $targetResourceInCompliance = $true + + if ($PSBoundParameters.ContainsKey('GroupScope') -and $targetResource.GroupScope -ne $GroupScope) + { + Write-Verbose -Message ($script:localizedData.NotDesiredPropertyState -f 'GroupScope', $GroupScope, $targetResource.GroupScope) + $targetResourceInCompliance = $false + } + + if ($PSBoundParameters.ContainsKey('Category') -and $targetResource.Category -ne $Category) + { + Write-Verbose -Message ($script:localizedData.NotDesiredPropertyState -f 'Category', $Category, $targetResource.Category) + $targetResourceInCompliance = $false + } + + if ($Path -and ($targetResource.Path -ne $Path)) + { + Write-Verbose -Message ($script:localizedData.NotDesiredPropertyState -f 'Path', $Path, $targetResource.Path) + $targetResourceInCompliance = $false + } + + if ($Description -and ($targetResource.Description -ne $Description)) + { + Write-Verbose -Message ($script:localizedData.NotDesiredPropertyState -f 'Description', $Description, $targetResource.Description) + $targetResourceInCompliance = $false + } + + if ($DisplayName -and ($targetResource.DisplayName -ne $DisplayName)) + { + Write-Verbose -Message ($script:localizedData.NotDesiredPropertyState -f 'DisplayName', $DisplayName, $targetResource.DisplayName) + $targetResourceInCompliance = $false + } + + if ($ManagedBy -and ($targetResource.ManagedBy -ne $ManagedBy)) + { + Write-Verbose -Message ($script:localizedData.NotDesiredPropertyState -f 'ManagedBy', $ManagedBy, $targetResource.ManagedBy) + $targetResourceInCompliance = $false + } + + if ($Notes -and ($targetResource.Notes -ne $Notes)) + { + Write-Verbose -Message ($script:localizedData.NotDesiredPropertyState -f 'Notes', $Notes, $targetResource.Notes) + $targetResourceInCompliance = $false + } + + # Test group members match passed membership parameters + if (-not (Test-Members @assertMemberParameters -ExistingMembers $targetResource.Members)) + { + Write-Verbose -Message $script:localizedData.GroupMembershipNotDesiredState + $targetResourceInCompliance = $false + } + + if ($targetResource.Ensure -ne $Ensure) + { + Write-Verbose -Message ($script:localizedData.NotDesiredPropertyState -f 'Ensure', $Ensure, $targetResource.Ensure) + $targetResourceInCompliance = $false + } + + return $targetResourceInCompliance +} #end function Test-TargetResource + +<# + .SYNOPSIS + Creates, removes or modifies the Active Directory group. + + .PARAMETER GroupName + Name of the Active Directory group. + + .PARAMETER GroupScope + Active Directory group scope. Default value is 'Global'. + + .PARAMETER Category + Active Directory group category. Default value is 'Security'. + + .PARAMETER Path + Location of the group within Active Directory expressed as a Distinguished Name. + + .PARAMETER Ensure + Specifies if this Active Directory group should be present or absent. + Default value is 'Present'. + + .PARAMETER Description + Description of the Active Directory group. + + .PARAMETER DisplayName + Display name of the Active Directory group. + + .PARAMETER Credential + Credentials used to enact the change upon. + + .PARAMETER DomainController + Active Directory domain controller to enact the change upon. + + .PARAMETER Members + Active Directory group membership should match membership exactly. + + .PARAMETER MembersToInclude + Active Directory group should include these members. + + .PARAMETER MembersToExclude + Active Directory group should NOT include these members. + + .PARAMETER MembershipAttribute + Active Directory attribute used to perform membership operations. + Default value is 'SamAccountName'. + + .PARAMETER ManagedBy + Active Directory managed by attribute specified as a DistinguishedName. + + .PARAMETER Notes + Active Directory group notes field. + + .PARAMETER RestoreFromRecycleBin + Try to restore the group from the recycle bin before creating a new one. +#> +function Set-TargetResource +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $GroupName, + + [Parameter()] + [ValidateSet('DomainLocal', 'Global', 'Universal')] + [System.String] + $GroupScope = 'Global', + + [Parameter()] + [ValidateSet('Security', 'Distribution')] + [System.String] + $Category = 'Security', + + [Parameter()] + [ValidateNotNullOrEmpty()] + [System.String] + $Path, + + [Parameter()] + [ValidateSet('Present', 'Absent')] + [System.String] + $Ensure = 'Present', + + [Parameter()] + [ValidateNotNullOrEmpty()] + [System.String] + $Description, + + [Parameter()] + [ValidateNotNullOrEmpty()] + [System.String] + $DisplayName, + + [Parameter()] + [ValidateNotNull()] + [System.Management.Automation.PSCredential] + [System.Management.Automation.CredentialAttribute()] + $Credential, + + [Parameter()] + [ValidateNotNullOrEmpty()] + [System.String] + $DomainController, + + [Parameter()] + [System.String[]] + $Members, + + [Parameter()] + [System.String[]] + $MembersToInclude, + + [Parameter()] + [System.String[]] + $MembersToExclude, + + [Parameter()] + [ValidateSet('SamAccountName', 'DistinguishedName', 'SID', 'ObjectGUID')] + [System.String] + $MembershipAttribute = 'SamAccountName', + + # This must be the user's DN + [Parameter()] + [ValidateNotNullOrEmpty()] + [System.String] + $ManagedBy, + + [Parameter()] + [ValidateNotNullOrEmpty()] + [System.String] + $Notes, + + [Parameter()] + [ValidateNotNull()] + [System.Boolean] + $RestoreFromRecycleBin + + ) + + Assert-Module -ModuleName 'ActiveDirectory' + + $assertMemberParameters = @{} + + # Members parameter should always be added to enforce an empty group (issue #189) + if ($PSBoundParameters.ContainsKey('Members')) + { + $assertMemberParameters['Members'] = $Members + } + + if ($PSBoundParameters.ContainsKey('MembersToInclude') -and -not [System.String]::IsNullOrEmpty($MembersToInclude)) + { + $assertMemberParameters['MembersToInclude'] = $MembersToInclude + } + + if ($PSBoundParameters.ContainsKey('MembersToExclude') -and -not [System.String]::IsNullOrEmpty($MembersToExclude)) + { + $assertMemberParameters['MembersToExclude'] = $MembersToExclude + } + + Assert-MemberParameters @assertMemberParameters + + $membersInMultipleDomains = $false + + if ($MembershipAttribute -eq 'DistinguishedName') + { + $allMembers = $Members + $MembersToInclude + $MembersToExclude + + $groupMemberDomains = @() + + foreach ($member in $allMembers) + { + $groupMemberDomains += Get-ADDomainNameFromDistinguishedName -DistinguishedName $member + } + + $uniqueGroupMemberDomainCount = $groupMemberDomains | + Select-Object -Unique + + $GroupMemberDomainCount = $uniqueGroupMemberDomainCount.count + + if ($GroupMemberDomainCount -gt 1 -or ($groupMemberDomains -ine (Get-DomainName)).Count -gt 0) + { + Write-Verbose -Message ($script:localizedData.GroupMembershipMultipleDomains -f $GroupMemberDomainCount) + $membersInMultipleDomains = $true + } + } + + $commonParameters = Get-ADCommonParameters @PSBoundParameters + + $getTargetResourceResult = Get-TargetResource @PSBoundParameters + + if ($getTargetResourceResult.Ensure -eq 'Present') + { + if ($Ensure -eq 'Present') + { + $setADGroupParams = $commonParameters.Clone() + $setADGroupParams['Identity'] = $getTargetResourceResult.DistinguishedName + + # Update existing group properties + if ($PSBoundParameters.ContainsKey('Category') -and $Category -ne $getTargetResourceResult.Category) + { + Write-Verbose -Message ($script:localizedData.UpdatingGroupProperty -f 'Category', $Category) + + $setADGroupParams['GroupCategory'] = $Category + } + + if ($PSBoundParameters.ContainsKey('GroupScope') -and $GroupScope -ne $getTargetResourceResult.GroupScope) + { + # Cannot change DomainLocal to Global or vice versa directly. Need to change them to a Universal group first! + Set-ADGroup -Identity $getTargetResourceResult.DistinguishedName -GroupScope 'Universal' -ErrorAction 'Stop' + + Write-Verbose -Message ($script:localizedData.UpdatingGroupProperty -f 'GroupScope', $GroupScope) + + $setADGroupParams['GroupScope'] = $GroupScope + } + + if ($Description -and ($Description -ne $getTargetResourceResult.Description)) + { + Write-Verbose -Message ($script:localizedData.UpdatingGroupProperty -f 'Description', $Description) + + $setADGroupParams['Description'] = $Description + } + + if ($DisplayName -and ($DisplayName -ne $getTargetResourceResult.DisplayName)) + { + Write-Verbose -Message ($script:localizedData.UpdatingGroupProperty -f 'DisplayName', $DisplayName) + + $setADGroupParams['DisplayName'] = $DisplayName + } + + if ($ManagedBy -and ($ManagedBy -ne $getTargetResourceResult.ManagedBy)) + { + Write-Verbose -Message ($script:localizedData.UpdatingGroupProperty -f 'ManagedBy', $ManagedBy) + + $setADGroupParams['ManagedBy'] = $ManagedBy + } + + if ($Notes -and ($Notes -ne $getTargetResourceResult.Notes)) + { + Write-Verbose -Message ($script:localizedData.UpdatingGroupProperty -f 'Notes', $Notes) + + $setADGroupParams['Replace'] = @{ + Info = $Notes + } + } + + Write-Verbose -Message ($script:localizedData.UpdatingGroup -f $GroupName) + + Set-ADGroup @setADGroupParams -ErrorAction 'Stop' + + $groupParentDistinguishedName = Get-ADObjectParentDN -DN $getTargetResourceResult.DistinguishedName + + # Move group if the path is not correct + if ($Path -and $Path -ne $groupParentDistinguishedName) + { + Write-Verbose -Message ($script:localizedData.MovingGroup -f $GroupName, $Path) + + $moveADObjectParams = $commonParameters.Clone() + $moveADObjectParams['Identity'] = $getTargetResourceResult.DistinguishedName + $moveADObjectParams['TargetPath'] = $Path + $moveADObjectParams['ErrorAction'] = 'Stop' + + Move-ADObject @moveADObjectParams + } + + if ($assertMemberParameters.Count -gt 0) + { + Write-Verbose -Message ($script:localizedData.RetrievingGroupMembers -f $MembershipAttribute) + + $adGroupMembers = (Get-ADGroupMember @commonParameters).$MembershipAttribute + + $assertMemberParameters['ExistingMembers'] = $adGroupMembers + + # Return $false if the members mismatch. + if (-not (Test-Members @assertMemberParameters)) + { + # Members parameter should always be enforce if it is bound (issue #189) + if ($PSBoundParameters.ContainsKey('Members')) + { + # Remove all existing first and add explicit members + $Members = Remove-DuplicateMembers -Members $Members + + # We can only remove members if there are members already in the group! + if ($adGroupMembers.Count -gt 0) + { + Write-Verbose -Message ($script:localizedData.RemovingGroupMembers -f $adGroupMembers.Count, $GroupName) + + Remove-ADGroupMember @commonParameters -Members $adGroupMembers -Confirm:$false -ErrorAction 'Stop' + } + + Write-Verbose -Message ($script:localizedData.AddingGroupMembers -f $Members.Count, $GroupName) + + Add-ADCommonGroupMember -Parameters $commonParameters -Members $Members -MembersInMultipleDomains:$membersInMultipleDomains + } + + if ($PSBoundParameters.ContainsKey('MembersToInclude') -and -not [System.String]::IsNullOrEmpty($MembersToInclude)) + { + $MembersToInclude = Remove-DuplicateMembers -Members $MembersToInclude + + Write-Verbose -Message ($script:localizedData.AddingGroupMembers -f $MembersToInclude.Count, $GroupName) + + Add-ADCommonGroupMember -Parameters $commonParameters -Members $MembersToInclude -MembersInMultipleDomains:$membersInMultipleDomains + } + + if ($PSBoundParameters.ContainsKey('MembersToExclude') -and -not [System.String]::IsNullOrEmpty($MembersToExclude)) + { + $MembersToExclude = Remove-DuplicateMembers -Members $MembersToExclude + + Write-Verbose -Message ($script:localizedData.RemovingGroupMembers -f $MembersToExclude.Count, $GroupName) + + Remove-ADGroupMember @commonParameters -Members $MembersToExclude -Confirm:$false -ErrorAction 'Stop' + } + } + } + } + elseif ($Ensure -eq 'Absent') + { + # Remove existing group + Write-Verbose -Message ($script:localizedData.RemovingGroup -f $GroupName) + + Remove-ADGroup @commonParameters -Confirm:$false -ErrorAction 'Stop' + } + } + else + { + # The Active Directory group does not exist, check if it should. + if ($Ensure -eq 'Present') + { + $commonParametersUsingName = Get-ADCommonParameters @PSBoundParameters -UseNameParameter + + $newAdGroupParameters = $commonParametersUsingName.Clone() + $newAdGroupParameters['GroupCategory'] = $Category + $newAdGroupParameters['GroupScope'] = $GroupScope + + if ($PSBoundParameters.ContainsKey('Description')) + { + $newAdGroupParameters['Description'] = $Description + } + + if ($PSBoundParameters.ContainsKey('DisplayName')) + { + $newAdGroupParameters['DisplayName'] = $DisplayName + } + + if ($PSBoundParameters.ContainsKey('ManagedBy')) + { + $newAdGroupParameters['ManagedBy'] = $ManagedBy + } + + if ($PSBoundParameters.ContainsKey('Path')) + { + $newAdGroupParameters['Path'] = $Path + } + + $adGroup = $null + + # Create group. Try to restore account first if it exists. + if ($RestoreFromRecycleBin) + { + Write-Verbose -Message ($script:localizedData.RestoringGroup -f $GroupName) + + $restoreParams = Get-ADCommonParameters @PSBoundParameters + + $adGroup = Restore-ADCommonObject @restoreParams -ObjectClass 'Group' + } + + <# + Check if the Active Directory group was restored, if not create + the group. + #> + if (-not $adGroup) + { + Write-Verbose -Message ($script:localizedData.AddingGroup -f $GroupName) + + $adGroup = New-ADGroup @newAdGroupParameters -PassThru -ErrorAction 'Stop' + } + + <# + Only the New-ADGroup cmdlet takes a -Name parameter. Refresh + the parameters with the -Identity parameter rather than -Name. + #> + $commonParameters = Get-ADCommonParameters @PSBoundParameters + + if ($PSBoundParameters.ContainsKey('Notes')) + { + # Can't set the Notes field when creating the group + Write-Verbose -Message ($script:localizedData.UpdatingGroupProperty -f 'Notes', $Notes) + + $setADGroupParams = $commonParameters.Clone() + $setADGroupParams['Identity'] = $adGroup.DistinguishedName + $setADGroupParams['ErrorAction'] = 'Stop' + $setADGroupParams['Add'] = @{ + Info = $Notes + } + + Set-ADGroup @setADGroupParams + } + + # Add the required members + if ($PSBoundParameters.ContainsKey('Members') -and -not [System.String]::IsNullOrEmpty($Members)) + { + $Members = Remove-DuplicateMembers -Members $Members + + Write-Verbose -Message ($script:localizedData.AddingGroupMembers -f $Members.Count, $GroupName) + + Add-ADCommonGroupMember -Parameters $commonParameters -Members $Members -MembersInMultipleDomains:$membersInMultipleDomains + } + elseif ($PSBoundParameters.ContainsKey('MembersToInclude') -and -not [System.String]::IsNullOrEmpty($MembersToInclude)) + { + $MembersToInclude = Remove-DuplicateMembers -Members $MembersToInclude + + Write-Verbose -Message ($script:localizedData.AddingGroupMembers -f $MembersToInclude.Count, $GroupName) + + Add-ADCommonGroupMember -Parameters $commonParameters -Members $MembersToInclude -MembersInMultipleDomains:$membersInMultipleDomains + } + } + } #end catch +} #end function Set-TargetResource + +Export-ModuleMember -Function *-TargetResource diff --git a/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_ADGroup/MSFT_ADGroup.schema.mof b/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_ADGroup/MSFT_ADGroup.schema.mof new file mode 100644 index 0000000..7021d37 --- /dev/null +++ b/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_ADGroup/MSFT_ADGroup.schema.mof @@ -0,0 +1,21 @@ +[ClassVersion("1.0.1.0"), FriendlyName("ADGroup")] +class MSFT_ADGroup : OMI_BaseResource +{ + [Key, Description("Name of the Active Directory group.")] String GroupName; + [Write, Description("Active Directory group scope. Default value is 'Global'."), ValueMap{"DomainLocal","Global","Universal"}, Values{"DomainLocal","Global","Universal"}] String GroupScope; + [Write, Description("Active Directory group category. Default value is 'Security'."), ValueMap{"Security","Distribution"}, Values{"Security","Distribution"}] String Category; + [Write, Description("Location of the group within Active Directory expressed as a Distinguished Name.")] String Path; + [Write, Description("Specifies if this Active Directory group should be present or absent. Default value is 'Present'."), ValueMap{"Present","Absent"}, Values{"Present","Absent"}] String Ensure; + [Write, Description("Description of the Active Directory group.")] String Description; + [Write, Description("Display name of the Active Directory group.")] String DisplayName; + [Write, Description("Credentials used to enact the change upon."), EmbeddedInstance("MSFT_Credential")] String Credential; + [Write, Description("Active Directory domain controller to enact the change upon.")] String DomainController; + [Write, Description("Active Directory group membership should match membership exactly.")] String Members[]; + [Write, Description("Active Directory group should include these members.")] String MembersToInclude[]; + [Write, Description("Active Directory group should NOT include these members.")] String MembersToExclude[]; + [Write, Description("Active Directory attribute used to perform membership operations. Default value is 'SamAccountName'."), ValueMap{"SamAccountName","DistinguishedName","ObjectGUID","SID"}, Values{"SamAccountName","DistinguishedName","ObjectGUID","SID"}] String MembershipAttribute; + [Write, Description("Active Directory managed by attribute specified as a DistinguishedName.")] String ManagedBy; + [Write, Description("Active Directory group notes field.")] String Notes; + [Write, Description("Try to restore the group from the recycle bin before creating a new one.")] Boolean RestoreFromRecycleBin; + [Read, Description("Returns the distinguished name of the Active Directory group.")] String DistinguishedName; +}; diff --git a/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_ADGroup/README.md b/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_ADGroup/README.md new file mode 100644 index 0000000..ef2dd48 --- /dev/null +++ b/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_ADGroup/README.md @@ -0,0 +1,11 @@ +# Description + +The ADGroup DSC resource will manage groups within Active Directory. + +## Requirements + +* Target machine must be running Windows Server 2008 R2 or later. +* The parameter `RestoreFromRecycleBin` requires that the feature Recycle + Bin has been enabled prior to an object is deleted. If the feature + Recycle Bin is disabled then the property `msDS-LastKnownRDN` is not + added the deleted object. diff --git a/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_ADGroup/en-US/MSFT_ADGroup.strings.psd1 b/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_ADGroup/en-US/MSFT_ADGroup.strings.psd1 new file mode 100644 index 0000000..7194ef5 --- /dev/null +++ b/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_ADGroup/en-US/MSFT_ADGroup.strings.psd1 @@ -0,0 +1,16 @@ +# culture="en-US" +ConvertFrom-StringData @' + RetrievingGroupMembers = Retrieving group membership based on '{0}' property. (ADG0001) + GroupMembershipNotDesiredState = Group membership is NOT in the desired state. (ADG0002) + AddingGroupMembers = Adding '{0}' member(s) to AD group '{1}'. (ADG0003) + RemovingGroupMembers = Removing '{0}' member(s) from AD group '{1}'. (ADG0004) + AddingGroup = Creating AD Group '{0}'. (ADG0005) + UpdatingGroup = Updating AD Group '{0}'. (ADG0006) + RemovingGroup = Removing AD Group '{0}'. (ADG0007) + MovingGroup = Moving AD Group '{0}' to '{1}'. (ADG0008) + RestoringGroup = Attempting to restore the group {0} from recycle bin. (ADG0009) + GroupNotFound = AD Group '{0}' was not found. (ADG00010) + NotDesiredPropertyState = AD Group '{0}' is not correct. Expected '{1}', actual '{2}'. (ADG0011) + UpdatingGroupProperty = Updating AD Group property '{0}' to '{1}'. (ADG0012) + GroupMembershipMultipleDomains = Group membership objects are in '{0}' different AD Domains. (ADG0013) +'@ diff --git a/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_ADGroup/en-US/about_ADGroup.help.txt b/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_ADGroup/en-US/about_ADGroup.help.txt new file mode 100644 index 0000000..ee212c3 --- /dev/null +++ b/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_ADGroup/en-US/about_ADGroup.help.txt @@ -0,0 +1,171 @@ +.NAME + ADGroup + +.DESCRIPTION + The ADGroup DSC resource will manage groups within Active Directory. + + ## Requirements + + * Target machine must be running Windows Server 2008 R2 or later. + * The parameter `RestoreFromRecycleBin` requires that the feature Recycle + Bin has been enabled prior to an object is deleted. If the feature + Recycle Bin is disabled then the property `msDS-LastKnownRDN` is not + added the deleted object. + +.PARAMETER GroupName + Key - String + Name of the Active Directory group. + +.PARAMETER GroupScope + Write - String + Allowed values: DomainLocal, Global, Universal + Active Directory group scope. Default value is 'Global'. + +.PARAMETER Category + Write - String + Allowed values: Security, Distribution + Active Directory group category. Default value is 'Security'. + +.PARAMETER Path + Write - String + Location of the group within Active Directory expressed as a Distinguished Name. + +.PARAMETER Ensure + Write - String + Allowed values: Present, Absent + Specifies if this Active Directory group should be present or absent. Default value is 'Present'. + +.PARAMETER Description + Write - String + Description of the Active Directory group. + +.PARAMETER DisplayName + Write - String + Display name of the Active Directory group. + +.PARAMETER Credential + Write - PSCredential + Credentials used to enact the change upon. + +.PARAMETER DomainController + Write - String + Active Directory domain controller to enact the change upon. + +.PARAMETER Members + Write - StringArray + Active Directory group membership should match membership exactly. + +.PARAMETER MembersToInclude + Write - StringArray + Active Directory group should include these members. + +.PARAMETER MembersToExclude + Write - StringArray + Active Directory group should NOT include these members. + +.PARAMETER MembershipAttribute + Write - String + Allowed values: SamAccountName, DistinguishedName, ObjectGUID, SID + Active Directory attribute used to perform membership operations. Default value is 'SamAccountName'. + +.PARAMETER ManagedBy + Write - String + Active Directory managed by attribute specified as a DistinguishedName. + +.PARAMETER Notes + Write - String + Active Directory group notes field. + +.PARAMETER RestoreFromRecycleBin + Write - Boolean + Try to restore the group from the recycle bin before creating a new one. + +.PARAMETER DistinguishedName + Read - String + Returns the distinguished name of the Active Directory group. + +.EXAMPLE 1 + +This configuration will create a new domain-local group + +Configuration ADGroup_NewGroup_Config +{ + param + ( + [parameter(Mandatory = $true)] + [System.String] + $GroupName, + + [ValidateSet('DomainLocal', 'Global', 'Universal')] + [System.String] + $Scope = 'Global', + + [ValidateSet('Security', 'Distribution')] + [System.String] + $Category = 'Security', + + [ValidateNotNullOrEmpty()] + [System.String] + $Description + ) + + Import-DscResource -Module ActiveDirectoryDsc + + Node localhost + { + ADGroup 'ExampleGroup' + { + GroupName = $GroupName + GroupScope = $Scope + Category = $Category + Description = $Description + Ensure = 'Present' + } + } +} + +.EXAMPLE 2 + +This configuration will create a new domain-local group with three members. + +Configuration ADGroup_NewGroupWithMembers_Config +{ + Import-DscResource -ModuleName ActiveDirectoryDsc + + node localhost + { + ADGroup 'dl1' + { + GroupName = 'DL_APP_1' + GroupScope = 'DomainLocal' + Members = 'john', 'jim', 'sally' + } + } +} + +.EXAMPLE 3 + +This configuration will create a new domain-local group in contoso with +three members in different domains. + +Configuration ADGroup_NewGroupMultiDomainMembers_Config +{ + Import-DscResource -ModuleName ActiveDirectoryDsc + + node localhost + { + ADGroup 'dl1' + { + GroupName = 'DL_APP_1' + GroupScope = 'DomainLocal' + MembershipAttribute = 'DistinguishedName' + Members = @( + 'CN=john,OU=Accounts,DC=contoso,DC=com' + 'CN=jim,OU=Accounts,DC=subdomain,DC=contoso,DC=com' + 'CN=sally,OU=Accounts,DC=anothersub,DC=contoso,DC=com' + ) + } + } +} + + diff --git a/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_ADKDSKey/MSFT_ADKDSKey.psm1 b/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_ADKDSKey/MSFT_ADKDSKey.psm1 new file mode 100644 index 0000000..73a337e --- /dev/null +++ b/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_ADKDSKey/MSFT_ADKDSKey.psm1 @@ -0,0 +1,516 @@ +$script:resourceModulePath = Split-Path -Path (Split-Path -Path $PSScriptRoot -Parent) -Parent +$script:modulesFolderPath = Join-Path -Path $script:resourceModulePath -ChildPath 'Modules' + +$script:localizationModulePath = Join-Path -Path $script:modulesFolderPath -ChildPath 'ActiveDirectoryDsc.Common' +Import-Module -Name (Join-Path -Path $script:localizationModulePath -ChildPath 'ActiveDirectoryDsc.Common.psm1') + +$script:localizedData = Get-LocalizedData -ResourceName 'MSFT_ADKDSKey' + +<# + .SYNOPSIS + Gets the specified KDS root key + + .PARAMETER EffectiveTime + Specifies the Effective time when a KDS root key can be used. + There is a 10 hour minimum from creation date to allow active directory + to properly replicate across all domain controllers. For this reason, + the date must be set in the future for creation.While this parameter + accepts a string, it will be converted into a DateTime object. + This will also try to take into account cultural settings. + + Example: + '05/01/1999 13:00' using default or 'en-US' culture would be May 1st, + but using 'de-DE' culture would be 5th of January. The culture is + automatically pulled from the operating system and this can be checked + using 'Get-Culture' +#> +function Get-TargetResource +{ + [CmdletBinding()] + [OutputType([System.Collections.Hashtable])] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $EffectiveTime + ) + + Assert-Module -ModuleName 'ActiveDirectory' + + $targetResource = @{ + EffectiveTime = $EffectiveTime + CreationTime = $null + KeyId = $null + Ensure = $null + DistinguishedName = $null + } + + Write-Verbose -Message ($script:localizedData.RetrievingKDSRootKey -f $EffectiveTime) + + try + { + $effectiveTimeObject = [DateTime]::Parse($EffectiveTime) + } + catch + { + $errorMessage = $script:localizedData.EffectiveTimeInvalid -f $EffectiveTime + New-InvalidOperationException -Message $errorMessage -ErrorRecord $_ + } + + $currentUser = Get-CurrentUser + if (-not (Assert-HasDomainAdminRights -User $currentUser)) + { + $errorMessage = $script:localizedData.IncorrectPermissions -f $currentUser.Name + New-InvalidResultException -Message $errorMessage + } + + try + { + $kdsRootKeys = Get-KdsRootKey + } + catch + { + $errorMessage = $script:localizedData.RetrievingKDSRootKeyError -f $EffectiveTime + New-InvalidOperationException -Message $errorMessage -ErrorRecord $_ + } + + $kdsRootKey = $null + if ($kdsRootKeys) + { + $kdsRootKey = $kdsRootKeys.GetEnumerator() | + Where-Object -FilterScript { + [DateTime]::Parse($_.EffectiveTime) -eq $effectiveTimeObject + } + } + + if (-not $kdsRootKey) + { + $targetResource['Ensure'] = 'Absent' + } + else + { + Write-Verbose -Message ($script:localizedData.FoundKDSRootKey -f $EffectiveTime) + if ($kdsRootKeys.Count -gt 1) + { + Write-Warning -Message ($script:localizedData.FoundKDSRootKeyMultiple) + } + + if ($kdsRootKey.Count -gt 1) + { + $errorMessage = $script:localizedData.FoundKDSRootKeySameEffectiveTime -f $EffectiveTime + New-InvalidOperationException -Message $errorMessage + } + elseif ($kdsRootKey) + { + $targetResource['Ensure'] = 'Present' + $targetResource['EffectiveTime'] = ([DateTime]::Parse($kdsRootKey.EffectiveTime)).ToString() + $targetResource['CreationTime'] = $kdsRootKey.CreationTime + $targetResource['KeyId'] = $kdsRootKey.KeyId + $targetResource['DistinguishedName'] = 'CN={0},CN=Master Root Keys,CN=Group Key Distribution Service,CN=Services,CN=Configuration,{1}' -f + $kdsRootKey.KeyId, (Get-ADRootDomainDN) + } + } + + return $targetResource +} + +<# + .SYNOPSIS + Creates or deletes the KDS root Key + + .PARAMETER EffectiveTime + Specifies the Effective time when a KDS root key can be used. + There is a 10 hour minimum from creation date to allow active directory + to properly replicate across all domain controllers. For this reason, + the date must be set in the future for creation.While this parameter + accepts a string, it will be converted into a DateTime object. + This will also try to take into account cultural settings. + + Example: + '05/01/1999 13:00' using default or 'en-US' culture would be May 1st, + but using 'de-DE' culture would be 5th of January. The culture is + automatically pulled from the operating system and this can be checked + using 'Get-Culture' + + .PARAMETER AllowUnsafeEffectiveTime + This option will allow you to create a KDS root key if EffectiveTime is set in the past. + This may cause issues if you are creating a Group Managed Service Account right + after you create the KDS Root Key. In order to get around this, you must create + the KDS Root Key using a date in the past. This should be used at your own risk + and should only be used in lab environments. + + .PARAMETER Ensure + Specifies if this KDS Root Key should be present or absent + + .PARAMETER ForceRemove + This option will allow you to remove a KDS root key if there is only one key left. + It should not break your Group Managed Service Accounts (gMSAs), but if the gMSA + password expires and it needs to request a new password, it will not be able to + generate a new password until a new KDS Root Key is installed and ready for use. + Because of this, the last KDS Root Key will not be removed unless this option is specified +#> +function Test-TargetResource +{ + [CmdletBinding()] + [OutputType([System.Boolean])] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $EffectiveTime, + + [Parameter()] + [ValidateNotNullOrEmpty()] + [System.Boolean] + $AllowUnsafeEffectiveTime, + + [Parameter()] + [ValidateSet('Present', 'Absent')] + [System.String] + $Ensure = 'Present', + + [Parameter()] + [ValidateNotNullOrEmpty()] + [System.Boolean] + $ForceRemove + ) + + $getTargetResourceParameters = @{ + EffectiveTime = $EffectiveTime + Ensure = $Ensure + } + + $compareTargetResourceNonCompliant = Compare-TargetResourceState @getTargetResourceParameters | + Where-Object -FilterScript { + $_.Pass -eq $false + } + + $ensureState = $compareTargetResourceNonCompliant | + Where-Object -FilterScript { + $_.Parameter -eq 'Ensure' + } + + if ($ensureState) + { + Write-Verbose -Message ($script:localizedData.NotDesiredPropertyState -f + 'Ensure', $EffectiveTime, $ensureState.Expected, $ensureState.Actual) + Write-Verbose -Message ($script:localizedData.KDSRootKeyNotInDesiredState -f $EffectiveTime) + return $false + } + else + { + Write-Verbose -Message ($script:localizedData.KDSRootKeyInDesiredState -f $EffectiveTime) + return $true + } +} + +<# + .SYNOPSIS + Creates or deletes the KDS root Key + + .PARAMETER EffectiveTime + Specifies the Effective time when a KDS root key can be used. + There is a 10 hour minimum from creation date to allow active directory + to properly replicate across all domain controllers. For this reason, + the date must be set in the future for creation.While this parameter + accepts a string, it will be converted into a DateTime object. + This will also try to take into account cultural settings. + + Example: + '05/01/1999 13:00' using default or 'en-US' culture would be May 1st, + but using 'de-DE' culture would be 5th of January. The culture is + automatically pulled from the operating system and this can be checked + using 'Get-Culture' + + .PARAMETER AllowUnsafeEffectiveTime + This option will allow you to create a KDS root key if EffectiveTime is set in the past. + This may cause issues if you are creating a Group Managed Service Account right + after you create the KDS Root Key. In order to get around this, you must create + the KDS Root Key using a date in the past. This should be used at your own risk + and should only be used in lab environments. + + .PARAMETER Ensure + Specifies if this KDS Root Key should be present or absent + + .PARAMETER ForceRemove + This option will allow you to remove a KDS root key if there is only one key left. + It should not break your Group Managed Service Accounts (gMSAs), but if the gMSA + password expires and it needs to request a new password, it will not be able to + generate a new password until a new KDS Root Key is installed and ready for use. + Because of this, the last KDS Root Key will not be removed unless this option is specified +#> +function Set-TargetResource +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $EffectiveTime, + + [Parameter()] + [ValidateNotNullOrEmpty()] + [System.Boolean] + $AllowUnsafeEffectiveTime, + + [Parameter()] + [ValidateSet('Present', 'Absent')] + [System.String] + $Ensure = 'Present', + + [Parameter()] + [ValidateNotNullOrEmpty()] + [System.Boolean] + $ForceRemove + ) + + $getTargetResourceParameters = @{ + EffectiveTime = $EffectiveTime + Ensure = $Ensure + } + + $compareTargetResource = Compare-TargetResourceState @getTargetResourceParameters + $ensureState = $compareTargetResource | + Where-Object -FilterScript { + $_.Parameter -eq 'Ensure' + } + + # Ensure is not in proper state + if ($ensureState.Pass -eq $false) + { + if ($Ensure -eq 'Present') + { + try + { + $effectiveTimeObject = [DateTime]::Parse($EffectiveTime) + } + catch + { + $errorMessage = $script:localizedData.EffectiveTimeInvalid -f $EffectiveTime + New-InvalidOperationException -Message $errorMessage -ErrorRecord $_ + } + + $currentDateTimeObject = Get-Date + + # We want the key to be present, but it currently does not exist + if ($effectiveTimeObject -le $currentDateTimeObject -and + $PSBoundParameters.ContainsKey('AllowUnsafeEffectiveTime') -and $AllowUnsafeEffectiveTime) + { + Write-Warning -Message ($script:localizedData.AddingKDSRootKeyDateInPast -f $EffectiveTime) + } + elseif ($effectiveTimeObject -le $currentDateTimeObject) + { + <# + Effective time is in the past and we don't have AllowUnsafeEffectiveTime set + to enabled, so we exit with an error + #> + $errorMessage = $script:localizedData.AddingKDSRootKeyError -f $EffectiveTime + New-InvalidOperationException -Message $errorMessage + } + else + { + Write-Verbose -Message ($script:localizedData.AddingKDSRootKey -f $EffectiveTime) + } + + <# + EffectiveTime appears to expect a UTC datetime, so we are converting + it to UTC before adding. Get-KDSRootKey will return the wrong time if we + don't convert first + #> + try + { + Add-KDSRootKey -EffectiveTime $effectiveTimeObject.ToUniversalTime() + } + catch + { + $errorMessage = $script:localizedData.KDSRootKeyAddError -f $EffectiveTime + New-InvalidOperationException -Message $errorMessage -ErrorRecord $_ + } + } + elseif ($Ensure -eq 'Absent') + { + # We want the account to be Absent, but it is Present + if ((Get-KdsRootKey).Count -gt 1) + { + Write-Verbose -Message ($script:localizedData.RemovingKDSRootKey -f $EffectiveTime) + } + else + { + if ($PSBoundParameters.ContainsKey('ForceRemove') -and $ForceRemove) + { + Write-Verbose -Message ($script:localizedData.RemovingKDSRootKey -f $EffectiveTime) + Write-Warning -Message ($script:localizedData.NotEnoughKDSRootKeysPresent -f $EffectiveTime) + } + else + { + $errorMessage = $script:localizedData.NotEnoughKDSRootKeysPresentNoForce -f $EffectiveTime + New-InvalidOperationException -Message $errorMessage + } + } + + $distinguishedName = $compareTargetResource | + Where-Object -FilterScript { $_.Parameter -eq 'DistinguishedName' } + + try + { + Remove-ADObject -Identity $distinguishedName.Actual -Confirm:$false + } + catch + { + $errorMessage = $script:localizedData.KDSRootKeyRemoveError -f $EffectiveTime + New-InvalidOperationException -Message $errorMessage -ErrorRecord $_ + } + } + } +} + +<# + .SYNOPSIS + Compares the state of the KDS root key + + .PARAMETER EffectiveTime + Specifies the Effective time when a KDS root key can be used. + There is a 10 hour minimum from creation date to allow active directory + to properly replicate across all domain controllers. For this reason, + the date must be set in the future for creation. While this parameter + accepts a string, it will be converted into a DateTime object. + This will also try to take into account cultural settings. + + Example: + '05/01/1999 13:00' using default or 'en-US' culture would be May 1st, + but using 'de-DE' culture would be 5th of January. The culture is + automatically pulled from the operating system and this can be checked + using 'Get-Culture' + + .PARAMETER Ensure + Specifies if this KDS Root Key should be present or absent + +#> +function Compare-TargetResourceState +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $EffectiveTime, + + [Parameter()] + [ValidateSet('Present', 'Absent')] + [System.String] + $Ensure + ) + + $getTargetResourceParameters = @{ + EffectiveTime = $EffectiveTime + } + + $getTargetResourceResult = Get-TargetResource @getTargetResourceParameters + $compareTargetResource = @() + + # Add DistinguishedName as it won't be passed as an argument, but we want to get the DN in Set + $PSBoundParameters['DistinguishedName'] = $getTargetResourceResult['DistinguishedName'] + + # Convert EffectiveTime to DateTime object for comparison + $PSBoundParameters['EffectiveTime'] = [DateTime]::Parse($EffectiveTime) + $getTargetResourceResult['EffectiveTime'] = [DateTime]::Parse($getTargetResourceResult.EffectiveTime) + + foreach ($parameter in $PSBoundParameters.Keys) + { + if ($PSBoundParameters.$parameter -eq $getTargetResourceResult.$parameter) + { + # Check if parameter is in compliance + $compareTargetResource += [pscustomobject] @{ + Parameter = $parameter + Expected = $PSBoundParameters.$parameter + Actual = $getTargetResourceResult.$parameter + Pass = $true + } + } + # Need to check if parameter is part of schema, otherwise ignore all other parameters like verbose + elseif ($getTargetResourceResult.ContainsKey($parameter)) + { + <# + We are out of compliance if we get here + $PSBoundParameters.$parameter -ne $getTargetResourceResult.$parameter + #> + $compareTargetResource += [pscustomobject] @{ + Parameter = $parameter + Expected = $PSBoundParameters.$parameter + Actual = $getTargetResourceResult.$parameter + Pass = $false + } + } + } #end foreach PSBoundParameter + + return $compareTargetResource +} + +<# + .SYNOPSIS + Checks permissions to see if the user or computer has domain admin permissions. + + .DESCRIPTION + DSC Resources run under the SYSTEM user context and there is no Credential parameter + to pass to the KDSRootKey powershell commands. For this reason, we need to check + permissions manually, otherwise we get back empty results with no error. One must use + PsDscRunAsCredential or run this resource on the domain controller + + .PARAMETER User + The user to check permissions against + + .NOTES + Get-KdsRootKey will return $null instead of a permission error if it can't retrieve the keys + so we need manually check +#> +function Assert-HasDomainAdminRights +{ + [CmdletBinding()] + [OutputType([System.Boolean])] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.Security.Principal.WindowsIdentity] + $User + ) + + $windowsPrincipal = New-Object -TypeName 'System.Security.Principal.WindowsPrincipal' -ArgumentList @($User) + $osInfo = Get-CimInstance -ClassName Win32_OperatingSystem + + Write-Verbose -Message ($script:localizedData.CheckingDomainAdminUserRights -f $User.Name) + Write-Verbose -Message ($script:localizedData.CheckingDomainAdminComputerRights -f $osInfo.CSName, $osInfo.ProductType) + + return $windowsPrincipal.IsInRole("Domain Admins") -or + $windowsPrincipal.IsInRole("Enterprise Admins") -or + $osInfo.ProductType -eq 2 +} + +<# + .SYNOPSIS + Returns a string with the Distinguished Name of the root domain. + + .DESCRIPTION + If you have a domain with sub-domains, this will return the root domain name. For example, + if you had a domain contoso.com and a sub domain of fake.contoso.com, it would return + contoso.com. + + This is used to get the Forest level root domain name. The KDS Root Key is created at the forest + level and this is used to determine it's distinguished name +#> +function Get-ADRootDomainDN +{ + [CmdletBinding()] + [OutputType([System.String])] + param () + + $rootDomainDN = (New-Object -TypeName System.DirectoryServices.DirectoryEntry('LDAP://RootDSE')).Get('rootDomainNamingContext') + Write-Verbose -Message ($script:localizedData.RetrievedRootDomainDN -f $rootDomainDN) + return $rootDomainDN +} + +Export-ModuleMember *-TargetResource diff --git a/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_ADKDSKey/MSFT_ADKDSKey.schema.mof b/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_ADKDSKey/MSFT_ADKDSKey.schema.mof new file mode 100644 index 0000000..50ea5f4 --- /dev/null +++ b/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_ADKDSKey/MSFT_ADKDSKey.schema.mof @@ -0,0 +1,11 @@ +[ClassVersion("1.0.1.0"), FriendlyName("ADKDSKey")] +class MSFT_ADKDSKey : OMI_BaseResource +{ + [Key, Description("Specifies the Effective time when a KDS root key can be used. There is a 10 hour minimum from creation date to allow active directory to properly replicate across all domain controllers. For this reason, the date must be set in the future for creation. While this parameter accepts a string, it will be converted into a DateTime object. This will also try to take into account cultural settings. Example: '05/01/1999 13:00 using default or 'en-US' culture would be May 1st, but using 'de-DE' culture would be 5th of January. The culture is automatically pulled from the operating system and this can be checked using 'Get-Culture'.")] String EffectiveTime; + [Write, Description("Specifies if this KDS Root Key should be present or absent. Default value is 'Present'."), ValueMap{"Present","Absent"}, Values{"Present","Absent"}] String Ensure; + [Write, Description("This option will allow you to create a KDS root key if EffectiveTime is set in the past. This may cause issues if you are creating a Group Managed Service Account right after you create the KDS Root Key. In order to get around this, you must create the KDS Root Key using a date in the past. This should be used at your own risk and should only be used in lab environments.")] Boolean AllowUnsafeEffectiveTime; + [Write, Description("This option will allow you to remove a KDS root key if there is only one key left. It should not break your Group Managed Service Accounts (gMSA), but if the gMSA password expires and it needs to request a new password, it will not be able to generate a new password until a new KDS Root Key is installed and ready for use. Because of this, the last KDS Root Key will not be removed unless this option is specified.")] Boolean ForceRemove; + [Read, Description("Returns the Distinguished Name (DN) of the KDS root key. The KDS Root Key is stored in 'CN=Master Root Keys,CN=Group Key Distribution Service,CN=Services,CN=Configuration' at the Forest level. This is also why replication needs 10 hours to occur before using the KDS Root Key as a safety measure.")] String DistinguishedName; + [Read, Description("Returns the Creation date and time of the KDS root key for informational purposes.")] DateTime CreationTime; + [Read, Description("Returns the KeyID of the KDS root key. This is the Common Name (CN) within Active Directory and is required to build the Distinguished Name.")] String KeyId; +}; diff --git a/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_ADKDSKey/README.md b/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_ADKDSKey/README.md new file mode 100644 index 0000000..fe95102 --- /dev/null +++ b/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_ADKDSKey/README.md @@ -0,0 +1,7 @@ +# Description + +The ADKDSKey DSC resource will manage KDS Root Keys within Active Directory. The KDS root keys are used to begin generating Group Managed Service Account (gMSA) passwords. + +## Requirements + +* Target machine must be running Windows Server 2008 R2 or later. diff --git a/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_ADKDSKey/en-US/MSFT_ADKDSKey.strings.psd1 b/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_ADKDSKey/en-US/MSFT_ADKDSKey.strings.psd1 new file mode 100644 index 0000000..98ddadc --- /dev/null +++ b/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_ADKDSKey/en-US/MSFT_ADKDSKey.strings.psd1 @@ -0,0 +1,25 @@ + +# culture='en-US' +ConvertFrom-StringData @' + RetrievingKDSRootKey = Retrieving KDS Root Key with effective date of '{0}'. (KDSK0001) + RetrievingKDSRootKeyError = There was an error retrieving the KDS Root Key with effective date of '{0}'. (KDSK0002) + AddingKDSRootKey = Creating KDS Root Key with the effective date of '{0}'. (KDSK0003) + AddingKDSRootKeyDateInPast = Effective date is in the past and the 'AllowUnsafeEffectiveTime' is set to Enabled. Adding KDS Root Key with the effective date of '{0}', overriding 10 hour safety measure for domain controller replication. (KDSK0004) + AddingKDSRootKeyError = Effective date of '{0}' is in the past and 'AllowUnsafeEffectiveTime' was not specified so the KDS Root Key will NOT be created. (KDSK0005) + KDSRootKeyAddError = There was an error when trying to Add the KDS Root Key with the effective date of '{0}'. (KDSK0006) + KDSRootKeyRemoveError = There was an error when trying to Remove the KDS Root Key with the effective date of '{0}'. (KDSK0007) + FoundKDSRootKeySameEffectiveTime = Found more than one KDS Root Keys with the same effective time, please ensure that only one KDS key exists with the effective time of '{0}'. (KDSK0008) + FoundKDSRootKeyMultiple = Found more than one KDS Root Keys. This shouldn't be an issue, but having only one key per domain is recommended. (KDSK0009) + FoundKDSRootKey = Found KDS Root Key with the effective date of '{0}'. (KDSK0010) + NotEnoughKDSRootKeysPresent = The KDS Root Key with effective date of '{0}' is the only key that exists. Please ensure a key exists if there are existing 'Group Managed Service Accounts (gMSAs)' present. (KDSK0011) + NotEnoughKDSRootKeysPresentNoForce = There is only one KDS Root Key left and the 'ForceRemove' parameter no set; therefore, the KDS Root Key with effective date of '{0}' will not be removed. (KDSK0012) + RemovingKDSRootKey = Removing the KDS Root Key with effective date '{0}'. (KDSK0013) + KDSRootKeyNotInDesiredState = KDS Root Key with the effective date of '{0}' is NOT in the desired state. (KDSK0014) + KDSRootKeyInDesiredState = KDS Root Key with the effective date of '{0}' is in the desired state. (KDSK0015) + NotDesiredPropertyState = The parameter of '{0}' for the KDS Root Key with the effective date of '{1}' is incorrect. Expected '{2}', actual '{3}'. (KDSK0016) + IncorrectPermissions = The DSC resource is running under the context of '{0}' and doesn't have 'Domain Admin' permissions. This resource needs to run as a Domain Admin or on a Domain Controller. (KDSK0017) + EffectiveTimeInvalid = The EffectiveTime of '{0}' is invalid. Please ensure that the date and time is parsable using DateTime. (KDSK0018) + CheckingDomainAdminUserRights = Checking if the user '{0}' has valid Domain Admin permissions. (KDSK0019) + CheckingDomainAdminComputerRights = Checking if the node '{0}' is a Domain Controller. The node has a product type of '{1}'. If the product type is 2, then it is a domain controller. (KDSK0020) + RetrievedRootDomainDN = Retrieved the root domain distinguished name of '{0}'. (KDSK0021) +'@ diff --git a/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_ADKDSKey/en-US/about_ADKDSKey.help.txt b/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_ADKDSKey/en-US/about_ADKDSKey.help.txt new file mode 100644 index 0000000..9d72ef3 --- /dev/null +++ b/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_ADKDSKey/en-US/about_ADKDSKey.help.txt @@ -0,0 +1,104 @@ +.NAME + ADKDSKey + +.DESCRIPTION + The ADKDSKey DSC resource will manage KDS Root Keys within Active Directory. The KDS root keys are used to begin generating Group Managed Service Account (gMSA) passwords. + + ## Requirements + + * Target machine must be running Windows Server 2008 R2 or later. + +.PARAMETER EffectiveTime + Key - String + Specifies the Effective time when a KDS root key can be used. There is a 10 hour minimum from creation date to allow active directory to properly replicate across all domain controllers. For this reason, the date must be set in the future for creation. While this parameter accepts a string, it will be converted into a DateTime object. This will also try to take into account cultural settings. Example: '05/01/1999 13:00 using default or 'en-US' culture would be May 1st, but using 'de-DE' culture would be 5th of January. The culture is automatically pulled from the operating system and this can be checked using 'Get-Culture'. + +.PARAMETER Ensure + Write - String + Allowed values: Present, Absent + Specifies if this KDS Root Key should be present or absent. Default value is 'Present'. + +.PARAMETER AllowUnsafeEffectiveTime + Write - Boolean + This option will allow you to create a KDS root key if EffectiveTime is set in the past. This may cause issues if you are creating a Group Managed Service Account right after you create the KDS Root Key. In order to get around this, you must create the KDS Root Key using a date in the past. This should be used at your own risk and should only be used in lab environments. + +.PARAMETER ForceRemove + Write - Boolean + This option will allow you to remove a KDS root key if there is only one key left. It should not break your Group Managed Service Accounts (gMSA), but if the gMSA password expires and it needs to request a new password, it will not be able to generate a new password until a new KDS Root Key is installed and ready for use. Because of this, the last KDS Root Key will not be removed unless this option is specified. + +.PARAMETER DistinguishedName + Read - String + Returns the Distinguished Name (DN) of the KDS root key. The KDS Root Key is stored in 'CN=Master Root Keys,CN=Group Key Distribution Service,CN=Services,CN=Configuration' at the Forest level. This is also why replication needs 10 hours to occur before using the KDS Root Key as a safety measure. + +.PARAMETER CreationTime + Read - DateTime + Returns the Creation date and time of the KDS root key for informational purposes. + +.PARAMETER KeyId + Read - String + Returns the KeyID of the KDS root key. This is the Common Name (CN) within Active Directory and is required to build the Distinguished Name. + +.EXAMPLE 1 + +This configuration will create a KDS root key. If the date is set to a time +slightly ahead in the future, the key won't be usable for at least 10 hours +from the creation time. + +Configuration ADKDSKey_CreateKDSRootKey_Config +{ + Import-DscResource -Module ActiveDirectoryDsc + + Node localhost + { + ADKDSKey 'ExampleKDSRootKey' + { + Ensure = 'Present' + EffectiveTime = '1/1/2030 13:00' + # Date must be set to at time in the future + } + } +} + +.EXAMPLE 2 + +This configuration will create a KDS root key in the past. This will allow +the key to be used right away, but if all the domain controllers haven't +replicated yet, there may be issues when retrieving the gMSA password. +Use with caution + +Configuration ADKDSKey_CreateKDSRootKeyInPast_Config +{ + Import-DscResource -Module ActiveDirectoryDsc + + Node localhost + { + ADKDSKey 'ExampleKDSRootKeyInPast' + { + Ensure = 'Present' + EffectiveTime = '1/1/1999 13:00' + AllowUnsafeEffectiveTime = $true # Use with caution + } + } +} + +.EXAMPLE 3 + +This configuration will remove the last KDS root key. Use with caution. +If gMSAs are installed on the network, they will not be able to reset +their passwords and it may cause services to fail. + +Configuration ADKDSKey_CreateKDSRootKeyRemoveLastKey_Config +{ + Import-DscResource -Module ActiveDirectoryDsc + + Node localhost + { + ADKDSKey 'ExampleKDSRootKeyForceRemove' + { + Ensure = 'Absent' + EffectiveTime = '1/1/2030 13:00' + ForceRemove = $true # This will allow you to remove the key if it's the last one + } + } +} + + diff --git a/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_ADManagedServiceAccount/MSFT_ADManagedServiceAccount.psm1 b/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_ADManagedServiceAccount/MSFT_ADManagedServiceAccount.psm1 new file mode 100644 index 0000000..4526ee7 --- /dev/null +++ b/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_ADManagedServiceAccount/MSFT_ADManagedServiceAccount.psm1 @@ -0,0 +1,733 @@ +$script:resourceModulePath = Split-Path -Path (Split-Path -Path $PSScriptRoot -Parent) -Parent +$script:modulesFolderPath = Join-Path -Path $script:resourceModulePath -ChildPath 'Modules' + +$script:localizationModulePath = Join-Path -Path $script:modulesFolderPath -ChildPath 'ActiveDirectoryDsc.Common' +Import-Module -Name (Join-Path -Path $script:localizationModulePath -ChildPath 'ActiveDirectoryDsc.Common.psm1') + +$script:localizedData = Get-LocalizedData -ResourceName 'MSFT_ADManagedServiceAccount' + +$script:errorCodeKdsRootKeyNotFound = -2146893811 + +<# + .SYNOPSIS + Returns the current state of an Active Directory managed service account. + + .PARAMETER ServiceAccountName + Specifies the Security Account Manager (SAM) account name of the managed service account (ldapDisplayName + 'sAMAccountName'). To be compatible with older operating systems, create a SAM account name that is 20 characters + or less. Once created, the user's SamAccountName and CN cannot be changed. + + .PARAMETER AccountType + The type of managed service account. Standalone will create a Standalone Managed Service Account (sMSA) and + Group will create a Group Managed Service Account (gMSA). + + .PARAMETER Credential + Specifies the user account credentials to use to perform this task. + This is only required if not executing the task on a domain controller or using the DomainController parameter. + + .PARAMETER DomainController + Specifies the Active Directory Domain Controller instance to use to perform the task. + This is only required if not executing the task on a domain controller. + + .PARAMETER MembershipAttribute + Active Directory attribute used to perform membership operations for Group Managed Service Accounts (gMSAs). + If not specified, this value defaults to SamAccountName. Only used when 'Group' is selected for 'AccountType'. + + .NOTES + Used Functions: + Name | Module + ------------------------------|-------------------------- + Get-ADObject | ActiveDirectory + Get-ADServiceAccount | ActiveDirectory + Assert-Module | ActiveDirectoryDsc.Common + Get-ADCommonParameters | ActiveDirectoryDsc.Common + Get-ADObjectParentDN | ActiveDirectoryDsc.Common + New-InvalidOperationException | ActiveDirectoryDsc.Common +#> +function Get-TargetResource +{ + [CmdletBinding()] + [OutputType([System.Collections.Hashtable])] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $ServiceAccountName, + + [Parameter(Mandatory = $true)] + [ValidateSet('Group', 'Standalone')] + [System.String] + $AccountType, + + [Parameter()] + [ValidateNotNull()] + [System.Management.Automation.PSCredential] + [System.Management.Automation.CredentialAttribute()] + $Credential, + + [Parameter()] + [ValidateNotNullOrEmpty()] + [System.String] + $DomainController, + + [Parameter()] + [ValidateSet('SamAccountName', 'DistinguishedName', 'ObjectSid', 'ObjectGUID')] + [System.String] + $MembershipAttribute = 'SamAccountName' + ) + + Assert-Module -ModuleName 'ActiveDirectory' + $adServiceAccountParameters = Get-ADCommonParameters @PSBoundParameters + + Write-Verbose -Message ($script:localizedData.RetrievingManagedServiceAccountMessage -f + $ServiceAccountName) + + try + { + $adServiceAccount = Get-ADServiceAccount @adServiceAccountParameters -Properties @( + 'DistinguishedName' + 'Description' + 'DisplayName' + 'ObjectClass' + 'Enabled' + 'PrincipalsAllowedToRetrieveManagedPassword' + 'KerberosEncryptionType' + ) + } + + catch [Microsoft.ActiveDirectory.Management.ADIdentityNotFoundException] + { + Write-Verbose -Message ($script:localizedData.ManagedServiceAccountNotFoundMessage -f + $AccountType, $ServiceAccountName) + $adServiceAccount = $null + } + catch + { + $errorMessage = $script:localizedData.RetrievingManagedServiceAccountError -f $ServiceAccountName + New-InvalidOperationException -Message $errorMessage -ErrorRecord $_ + } + + if ($adServiceAccount) + { + # Resource exists + $managedPasswordPrincipals = @() + + if ($adServiceAccount.ObjectClass -eq 'msDS-ManagedServiceAccount') + { + $existingAccountType = 'Standalone' + } + else + { + $existingAccountType = 'Group' + + Write-Verbose -Message ($script:localizedData.RetrievingManagedPasswordPrincipalsMessage -f + $MembershipAttribute) + + foreach ($identity in $adServiceAccount.PrincipalsAllowedToRetrieveManagedPassword) + { + try + { + $principal = (Get-ADObject -Identity $identity -Properties $MembershipAttribute).$MembershipAttribute + } + catch [Microsoft.ActiveDirectory.Management.ADIdentityNotFoundException] + { + # Add unresolved SID as principal if the identity could not be found + $principal = $identity + } + catch + { + $errorMessage = $script:localizedData.RetrievingManagedPasswordPrincipalsError -f $identity + New-InvalidOperationException -Message $errorMessage -ErrorRecord $_ + } + + $managedPasswordPrincipals += $principal + } + } + + $targetResource = @{ + ServiceAccountName = $ServiceAccountName + AccountType = $existingAccountType + Path = Get-ADObjectParentDN -DN $adServiceAccount.DistinguishedName + Description = $adServiceAccount.Description + DisplayName = $adServiceAccount.DisplayName + DistinguishedName = $adServiceAccount.DistinguishedName + Enabled = $adServiceAccount.Enabled + KerberosEncryptionType = $adServiceAccount.KerberosEncryptionType -split (', ') + ManagedPasswordPrincipals = $managedPasswordPrincipals + MembershipAttribute = $MembershipAttribute + Ensure = 'Present' + } + } + else + { + # Resource does not exist + $targetResource = @{ + ServiceAccountName = $ServiceAccountName + AccountType = $AccountType + Path = $null + Description = $null + DisplayName = $null + DistinguishedName = $null + Enabled = $false + KerberosEncryptionType = @() + ManagedPasswordPrincipals = @() + MembershipAttribute = $MembershipAttribute + Ensure = 'Absent' + } + } + + return $targetResource +} #end function Get-TargetResource + +<# + .SYNOPSIS + Tests if an Active Directory managed service account is in the desired state. + + .PARAMETER ServiceAccountName + Specifies the Security Account Manager (SAM) account name of the managed service account (ldapDisplayName + 'sAMAccountName'). To be compatible with older operating systems, create a SAM account name that is 20 + characters or less. Once created, the user's SamAccountName and CN cannot be changed. + + .PARAMETER AccountType + The type of managed service account. Standalone will create a Standalone Managed Service Account (sMSA) and + Group will create a Group Managed Service Account (gMSA). + + .PARAMETER Credential + Specifies the user account credentials to use to perform this task. + This is only required if not executing the task on a domain controller or using the DomainController parameter. + + .PARAMETER Description + Specifies the description of the account (ldapDisplayName 'description'). + + .PARAMETER DisplayName + Specifies the display name of the account (ldapDisplayName 'displayName'). + + .PARAMETER DomainController + Specifies the Active Directory Domain Controller instance to use to perform the task. + This is only required if not executing the task on a domain controller. + + .PARAMETER Ensure + Specifies whether the user account is created or deleted. If not specified, this value defaults to Present. + + .PARAMETER KerberosEncryptionType + Specifies which Kerberos encryption types the account supports when creating service tickets. + This value sets the encryption types supported flags of the Active Directory msDS-SupportedEncryptionTypes + attribute. + + .PARAMETER ManagedPasswordPrincipals + Specifies the membership policy for systems which can use a group managed service account. (ldapDisplayName + 'msDS-GroupMSAMembership'). Only used when 'Group' is selected for 'AccountType'. + + .PARAMETER MembershipAttribute + Active Directory attribute used to perform membership operations for Group Managed Service Accounts (gMSAs). + If not specified, this value defaults to SamAccountName. Only used when 'Group' is selected for 'AccountType'. + + .PARAMETER Path + Specifies the X.500 path of the Organizational Unit (OU) or container where the new account is created. + Specified as a Distinguished Name (DN). + + .NOTES + Used Functions: + Name | Module + ------------------------------|-------------------------- + Compare-ResourcePropertyState | ActiveDirectoryDsc.Common +#> +function Test-TargetResource +{ + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingPlainTextForPassword', "", + Justification = 'False positive on ManagedPasswordPrincipals')] + [CmdletBinding()] + [OutputType([System.Boolean])] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $ServiceAccountName, + + [Parameter(Mandatory = $true)] + [ValidateSet('Group', 'Standalone')] + [System.String] + $AccountType, + + [Parameter()] + [ValidateNotNull()] + [System.Management.Automation.PSCredential] + [System.Management.Automation.CredentialAttribute()] + $Credential, + + [Parameter()] + [System.String] + $Description, + + [Parameter()] + [System.String] + $DisplayName, + + [Parameter()] + [ValidateNotNullOrEmpty()] + [System.String] + $DomainController, + + [Parameter()] + [ValidateSet('Present', 'Absent')] + [System.String] + $Ensure = 'Present', + + [Parameter()] + [ValidateSet('None', 'RC4', 'AES128', 'AES256')] + [System.String[]] + $KerberosEncryptionType, + + [Parameter()] + [System.String[]] + $ManagedPasswordPrincipals, + + [Parameter()] + [ValidateSet('SamAccountName', 'DistinguishedName', 'ObjectSid', 'ObjectGUID')] + [System.String] + $MembershipAttribute = 'SamAccountName', + + [Parameter()] + [System.String] + $Path + ) + + # Need to set these parameters to compare if users are using the default parameter values + [HashTable] $parameters = $PSBoundParameters + $parameters['MembershipAttribute'] = $MembershipAttribute + + $getTargetResourceParameters = @{ + ServiceAccountName = $ServiceAccountName + AccountType = $AccountType + DomainController = $DomainController + MembershipAttribute = $MembershipAttribute + } + + @($getTargetResourceParameters.Keys) | + ForEach-Object { + if (-not $parameters.ContainsKey($_)) + { + $getTargetResourceParameters.Remove($_) + } + } + + $getTargetResourceResult = Get-TargetResource @getTargetResourceParameters + + if ($getTargetResourceResult.Ensure -eq 'Present') + { + # Resource exists + if ($Ensure -eq 'Present') + { + # Resource should exist + $propertiesNotInDesiredState = ( + Compare-ResourcePropertyState -CurrentValues $getTargetResourceResult -DesiredValues $parameters ` + -IgnoreProperties 'DomainController', 'Credential' | Where-Object -Property InDesiredState -eq $false) + + if ($propertiesNotInDesiredState) + { + $inDesiredState = $false + } + else + { + # Resource is in desired state + Write-Verbose -Message ($script:localizedData.ManagedServiceAccountInDesiredStateMessage -f + $AccountType, $ServiceAccountName) + $inDesiredState = $true + } + } + else + { + # Resource should not exist + Write-Verbose -Message ($script:localizedData.ResourceExistsButShouldNotMessage -f + $AccountType, $ServiceAccountName) + $inDesiredState = $false + } + } + else + { + # Resource does not exist + if ($Ensure -eq 'Present') + { + # Resource should exist + Write-Verbose -Message ($script:localizedData.ResourceDoesNotExistButShouldMessage -f + $AccountType, $ServiceAccountName) + $inDesiredState = $false + } + else + { + # Resource should not exist + Write-Verbose -Message ($script:localizedData.ManagedServiceAccountInDesiredStateMessage -f + $AccountType, $ServiceAccountName) + $inDesiredState = $true + } + } + + $inDesiredState +} #end function Test-TargetResource + +<# + .SYNOPSIS + Sets the state of an Active Directory managed service account. + + .PARAMETER ServiceAccountName + Specifies the Security Account Manager (SAM) account name of the managed service account (ldapDisplayName + 'sAMAccountName'). To be compatible with older operating systems, create a SAM account name that is 20 + characters or less. Once created, the user's SamAccountName and CN cannot be changed. + + .PARAMETER AccountType + The type of managed service account. Standalone will create a Standalone Managed Service Account (sMSA) and + Group will create a Group Managed Service Account (gMSA). + + .PARAMETER Credential + Specifies the user account credentials to use to perform this task. + This is only required if not executing the task on a domain controller or using the DomainController parameter. + + .PARAMETER Description + Specifies the description of the account (ldapDisplayName 'description'). + + .PARAMETER DisplayName + Specifies the display name of the account (ldapDisplayName 'displayName'). + + .PARAMETER DomainController + Specifies the Active Directory Domain Controller instance to use to perform the task. + This is only required if not executing the task on a domain controller. + + .PARAMETER Ensure + Specifies whether the user account is created or deleted. If not specified, this value defaults to Present. + + .PARAMETER KerberosEncryptionType + Specifies which Kerberos encryption types the account supports when creating service tickets. + This value sets the encryption types supported flags of the Active Directory msDS-SupportedEncryptionTypes + attribute. + + .PARAMETER ManagedPasswordPrincipals + Specifies the membership policy for systems which can use a group managed service account. (ldapDisplayName + 'msDS-GroupMSAMembership'). Only used when 'Group' is selected for 'AccountType'. + + .PARAMETER MembershipAttribute + Active Directory attribute used to perform membership operations for Group Managed Service Accounts (gMSAs). + If not specified, this value defaults to SamAccountName. Only used when 'Group' is selected for 'AccountType'. + + .PARAMETER Path + Specifies the X.500 path of the Organizational Unit (OU) or container where the new account is created. + Specified as a Distinguished Name (DN). + + .NOTES + Used Functions: + Name | Module + ------------------------------|-------------------------- + Get-ADDomain | ActiveDirectory + Move-ADObject | ActiveDirectory + New-ADServiceAccount | ActiveDirectory + Remove-ADServiceAccount | ActiveDirectory + Set-ADServiceAccount | ActiveDirectory + Compare-ResourcePropertyState | ActiveDirectoryDsc.Common + Get-ADCommonParameters | ActiveDirectoryDsc.Common + Get-DomainName | ActiveDirectoryDsc.Common + New-InvalidOperationException | ActiveDirectoryDsc.Common +#> + +function Set-TargetResource +{ + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingPlainTextForPassword', "", + Justification = 'False positive on ManagedPasswordPrincipals')] + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $ServiceAccountName, + + [Parameter(Mandatory = $true)] + [ValidateSet('Group', 'Standalone')] + [System.String] + $AccountType, + + [Parameter()] + [ValidateNotNull()] + [System.Management.Automation.PSCredential] + [System.Management.Automation.CredentialAttribute()] + $Credential, + + [Parameter()] + [System.String] + $Description, + + [Parameter()] + [System.String] + $DisplayName, + + [Parameter()] + [ValidateNotNullOrEmpty()] + [System.String] + $DomainController, + + [Parameter()] + [ValidateSet('Present', 'Absent')] + [System.String] + $Ensure = 'Present', + + [Parameter()] + [ValidateSet('None', 'RC4', 'AES128', 'AES256')] + [System.String[]] + $KerberosEncryptionType, + + [Parameter()] + [System.String[]] + $ManagedPasswordPrincipals, + + [Parameter()] + [ValidateSet('SamAccountName', 'DistinguishedName', 'ObjectSid', 'ObjectGUID')] + [System.String] + $MembershipAttribute = 'SamAccountName', + + [Parameter()] + [ValidateNotNullOrEmpty()] + [System.String] + $Path + ) + + # Need to set these to compare if not specified since user is using defaults + [HashTable] $parameters = $PSBoundParameters + $parameters['MembershipAttribute'] = $MembershipAttribute + + $adServiceAccountParameters = Get-ADCommonParameters @parameters + + $getTargetResourceParameters = @{ + ServiceAccountName = $ServiceAccountName + AccountType = $AccountType + DomainController = $DomainController + MembershipAttribute = $MembershipAttribute + } + + @($getTargetResourceParameters.Keys) | + ForEach-Object { + if (-not $parameters.ContainsKey($_)) + { + $getTargetResourceParameters.Remove($_) + } + } + + $getTargetResourceResult = Get-TargetResource @getTargetResourceParameters + + if ($Ensure -eq 'Present') + { + # Resource should be present + if ($getTargetResourceResult.Ensure -eq 'Present') + { + # Resource is present + $createNewAdServiceAccount = $false + $propertiesNotInDesiredState = ( + Compare-ResourcePropertyState -CurrentValues $getTargetResourceResult -DesiredValues $parameters ` + -IgnoreProperties 'DomainController', 'Credential' | Where-Object -Property InDesiredState -eq $false) + if ($propertiesNotInDesiredState) + { + if ($propertiesNotInDesiredState.ParameterName -contains 'AccountType') + { + # AccountType has changed, so the account needs recreating + Write-Verbose -Message ($script:localizedData.RecreatingManagedServiceAccountMessage -f + $AccountType, $ServiceAccountName) + try + { + Remove-ADServiceAccount @adServiceAccountParameters -Confirm:$false + } + catch + { + $errorMessage = ($script:localizedData.RemovingManagedServiceAccountError -f + $AccountType, $ServiceAccountName) + New-InvalidOperationException -Message $errorMessage -ErrorRecord $_ + } + + $createNewAdServiceAccount = $true + } + else + { + $setServiceAccountParameters = $adServiceAccountParameters.Clone() + $setAdServiceAccountRequired = $false + $moveAdServiceAccountRequired = $false + + foreach ($property in $propertiesNotInDesiredState) + { + if ($property.ParameterName -eq 'Path') + { + # The path has changed, so the account needs moving, but not until after any other changes + $moveAdServiceAccountRequired = $true + } + else + { + $setAdServiceAccountRequired = $true + + Write-Verbose -Message ($script:localizedData.UpdatingManagedServiceAccountPropertyMessage -f + $AccountType, $ServiceAccountName, $property.ParameterName, ($property.Expected -join ', ')) + + if ($property.ParameterName -eq 'ManagedPasswordPrincipals' -and $AccountType -eq 'Group') + { + $setServiceAccountParameters.Add('PrincipalsAllowedToRetrieveManagedPassword', + $ManagedPasswordPrincipals) + } + else + { + $SetServiceAccountParameters.Add($property.ParameterName, $property.Expected) + } + } + } + + if ($setAdServiceAccountRequired) + { + try + { + Set-ADServiceAccount @setServiceAccountParameters + } + catch + { + $errorMessage = ($script:localizedData.SettingManagedServiceAccountError -f + $AccountType, $ServiceAccountName) + New-InvalidOperationException -Message $errorMessage -ErrorRecord $_ + } + } + + if ($moveAdServiceAccountRequired) + { + Write-Verbose -Message ($script:localizedData.MovingManagedServiceAccountMessage -f + $AccountType, $ServiceAccountName, $getTargetResourceResult.Path, $Path) + $moveADObjectParameters = $adServiceAccountParameters.Clone() + $moveADObjectParameters.Identity = $getTargetResourceResult.DistinguishedName + try + { + Move-ADObject @moveADObjectParameters -TargetPath $Path + } + catch + { + $errorMessage = ($script:localizedData.MovingManagedServiceAccountError -f + $AccountType, $ServiceAccountName, $getTargetResourceResult.Path, $Path) + New-InvalidOperationException -Message $errorMessage -ErrorRecord $_ + } + } + } + } + } + else + { + # Resource is absent + $createNewAdServiceAccount = $true + } + + if ($createNewAdServiceAccount) + { + if (-not $parameters.ContainsKey('Path')) + { + # Get default MSA path as one has not been specified + try + { + $domainDistinguishedName = (Get-ADDomain).DistinguishedName + } + catch + { + $errorMessage = $script:localizedData.GettingADDomainError + New-InvalidOperationException -Message $errorMessage -ErrorRecord $_ + } + + $messagePath = "CN=Managed Service Accounts,$domainDistinguishedName" + } + else + { + $messagePath = $Path + } + + Write-Verbose -Message ($script:localizedData.AddingManagedServiceAccountMessage -f + $AccountType, $ServiceAccountName, $messagePath) + + $newAdServiceAccountParameters = Get-ADCommonParameters @parameters -UseNameParameter + + if ($parameters.ContainsKey('Description')) + { + $newAdServiceAccountParameters.Description = $Description + } + + if ($parameters.ContainsKey('DisplayName')) + { + $newAdServiceAccountParameters.DisplayName = $DisplayName + } + + if ($parameters.ContainsKey('Path')) + { + $newAdServiceAccountParameters.Path = $Path + } + + if ( $AccountType -eq 'Standalone' ) + { + # Create standalone managed service account + $newAdServiceAccountParameters.RestrictToSingleComputer = $true + } + else + { + # Create group managed service account + $newAdServiceAccountParameters.DNSHostName = "$ServiceAccountName.$(Get-DomainName)" + + if ($parameters.ContainsKey('ManagedPasswordPrincipals')) + { + $newAdServiceAccountParameters.PrincipalsAllowedToRetrieveManagedPassword = ` + $ManagedPasswordPrincipals + } + } + + try + { + New-ADServiceAccount @newAdServiceAccountParameters + } + catch [Microsoft.ActiveDirectory.Management.ADException] + { + if ($_.Exception.ErrorCode -eq $script:errorCodeKdsRootKeyNotFound) + { + $errorMessage = ($script:localizedData.KdsRootKeyNotFoundError -f + $ServiceAccountName) + } + else + { + $errorMessage = ($script:localizedData.AddingManagedServiceAccountError -f + $AccountType, $ServiceAccountName, $messagePath) + } + + New-InvalidOperationException -Message $errorMessage -ErrorRecord $_ + } + catch + { + $errorMessage = ($script:localizedData.AddingManagedServiceAccountError -f + $AccountType, $ServiceAccountName, $messagePath) + New-InvalidOperationException -Message $errorMessage -ErrorRecord $_ + } + } + } + else + { + # Resource should be absent + if ($getTargetResourceResult.Ensure -eq 'Present') + { + # Resource is present + Write-Verbose -Message ($script:localizedData.RemovingManagedServiceAccountMessage -f + $AccountType, $ServiceAccountName) + + try + { + Remove-ADServiceAccount @adServiceAccountParameters -Confirm:$false + } + catch + { + $errorMessage = ($script:localizedData.RemovingManagedServiceAccountError -f + $AccountType, $ServiceAccountName) + New-InvalidOperationException -Message $errorMessage -ErrorRecord $_ + } + } + else + { + # Resource is absent + Write-Verbose -Message ($script:localizedData.ManagedServiceAccountInDesiredStateMessage -f + $AccountType, $ServiceAccountName) + } + } +} #end function Set-TargetResource + +Export-ModuleMember -Function *-TargetResource diff --git a/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_ADManagedServiceAccount/MSFT_ADManagedServiceAccount.schema.mof b/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_ADManagedServiceAccount/MSFT_ADManagedServiceAccount.schema.mof new file mode 100644 index 0000000..31ffe06 --- /dev/null +++ b/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_ADManagedServiceAccount/MSFT_ADManagedServiceAccount.schema.mof @@ -0,0 +1,17 @@ +[ClassVersion("1.0.1.0"), FriendlyName("ADManagedServiceAccount")] +class MSFT_ADManagedServiceAccount : OMI_BaseResource +{ + [Key, Description("Specifies the Security Account Manager (SAM) account name of the managed service account (ldapDisplayName 'sAMAccountName'). To be compatible with older operating systems, create a SAM account name that is 20 characters or less. Once created, the user's SamAccountName and CN cannot be changed.")] String ServiceAccountName; + [Required, Description("The type of managed service account. Standalone will create a Standalone Managed Service Account (sMSA) and Group will create a Group Managed Service Account (gMSA)."), ValueMap{"Group","Standalone"}, Values{"Group","Standalone"}] String AccountType; + [Write, Description("Specifies the user account credentials to use to perform this task. This is only required if not executing the task on a domain controller or using the parameter DomainController."), EmbeddedInstance("MSFT_Credential")] String Credential; + [Write, Description("Specifies the description of the account (ldapDisplayName 'description').")] String Description; + [Write, Description("Specifies the display name of the account (ldapDisplayName 'displayName').")] String DisplayName; + [Write, Description("Specifies the Active Directory Domain Controller instance to use to perform the task. This is only required if not executing the task on a domain controller.")] String DomainController; + [Write, Description("Specifies whether the user account is created or deleted. If not specified, this value defaults to Present."), ValueMap{"Present","Absent"}, Values{"Present","Absent"}] String Ensure; + [Write, Description("Specifies which Kerberos encryption types the account supports when creating service tickets. This value sets the encryption types supported flags of the Active Directory msDS-SupportedEncryptionTypes attribute."),ValueMap{"None","RC4","AES128","AES256"}, Values{"None","RC4","AES128","AES256"}] String KerberosEncryptionType[]; + [Write, Description("Specifies the membership policy for systems which can use a group managed service account. (ldapDisplayName 'msDS-GroupMSAMembership'). Only used when 'Group' is selected for 'AccountType'.")] String ManagedPasswordPrincipals[]; + [Write, Description("Active Directory attribute used to perform membership operations for Group Managed Service Accounts (gMSA). If not specified, this value defaults to SamAccountName."), ValueMap{"SamAccountName","DistinguishedName","ObjectGUID","ObjectSid"}, Values{"SamAccountName","DistinguishedName","ObjectGUID","ObjectSid"}] String MembershipAttribute; + [Write, Description("Specifies the X.500 path of the Organizational Unit (OU) or container where the new account is created. Specified as a Distinguished Name (DN).")] String Path; + [Read, Description("Returns whether the user account is enabled or disabled.")] Boolean Enabled; + [Read, Description("Returns the Distinguished Name of the Service Account.")] String DistinguishedName; +}; diff --git a/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_ADManagedServiceAccount/README.md b/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_ADManagedServiceAccount/README.md new file mode 100644 index 0000000..059ae00 --- /dev/null +++ b/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_ADManagedServiceAccount/README.md @@ -0,0 +1,9 @@ +# Description + +The ADManagedServiceAccount DSC resource will manage Single and Group Managed Service Accounts (MSAs) within Active Directory. A Managed Service Account is a managed domain account that provides automatic password management, simplified service principal name (SPN) management and the ability to delegate management to other administrators. +A Single Managed Service Account can only be used on a single computer, whereas a Group Managed Service Account can be shared across multiple computers. + +## Requirements + +* Target machine must be running Windows Server 2008 R2 or later. +* Group Managed Service Accounts need at least one Windows Server 2012 Domain Controller. diff --git a/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_ADManagedServiceAccount/en-US/MSFT_ADManagedServiceAccount.strings.psd1 b/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_ADManagedServiceAccount/en-US/MSFT_ADManagedServiceAccount.strings.psd1 new file mode 100644 index 0000000..afcc58b --- /dev/null +++ b/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_ADManagedServiceAccount/en-US/MSFT_ADManagedServiceAccount.strings.psd1 @@ -0,0 +1,22 @@ +# culture='en-US' +ConvertFrom-StringData @' + AddingManagedServiceAccountMessage = Adding {0} Account '{1}' to '{2}'. (MSA0001) + RecreatingManagedServiceAccountMessage = Recreating {0} Account '{1}'. (MSA0002) + RemovingManagedServiceAccountMessage = Removing {0} Account '{1}'. (MSA0003) + MovingManagedServiceAccountMessage = Moving {0} Account '{1}' from '{2}' to '{3}'. (MSA0004) + ManagedServiceAccountNotFoundMessage = {0} Account '{1}' was not found. (MSA0005) + RetrievingManagedServiceAccountMessage = Retrieving Account '{0}'. (MSA0006) + ManagedServiceAccountInDesiredStateMessage = {0} Account '{1}' is in the desired state. (MSA0007) + UpdatingManagedServiceAccountPropertyMessage = Updating {0} Account '{1}' property '{2}' to '{3}'. (MSA0008) + RetrievingManagedPasswordPrincipalsMessage = Retrieving Principals Allowed To Retrieve Managed Password based on '{0}' property. (MSA0009) + ResourceExistsButShouldNotMessage = {0} Account '{1}' exists but should not. (MSA0010) + ResourceDoesNotExistButShouldMessage = {0} Account '{1}' does not exist but should. (MSA0011) + AddingManagedServiceAccountError = Error adding {0} Account '{1}' to '{2}'. (MSA0012) + RemovingManagedServiceAccountError = Error removing {0} Account '{1}'. (MSA0013) + SettingManagedServiceAccountError = Error setting {0} Account '{1}'. (MSA0014) + MovingManagedServiceAccountError = Error moving {0} Account '{1}' from '{2}' to '{3}'. (MSA0015) + RetrievingManagedServiceAccountError = Error retrieving Account '{0}'. (MSA0016) + RetrievingManagedPasswordPrincipalsError = Error retrieving Principal '{0}'. (MSA0017) + GettingADDomainError = Error getting Active Directory Domain details. (MSA0018) + KdsRootKeyNotFoundError = Error adding group account '{0}'. The KDS Root Key was not found. (MSA0019) +'@ diff --git a/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_ADManagedServiceAccount/en-US/about_ADManagedServiceAccount.help.txt b/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_ADManagedServiceAccount/en-US/about_ADManagedServiceAccount.help.txt new file mode 100644 index 0000000..67bb3de --- /dev/null +++ b/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_ADManagedServiceAccount/en-US/about_ADManagedServiceAccount.help.txt @@ -0,0 +1,161 @@ +.NAME + ADManagedServiceAccount + +.DESCRIPTION + The ADManagedServiceAccount DSC resource will manage Single and Group Managed Service Accounts (MSAs) within Active Directory. A Managed Service Account is a managed domain account that provides automatic password management, simplified service principal name (SPN) management and the ability to delegate management to other administrators. + A Single Managed Service Account can only be used on a single computer, whereas a Group Managed Service Account can be shared across multiple computers. + + ## Requirements + + * Target machine must be running Windows Server 2008 R2 or later. + * Group Managed Service Accounts need at least one Windows Server 2012 Domain Controller. + +.PARAMETER ServiceAccountName + Key - String + Specifies the Security Account Manager (SAM) account name of the managed service account (ldapDisplayName 'sAMAccountName'). To be compatible with older operating systems, create a SAM account name that is 20 characters or less. Once created, the user's SamAccountName and CN cannot be changed. + +.PARAMETER AccountType + Required - String + Allowed values: Group, Standalone + The type of managed service account. Standalone will create a Standalone Managed Service Account (sMSA) and Group will create a Group Managed Service Account (gMSA). + +.PARAMETER Credential + Write - PSCredential + Specifies the user account credentials to use to perform this task. This is only required if not executing the task on a domain controller or using the parameter DomainController. + +.PARAMETER Description + Write - String + Specifies the description of the account (ldapDisplayName 'description'). + +.PARAMETER DisplayName + Write - String + Specifies the display name of the account (ldapDisplayName 'displayName'). + +.PARAMETER DomainController + Write - String + Specifies the Active Directory Domain Controller instance to use to perform the task. This is only required if not executing the task on a domain controller. + +.PARAMETER Ensure + Write - String + Allowed values: Present, Absent + Specifies whether the user account is created or deleted. If not specified, this value defaults to Present. + +.PARAMETER KerberosEncryptionType + Write - StringArray + Allowed values: None, RC4, AES128, AES256 + Specifies which Kerberos encryption types the account supports when creating service tickets. This value sets the encryption types supported flags of the Active Directory msDS-SupportedEncryptionTypes attribute. + +.PARAMETER ManagedPasswordPrincipals + Write - StringArray + Specifies the membership policy for systems which can use a group managed service account. (ldapDisplayName 'msDS-GroupMSAMembership'). Only used when 'Group' is selected for 'AccountType'. + +.PARAMETER MembershipAttribute + Write - String + Allowed values: SamAccountName, DistinguishedName, ObjectGUID, ObjectSid + Active Directory attribute used to perform membership operations for Group Managed Service Accounts (gMSA). If not specified, this value defaults to SamAccountName. + +.PARAMETER Path + Write - String + Specifies the X.500 path of the Organizational Unit (OU) or container where the new account is created. Specified as a Distinguished Name (DN). + +.PARAMETER Enabled + Read - Boolean + Returns whether the user account is enabled or disabled. + +.PARAMETER DistinguishedName + Read - String + Returns the Distinguished Name of the Service Account. + +.EXAMPLE 1 + +This configuration will create a standalone managed service account in the default 'Managed Service Accounts' +container. + +Configuration ADManagedServiceAccount_CreateManagedServiceAccount_Config +{ + Import-DscResource -Module ActiveDirectoryDsc + + Node localhost + { + ADManagedServiceAccount 'ExampleStandaloneMSA' + { + Ensure = 'Present' + ServiceAccountName = 'Service01' + AccountType = 'Standalone' + } + } +} + +.EXAMPLE 2 + +This configuration will create a group managed service account in the default 'Managed Service Accounts' +container. + +Configuration ADManagedServiceAccount_CreateGroupManagedServiceAccount_Config +{ + Import-DscResource -Module ActiveDirectoryDsc + + Node localhost + { + ADManagedServiceAccount 'ExampleGroupMSA' + { + Ensure = 'Present' + ServiceAccountName = 'Service01' + AccountType = 'Group' + } + } +} + +.EXAMPLE 3 + +This configuration will create a group managed service account with members in the default 'Managed Service +Accounts' container. + +Configuration ADManagedServiceAccount_CreateGroupManagedServiceAccountWithMembers_Config +{ + Import-DscResource -Module ActiveDirectoryDsc + + Node localhost + { + ADManagedServiceAccount 'AddingMembersUsingSamAccountName' + { + Ensure = 'Present' + ServiceAccountName = 'Service01' + AccountType = 'Group' + ManagedPasswordPrincipals = 'User01', 'Computer01$' + } + + ADManagedServiceAccount 'AddingMembersUsingDN' + { + Ensure = 'Present' + ServiceAccountName = 'Service02' + AccountType = 'Group' + ManagedPasswordPrincipals = 'CN=User01,OU=Users,DC=contoso,DC=com', 'CN=Computer01,OU=Computers,DC=contoso,DC=com' + } + } +} + +.EXAMPLE 4 + +This configuration will create a group managed service account in the specified path. + +Configuration ADManagedServiceAccount_CreateGroupManagedServiceAccountCustomPath_Config +{ + Import-DscResource -Module ActiveDirectoryDsc + + Node localhost + { + Node localhost + { + ADManagedServiceAccount 'ExampleGroupMSA' + { + Ensure = 'Present' + ServiceAccountName = 'Service01' + AccountType = 'Group' + Path = 'OU=ServiceAccounts,DC=contoso,DC=com' + } + } + } +} + + diff --git a/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_ADObjectEnabledState/MSFT_ADObjectEnabledState.psm1 b/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_ADObjectEnabledState/MSFT_ADObjectEnabledState.psm1 new file mode 100644 index 0000000..905e268 --- /dev/null +++ b/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_ADObjectEnabledState/MSFT_ADObjectEnabledState.psm1 @@ -0,0 +1,386 @@ +$script:resourceModulePath = Split-Path -Path (Split-Path -Path $PSScriptRoot -Parent) -Parent +$script:modulesFolderPath = Join-Path -Path $script:resourceModulePath -ChildPath 'Modules' + +$script:localizationModulePath = Join-Path -Path $script:modulesFolderPath -ChildPath 'ActiveDirectoryDsc.Common' +Import-Module -Name (Join-Path -Path $script:localizationModulePath -ChildPath 'ActiveDirectoryDsc.Common.psm1') + +$script:localizedData = Get-LocalizedData -ResourceName 'MSFT_ADObjectEnabledState' + +<# + .SYNOPSIS + Returns the current state of the property Enabled of an Active Directory + object. + + .PARAMETER Identity + Specifies the identity of an object that has the object class specified + in the parameter ObjectClass. When ObjectClass is set to 'Computer' then + this property can be set to either distinguished name, GUID (objectGUID), + security identifier (objectSid), or security Accounts Manager account + name (sAMAccountName). + + .PARAMETER ObjectClass + Specifies the object class. + + .PARAMETER Enabled + Specifies the value of the Enabled property. + + Not used in Get-TargetResource. + + .PARAMETER DomainController + Specifies the Active Directory Domain Services instance to connect to perform the task. + + Used by Get-ADCommonParameters and is returned as a common parameter. + + .PARAMETER Credential + Specifies the user account credentials to use to perform the task. + + Used by Get-ADCommonParameters and is returned as a common parameter. +#> +function Get-TargetResource +{ + [CmdletBinding()] + [OutputType([System.Collections.Hashtable])] + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $Identity, + + [Parameter(Mandatory = $true)] + [ValidateSet('Computer')] + [System.String] + $ObjectClass, + + [Parameter(Mandatory = $true)] + [ValidateNotNull()] + [System.Boolean] + $Enabled, + + [Parameter()] + [ValidateNotNull()] + [System.String] + $DomainController, + + [Parameter()] + [ValidateNotNull()] + [System.Management.Automation.PSCredential] + [System.Management.Automation.CredentialAttribute()] + $Credential + ) + + Assert-Module -ModuleName 'ActiveDirectory' -ImportModule + + <# + These are properties that have no corresponding property in a + Computer account object. + #> + $getTargetResourceReturnValue = @{ + Identity = $Identity + ObjectClass = $ObjectClass + Enabled = $false + DomainController = $DomainController + Credential = $Credential + } + + switch ($ObjectClass) + { + 'Computer' + { + $getADComputerResult = $null + + try + { + Write-Verbose -Message ($script:localizedData.RetrievingComputerAccount -f $Identity) + + $getADComputerParameters = Get-ADCommonParameters @PSBoundParameters + $getADComputerParameters['Properties'] = 'Enabled' + + # If the computer account is not found Get-ADComputer will throw an error. + $getADComputerResult = Get-ADComputer @getADComputerParameters + + $getTargetResourceReturnValue['Enabled'] = $getADComputerResult.Enabled + + if ($getADComputerResult.Enabled) + { + Write-Verbose -Message $script:localizedData.ComputerAccountEnabled + } + else + { + Write-Verbose -Message $script:localizedData.ComputerAccountDisabled + } + } + catch + { + $errorMessage = $script:localizedData.FailedToRetrieveComputerAccount -f $Identity + New-InvalidOperationException -Message $errorMessage -ErrorRecord $_ + } + } + } + + return $getTargetResourceReturnValue +} + +<# + .SYNOPSIS + Determines if the property Enabled of the Active Directory object is in + the desired state. + + .PARAMETER Identity + Specifies the identity of an object that has the object class specified + in the parameter ObjectClass. When ObjectClass is set to 'Computer' then + this property can be set to either distinguished name, GUID (objectGUID), + security identifier (objectSid), or security Accounts Manager account + name (sAMAccountName). + + .PARAMETER ObjectClass + Specifies the object class. + + .PARAMETER Enabled + Specifies the value of the Enabled property. + + .PARAMETER DomainController + Specifies the Active Directory Domain Services instance to connect to + perform the task. + + .PARAMETER Credential + Specifies the user account credentials to use to perform the task. +#> +function Test-TargetResource +{ + [CmdletBinding()] + [OutputType([System.Boolean])] + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $Identity, + + [Parameter(Mandatory = $true)] + [ValidateSet('Computer')] + [System.String] + $ObjectClass, + + [Parameter(Mandatory = $true)] + [ValidateNotNull()] + [System.Boolean] + $Enabled, + + [Parameter()] + [ValidateNotNull()] + [System.String] + $DomainController, + + [Parameter()] + [ValidateNotNull()] + [System.Management.Automation.PSCredential] + [System.Management.Automation.CredentialAttribute()] + $Credential + ) + + Write-Verbose -Message ( + $script:localizedData.TestConfiguration -f $Identity, $ObjectClass + ) + + $compareTargetResourceStateResult = Compare-TargetResourceState @PSBoundParameters + + if ($false -in $compareTargetResourceStateResult.InDesiredState) + { + $testTargetResourceReturnValue = $false + } + else + { + $testTargetResourceReturnValue = $true + } + + switch ($ObjectClass) + { + 'Computer' + { + if ($testTargetResourceReturnValue) + { + Write-Verbose -Message ($script:localizedData.ComputerAccountInDesiredState -f $Identity) + } + else + { + Write-Verbose -Message ($script:localizedData.ComputerAccountNotInDesiredState -f $Identity) + } + } + } + + return $testTargetResourceReturnValue +} + +<# + .SYNOPSIS + Sets the property Enabled of the Active Directory object. + + .PARAMETER Identity + Specifies the identity of an object that has the object class specified + in the parameter ObjectClass. When ObjectClass is set to 'Computer' then + this property can be set to either distinguished name, GUID (objectGUID), + security identifier (objectSid), or security Accounts Manager account + name (sAMAccountName). + + .PARAMETER ObjectClass + Specifies the object class. + + .PARAMETER Enabled + Specifies the value of the Enabled property. + + .PARAMETER DomainController + Specifies the Active Directory Domain Services instance to connect to + perform the task. + + .PARAMETER Credential + Specifies the user account credentials to use to perform the task. +#> +function Set-TargetResource +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $Identity, + + [Parameter(Mandatory = $true)] + [ValidateSet('Computer')] + [System.String] + $ObjectClass, + + [Parameter(Mandatory = $true)] + [ValidateNotNull()] + [System.Boolean] + $Enabled, + + [Parameter()] + [ValidateNotNull()] + [System.String] + $DomainController, + + [Parameter()] + [ValidateNotNull()] + [System.Management.Automation.PSCredential] + [System.Management.Automation.CredentialAttribute()] + $Credential + ) + + $compareTargetResourceStateResult = Compare-TargetResourceState @PSBoundParameters + + # Get all properties that are not in desired state. + $propertiesNotInDesiredState = $compareTargetResourceStateResult | Where-Object -FilterScript { + -not $_.InDesiredState + } + + if ($propertiesNotInDesiredState.Where( { $_.ParameterName -eq 'Enabled' })) + { + $commonParameters = Get-ADCommonParameters @PSBoundParameters + + switch ($ObjectClass) + { + 'Computer' + { + $setADComputerParameters = $commonParameters.Clone() + $setADComputerParameters['Enabled'] = $Enabled + + Set-DscADComputer -Parameters $setADComputerParameters + + if ($Enabled) + { + Write-Verbose -Message ( + $script:localizedData.ComputerAccountHasBeenEnabled -f $Identity + ) + } + else + { + Write-Verbose -Message ( + $script:localizedData.ComputerAccountHasBeenDisabled -f $Identity + ) + } + } + } + } +} + +<# + .SYNOPSIS + Compares the properties in the current state with the properties of the + desired state and returns a hashtable with the comaprison result. + + .PARAMETER Identity + Specifies the identity of an object that has the object class specified + in the parameter ObjectClass. When ObjectClass is set to 'Computer' then + this property can be set to either distinguished name, GUID (objectGUID), + security identifier (objectSid), or security Accounts Manager account + name (sAMAccountName). + + .PARAMETER ObjectClass + Specifies the object class. + + .PARAMETER Enabled + Specifies the value of the Enabled property. + + .PARAMETER DomainController + Specifies the Active Directory Domain Services instance to connect to + perform the task. + + .PARAMETER Credential + Specifies the user account credentials to use to perform the task. +#> +function Compare-TargetResourceState +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $Identity, + + [Parameter(Mandatory = $true)] + [ValidateSet('Computer')] + [System.String] + $ObjectClass, + + [Parameter(Mandatory = $true)] + [ValidateNotNull()] + [System.Boolean] + $Enabled, + + [Parameter()] + [ValidateNotNull()] + [System.String] + $DomainController, + + [Parameter()] + [ValidateNotNull()] + [System.Management.Automation.PSCredential] + [System.Management.Automation.CredentialAttribute()] + $Credential + ) + + $getTargetResourceParameters = @{ + Identity = $Identity + ObjectClass = $ObjectClass + Enabled = $Enabled + DomainController = $DomainController + Credential = $Credential + } + + # Need the @() around this to get a new array to enumerate. + @($getTargetResourceParameters.Keys) | ForEach-Object { + if (-not $PSBoundParameters.ContainsKey($_)) + { + $getTargetResourceParameters.Remove($_) + } + } + + $getTargetResourceResult = Get-TargetResource @getTargetResourceParameters + + $compareTargetResourceStateParameters = @{ + CurrentValues = $getTargetResourceResult + DesiredValues = $PSBoundParameters + Properties = @('Enabled') + } + + return Compare-ResourcePropertyState @compareTargetResourceStateParameters +} diff --git a/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_ADObjectEnabledState/MSFT_ADObjectEnabledState.schema.mof b/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_ADObjectEnabledState/MSFT_ADObjectEnabledState.schema.mof new file mode 100644 index 0000000..fa7b0b0 --- /dev/null +++ b/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_ADObjectEnabledState/MSFT_ADObjectEnabledState.schema.mof @@ -0,0 +1,9 @@ +[ClassVersion("1.0.0.0"), FriendlyName("ADObjectEnabledState")] +class MSFT_ADObjectEnabledState : OMI_BaseResource +{ + [Key, Description("Specifies the identity of an object that has the object class specified in the parameter ObjectClass. When ObjectClass is set to 'Computer' then this property can be set to either distinguished name, GUID (objectGUID), security identifier (objectSid), or security Accounts Manager account name (sAMAccountName).")] String Identity; + [Key, Description("Specifies the object class."), ValueMap{"Computer"}, Values{"Computer"}] String ObjectClass; + [Required, Description("Specifies the value of the Enabled property.")] Boolean Enabled; + [Write, Description("Specifies the Active Directory Domain Services instance to connect to perform the task.")] String DomainController; + [Write, Description("Specifies the user account credentials to use to perform the task."), EmbeddedInstance("MSFT_Credential")] String Credential; +}; diff --git a/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_ADObjectEnabledState/README.md b/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_ADObjectEnabledState/README.md new file mode 100644 index 0000000..8a6978d --- /dev/null +++ b/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_ADObjectEnabledState/README.md @@ -0,0 +1,15 @@ +# Description + +This resource enforces the property `Enabled` on the object class *Computer*. + +>This resource could support other object classes like *msDS-ManagedServiceAccount*, +>*msDS-GroupManagedServiceAccount*, and *User*. But these object classes +>are not yet supported due to that other resources already enforces the +>`Enabled` property. If this resource should support another object class, +>then it should be made so that only one resource enforces the enabled +>property. This is to prevent a potential "ping-pong" behavior if both +>resource would be used in a configuration. + +## Requirements + +* Target machine must be running Windows Server 2008 R2 or later. diff --git a/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_ADObjectEnabledState/en-US/MSFT_ADObjectEnabledState.strings.psd1 b/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_ADObjectEnabledState/en-US/MSFT_ADObjectEnabledState.strings.psd1 new file mode 100644 index 0000000..9ff22a6 --- /dev/null +++ b/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_ADObjectEnabledState/en-US/MSFT_ADObjectEnabledState.strings.psd1 @@ -0,0 +1,12 @@ +# culture="en-US" +ConvertFrom-StringData @' + RetrievingComputerAccount = Retrieving the information about the computer account '{0}' from Active Directory. (ADOES0001) + ComputerAccountEnabled = The computer account is enabled. (ADOES0002) + ComputerAccountDisabled = The computer account is disabled. (ADOES0003) + FailedToRetrieveComputerAccount = Failed to retrieve the computer account '{0}' from Active Directory. (ADOES0005) + TestConfiguration = Determining the current state of the enabled property of the object with the identity '{0}' and object class '{1}'. (ADOES0006) + ComputerAccountInDesiredState = The property Enabled of the computer account '{0}' is in the desired state. (ADOES0007) + ComputerAccountNotInDesiredState = The property Enabled of the computer account '{0}' is not in the desired state. (ADOES0008) + ComputerAccountHasBeenDisabled = The computer account '{0}' has been disabled. (ADOES0009) + ComputerAccountHasBeenEnabled = The computer account '{0}' has been enabled. (ADOES0010) +'@ diff --git a/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_ADObjectEnabledState/en-US/about_ADObjectEnabledState.help.txt b/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_ADObjectEnabledState/en-US/about_ADObjectEnabledState.help.txt new file mode 100644 index 0000000..63f94b2 --- /dev/null +++ b/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_ADObjectEnabledState/en-US/about_ADObjectEnabledState.help.txt @@ -0,0 +1,155 @@ +.NAME + ADObjectEnabledState + +.DESCRIPTION + This resource enforces the property `Enabled` on the object class *Computer*. + + >This resource could support other object classes like *msDS-ManagedServiceAccount*, + >*msDS-GroupManagedServiceAccount*, and *User*. But these object classes + >are not yet supported due to that other resources already enforces the + >`Enabled` property. If this resource should support another object class, + >then it should be made so that only one resource enforces the enabled + >property. This is to prevent a potential "ping-pong" behavior if both + >resource would be used in a configuration. + + ## Requirements + + * Target machine must be running Windows Server 2008 R2 or later. + +.PARAMETER Identity + Key - String + Specifies the identity of an object that has the object class specified in the parameter ObjectClass. When ObjectClass is set to 'Computer' then this property can be set to either distinguished name, GUID (objectGUID), security identifier (objectSid), or security Accounts Manager account name (sAMAccountName). + +.PARAMETER ObjectClass + Key - String + Allowed values: Computer + Specifies the object class. + +.PARAMETER Enabled + Required - Boolean + Specifies the value of the Enabled property. + +.PARAMETER DomainController + Write - String + Specifies the Active Directory Domain Services instance to connect to perform the task. + +.PARAMETER Credential + Write - PSCredential + Specifies the user account credentials to use to perform the task. + +.EXAMPLE 1 + +This configuration will create a computer account disabled, and +enforcing the account to be enabled. + +Configuration ADObjectEnabledState_EnabledComputerAccount_Config +{ + Import-DscResource -ModuleName ActiveDirectoryDsc + + node localhost + { + ADComputer 'CreateDisabled' + { + ComputerName = 'CLU_CNO01' + EnabledOnCreation = $false + } + + ADObjectEnabledState 'EnforceEnabledPropertyToEnabled' + { + Identity = 'CLU_CNO01' + ObjectClass = 'Computer' + Enabled = $true + + DependsOn = '[ADComputer]CreateDisabled' + } + } +} + +.EXAMPLE 2 + +This configuration will create a computer account disabled, configure +a cluster using the disabled computer account, and enforcing the +computer account to be enabled. + +Configuration ADObjectEnabledState_CreateClusterComputerAccount_Config +{ + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.Management.Automation.PSCredential] + $Credential + ) + + Import-DscResource -ModuleName ActiveDirectoryDsc + Import-DscResource -ModuleName xFailoverCluster -ModuleVersion '1.14.1' + + node localhost + { + ADComputer 'ClusterAccount' + { + ComputerName = 'CLU_CNO01' + EnabledOnCreation = $false + } + + xCluster 'CreateCluster' + { + Name = 'CLU_CNO01' + StaticIPAddress = '192.168.100.20/24' + DomainAdministratorCredential = $Credential + + DependsOn = '[ADComputer]ClusterAccount' + } + + ADObjectEnabledState 'EnforceEnabledPropertyToEnabled' + { + Identity = 'CLU_CNO01' + ObjectClass = 'Computer' + Enabled = $true + + DependsOn = '[xCluster]CreateCluster' + } + } +} + +.EXAMPLE 3 + +This configuration will configure a cluster using a pre-staged computer +account, and enforcing the pre-staged computer account to be enabled. + +Configuration ADObjectEnabledState_EnabledPrestagedClusterComputerAccount_Config +{ + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.Management.Automation.PSCredential] + $Credential + ) + + Import-DscResource -ModuleName ActiveDirectoryDsc + Import-DscResource -ModuleName xFailoverCluster -ModuleVersion '1.14.1' + + node localhost + { + xCluster 'CreateCluster' + { + Name = 'CLU_CNO01' + StaticIPAddress = '192.168.100.20/24' + DomainAdministratorCredential = $Credential + } + + ADObjectEnabledState 'EnforceEnabledPropertyToEnabled' + { + Identity = 'CLU_CNO01' + ObjectClass = 'Computer' + Enabled = $true + + DependsOn = @( + '[xCluster]CreateCluster' + ) + } + } +} + + diff --git a/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_ADObjectPermissionEntry/MSFT_ADObjectPermissionEntry.psm1 b/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_ADObjectPermissionEntry/MSFT_ADObjectPermissionEntry.psm1 new file mode 100644 index 0000000..ee1931e --- /dev/null +++ b/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_ADObjectPermissionEntry/MSFT_ADObjectPermissionEntry.psm1 @@ -0,0 +1,372 @@ +$script:resourceModulePath = Split-Path -Path (Split-Path -Path $PSScriptRoot -Parent) -Parent +$script:modulesFolderPath = Join-Path -Path $script:resourceModulePath -ChildPath 'Modules' + +$script:localizationModulePath = Join-Path -Path $script:modulesFolderPath -ChildPath 'ActiveDirectoryDsc.Common' +Import-Module -Name (Join-Path -Path $script:localizationModulePath -ChildPath 'ActiveDirectoryDsc.Common.psm1') + +$script:localizedData = Get-LocalizedData -ResourceName 'MSFT_ADObjectPermissionEntry' + +<# + .SYNOPSIS + Get the current state of the object permission entry. + + .PARAMETER Path + Active Directory path of the target object to add or remove the + permission entry, specified as a Distinguished Name. + + .PARAMETER IdentityReference + Indicates the identity of the principal for the permission entry. + + .PARAMETER AccessControlType + Indicates whether to Allow or Deny access to the target object. + + .PARAMETER ObjectType + The schema GUID of the object to which the access rule applies. + + .PARAMETER ActiveDirectorySecurityInheritance + One of the 'ActiveDirectorySecurityInheritance' enumeration values that + specifies the inheritance type of the access rule. + + .PARAMETER InheritedObjectType + The schema GUID of the child object type that can inherit this access + rule. +#> +function Get-TargetResource +{ + [CmdletBinding()] + [OutputType([System.Collections.Hashtable])] + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $Path, + + [Parameter(Mandatory = $true)] + [System.String] + $IdentityReference, + + [Parameter(Mandatory = $true)] + [ValidateSet('Allow', 'Deny')] + [System.String] + $AccessControlType, + + [Parameter(Mandatory = $true)] + [System.String] + $ObjectType, + + [Parameter(Mandatory = $true)] + [ValidateSet('All', 'Children', 'Descendents', 'None', 'SelfAndChildren')] + [System.String] + $ActiveDirectorySecurityInheritance, + + [Parameter(Mandatory = $true)] + [System.String] + $InheritedObjectType + ) + + Assert-ADPSDrive + + # Return object, by default representing an absent ace + $returnValue = @{ + Ensure = 'Absent' + Path = $Path + IdentityReference = $IdentityReference + ActiveDirectoryRights = '' + AccessControlType = $AccessControlType + ObjectType = $ObjectType + ActiveDirectorySecurityInheritance = $ActiveDirectorySecurityInheritance + InheritedObjectType = $InheritedObjectType + } + + try + { + # Get the current acl + $acl = Get-Acl -Path "AD:$Path" -ErrorAction Stop + } + catch [System.Management.Automation.ItemNotFoundException] + { + Write-Verbose -Message ($script:localizedData.ObjectPathIsAbsent -f $Path) + $acl = $null + } + catch + { + throw $_ + } + + if ($null -ne $acl) + { + foreach ($access in $acl.Access) + { + if ($access.IsInherited -eq $false) + { + <# + Check if the ace does match the parameters. If yes, the target + ace has been found, return present with the assigned rights. + #> + if ($access.IdentityReference.Value -eq $IdentityReference -and + $access.AccessControlType -eq $AccessControlType -and + $access.ObjectType.Guid -eq $ObjectType -and + $access.InheritanceType -eq $ActiveDirectorySecurityInheritance -and + $access.InheritedObjectType.Guid -eq $InheritedObjectType) + { + $returnValue['Ensure'] = 'Present' + $returnValue['ActiveDirectoryRights'] = [System.String[]] $access.ActiveDirectoryRights.ToString().Split(',').ForEach( { $_.Trim() }) + } + } + } + } + + if ($returnValue.Ensure -eq 'Present') + { + Write-Verbose -Message ($script:localizedData.ObjectPermissionEntryFound -f $Path) + } + else + { + Write-Verbose -Message ($script:localizedData.ObjectPermissionEntryNotFound -f $Path) + } + + return $returnValue +} + +<# + .SYNOPSIS + Add or remove the object permission entry. + + .PARAMETER Ensure + Indicates if the access will be added (Present) or will be removed + (Absent). Default is 'Present'. + + .PARAMETER Path + Active Directory path of the target object to add or remove the + permission entry, specified as a Distinguished Name. + + .PARAMETER IdentityReference + Indicates the identity of the principal for the permission entry. + + .PARAMETER ActiveDirectoryRights + A combination of one or more of the ActiveDirectoryRights enumeration + values that specifies the rights of the access rule. Default is + 'GenericAll'. + + .PARAMETER AccessControlType + Indicates whether to Allow or Deny access to the target object. + + .PARAMETER ObjectType + The schema GUID of the object to which the access rule applies. + + .PARAMETER ActiveDirectorySecurityInheritance + One of the 'ActiveDirectorySecurityInheritance' enumeration values that + specifies the inheritance type of the access rule. + + .PARAMETER InheritedObjectType + The schema GUID of the child object type that can inherit this access + rule. +#> +function Set-TargetResource +{ + [CmdletBinding()] + param + ( + [Parameter()] + [ValidateSet('Present', 'Absent')] + [System.String] + $Ensure = 'Present', + + [Parameter(Mandatory = $true)] + [System.String] + $Path, + + [Parameter(Mandatory = $true)] + [System.String] + $IdentityReference, + + [Parameter()] + [ValidateSet('AccessSystemSecurity', 'CreateChild', 'Delete', 'DeleteChild', 'DeleteTree', 'ExtendedRight', 'GenericAll', 'GenericExecute', 'GenericRead', 'GenericWrite', 'ListChildren', 'ListObject', 'ReadControl', 'ReadProperty', 'Self', 'Synchronize', 'WriteDacl', 'WriteOwner', 'WriteProperty')] + [System.String[]] + $ActiveDirectoryRights = 'GenericAll', + + [Parameter(Mandatory = $true)] + [ValidateSet('Allow', 'Deny')] + [System.String] + $AccessControlType, + + [Parameter(Mandatory = $true)] + [System.String] + $ObjectType, + + [Parameter(Mandatory = $true)] + [ValidateSet('All', 'Children', 'Descendents', 'None', 'SelfAndChildren')] + [System.String] + $ActiveDirectorySecurityInheritance, + + [Parameter(Mandatory = $true)] + [System.String] + $InheritedObjectType + ) + + Assert-ADPSDrive + + # Get the current acl + $acl = Get-Acl -Path "AD:$Path" + + if ($Ensure -eq 'Present') + { + Write-Verbose -Message ($script:localizedData.AddingObjectPermissionEntry -f $Path) + + $ntAccount = New-Object -TypeName 'System.Security.Principal.NTAccount' -ArgumentList $IdentityReference + + $ace = New-Object -TypeName 'System.DirectoryServices.ActiveDirectoryAccessRule' -ArgumentList @( + $ntAccount, + $ActiveDirectoryRights, + $AccessControlType, + $ObjectType, + $ActiveDirectorySecurityInheritance, + $InheritedObjectType + ) + + $acl.AddAccessRule($ace) + } + else + { + <# + Iterate through all ace entries to find the desired ace, which + should be absent. If found, remove the ace from the acl. + #> + foreach ($access in $acl.Access) + { + if ($access.IsInherited -eq $false) + { + if ($access.IdentityReference.Value -eq $IdentityReference -and + $access.AccessControlType -eq $AccessControlType -and + $access.ObjectType.Guid -eq $ObjectType -and + $access.InheritanceType -eq $ActiveDirectorySecurityInheritance -and + $access.InheritedObjectType.Guid -eq $InheritedObjectType) + { + Write-Verbose -Message ($script:localizedData.RemovingObjectPermissionEntry -f $Path) + + $acl.RemoveAccessRule($access) + } + } + } + } + + # Set the updated acl to the object + $acl | + Set-Acl -Path "AD:$Path" +} + +<# + .SYNOPSIS + Test the object permission entry. + + .PARAMETER Ensure + Indicates if the access will be added (Present) or will be removed + (Absent). Default is 'Present'. + + .PARAMETER Path + Active Directory path of the target object to add or remove the + permission entry, specified as a Distinguished Name. + + .PARAMETER IdentityReference + Indicates the identity of the principal for the permission entry. + + .PARAMETER ActiveDirectoryRights + A combination of one or more of the ActiveDirectoryRights enumeration + values that specifies the rights of the access rule. Default is + 'GenericAll'. + + .PARAMETER AccessControlType + Indicates whether to Allow or Deny access to the target object. + + .PARAMETER ObjectType + The schema GUID of the object to which the access rule applies. + + .PARAMETER ActiveDirectorySecurityInheritance + One of the 'ActiveDirectorySecurityInheritance' enumeration values that + specifies the inheritance type of the access rule. + + .PARAMETER InheritedObjectType + The schema GUID of the child object type that can inherit this access + rule. +#> +function Test-TargetResource +{ + [CmdletBinding()] + [OutputType([System.Boolean])] + param + ( + [Parameter()] + [ValidateSet('Present', 'Absent')] + [System.String] + $Ensure = 'Present', + + [Parameter(Mandatory = $true)] + [System.String] + $Path, + + [Parameter(Mandatory = $true)] + [System.String] + $IdentityReference, + + [Parameter()] + [ValidateSet('AccessSystemSecurity', 'CreateChild', 'Delete', 'DeleteChild', 'DeleteTree', 'ExtendedRight', 'GenericAll', 'GenericExecute', 'GenericRead', 'GenericWrite', 'ListChildren', 'ListObject', 'ReadControl', 'ReadProperty', 'Self', 'Synchronize', 'WriteDacl', 'WriteOwner', 'WriteProperty')] + [System.String[]] + $ActiveDirectoryRights = 'GenericAll', + + [Parameter(Mandatory = $true)] + [ValidateSet('Allow', 'Deny')] + [System.String] + $AccessControlType, + + [Parameter(Mandatory = $true)] + [System.String] + $ObjectType, + + [Parameter(Mandatory = $true)] + [ValidateSet('All', 'Children', 'Descendents', 'None', 'SelfAndChildren')] + [System.String] + $ActiveDirectorySecurityInheritance, + + [Parameter(Mandatory = $true)] + [System.String] + $InheritedObjectType + ) + + # Get the current state + $getTargetResourceSplat = @{ + Path = $Path + IdentityReference = $IdentityReference + AccessControlType = $AccessControlType + ObjectType = $ObjectType + ActiveDirectorySecurityInheritance = $ActiveDirectorySecurityInheritance + InheritedObjectType = $InheritedObjectType + } + $currentState = Get-TargetResource @getTargetResourceSplat + + # Always check, if the ensure state is desired + $returnValue = $currentState.Ensure -eq $Ensure + + # Only check the Active Directory rights, if ensure is set to present + if ($Ensure -eq 'Present') + { + # Convert to array to a string for easy compare + [System.String] $currentActiveDirectoryRights = ($currentState.ActiveDirectoryRights | + Sort-Object) -join ', ' + + [System.String] $desiredActiveDirectoryRights = ($ActiveDirectoryRights | + Sort-Object) -join ', ' + + $returnValue = $returnValue -and $currentActiveDirectoryRights -eq $desiredActiveDirectoryRights + } + + if ($returnValue) + { + Write-Verbose -Message ($script:localizedData.ObjectPermissionEntryInDesiredState -f $Path) + } + else + { + Write-Verbose -Message ($script:localizedData.ObjectPermissionEntryNotInDesiredState -f $Path) + } + + return $returnValue +} diff --git a/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_ADObjectPermissionEntry/MSFT_ADObjectPermissionEntry.schema.mof b/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_ADObjectPermissionEntry/MSFT_ADObjectPermissionEntry.schema.mof new file mode 100644 index 0000000..b5e4770 --- /dev/null +++ b/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_ADObjectPermissionEntry/MSFT_ADObjectPermissionEntry.schema.mof @@ -0,0 +1,12 @@ +[ClassVersion("1.0.0.0"), FriendlyName("ADObjectPermissionEntry")] +class MSFT_ADObjectPermissionEntry : OMI_BaseResource +{ + [Write, Description("Indicates if the access will be added (Present) or will be removed (Absent). Default value is 'Present'."), ValueMap{"Present", "Absent"}, Values{"Present", "Absent"}] String Ensure; + [Key, Description("Active Directory path of the target object to add or remove the permission entry, specified as a Distinguished Name.")] String Path; + [Key, Description("Indicates the identity of the principal for the ACE. Use the notation DOMAIN\\SamAccountName for the identity.")] String IdentityReference; + [Write, Description("A combination of one or more of the ActiveDirectoryRights enumeration values that specifies the rights of the access rule. Default value is 'GenericAll'."), ValueMap{"AccessSystemSecurity", "CreateChild", "Delete", "DeleteChild", "DeleteTree", "ExtendedRight", "GenericAll", "GenericExecute", "GenericRead", "GenericWrite", "ListChildren", "ListObject", "ReadControl", "ReadProperty", "Self", "Synchronize", "WriteDacl", "WriteOwner", "WriteProperty"}, Values{"AccessSystemSecurity", "CreateChild", "Delete", "DeleteChild", "DeleteTree", "ExtendedRight", "GenericAll", "GenericExecute", "GenericRead", "GenericWrite", "ListChildren", "ListObject", "ReadControl", "ReadProperty", "Self", "Synchronize", "WriteDacl", "WriteOwner", "WriteProperty"}] String ActiveDirectoryRights[]; + [Key, Description("Indicates whether to Allow or Deny access to the target object."), ValueMap{"Allow", "Deny"}, Values{"Allow", "Deny"}] String AccessControlType; + [Key, Description("The schema GUID of the object to which the access rule applies. If the permission entry shouldn't be restricted to a specific object type, use the zero guid (00000000-0000-0000-0000-000000000000).")] String ObjectType; + [Key, Description("One of the 'ActiveDirectorySecurityInheritance' enumeration values that specifies the inheritance type of the access rule."), ValueMap{"All", "Children", "Descendents", "None", "SelfAndChildren"}, Values{"All", "Children", "Descendents", "None", "SelfAndChildren"}] String ActiveDirectorySecurityInheritance; + [Key, Description("The schema GUID of the child object type that can inherit this access rule. If the permission entry shouldn't be restricted to a specific inherited object type, use the zero guid (00000000-0000-0000-0000-000000000000).")] String InheritedObjectType; +}; diff --git a/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_ADObjectPermissionEntry/README.md b/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_ADObjectPermissionEntry/README.md new file mode 100644 index 0000000..993ab74 --- /dev/null +++ b/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_ADObjectPermissionEntry/README.md @@ -0,0 +1,10 @@ +# Description + +The ADObjectPermissionEntry DSC resource will manage access control lists on Active Directory objects. The resource is +designed to to manage just one entry in the list of permissios (ACL) for one AD object. It will only interact with the +one permission and leave all others as they were. The resource can be used multiple times to add multiple entries into +one ACL. + +## Requirements + +* Target machine must be running Windows Server 2008 R2 or later. diff --git a/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_ADObjectPermissionEntry/en-US/MSFT_ADObjectPermissionEntry.strings.psd1 b/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_ADObjectPermissionEntry/en-US/MSFT_ADObjectPermissionEntry.strings.psd1 new file mode 100644 index 0000000..6827587 --- /dev/null +++ b/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_ADObjectPermissionEntry/en-US/MSFT_ADObjectPermissionEntry.strings.psd1 @@ -0,0 +1,11 @@ + +# culture='en-US' +ConvertFrom-StringData @' + ObjectPermissionEntryFound = Object permission entry found on object '{0}'. (OPE0001) + ObjectPermissionEntryNotFound = Object permission entry not found on object '{0}'. (OPE0002) + AddingObjectPermissionEntry = Adding object permission entry to object '{0}'. (OPE0003) + RemovingObjectPermissionEntry = Removing object permission entry from object '{0}'. (OPE0004) + ObjectPermissionEntryInDesiredState = Object permission entry on object '{0}' is in the desired state. (OPE0005) + ObjectPermissionEntryNotInDesiredState = Object permission entry on object '{0}' is not in the desired state. (OPE0006) + ObjectPathIsAbsent = Object Path '{0}' is absent from Active Directory. (OPE0007) +'@ diff --git a/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_ADObjectPermissionEntry/en-US/about_ADObjectPermissionEntry.help.txt b/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_ADObjectPermissionEntry/en-US/about_ADObjectPermissionEntry.help.txt new file mode 100644 index 0000000..20a2a20 --- /dev/null +++ b/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_ADObjectPermissionEntry/en-US/about_ADObjectPermissionEntry.help.txt @@ -0,0 +1,129 @@ +.NAME + ADObjectPermissionEntry + +.DESCRIPTION + The ADObjectPermissionEntry DSC resource will manage access control lists on Active Directory objects. The resource is + designed to to manage just one entry in the list of permissios (ACL) for one AD object. It will only interact with the + one permission and leave all others as they were. The resource can be used multiple times to add multiple entries into + one ACL. + + ## Requirements + + * Target machine must be running Windows Server 2008 R2 or later. + +.PARAMETER Ensure + Write - String + Allowed values: Present, Absent + Indicates if the access will be added (Present) or will be removed (Absent). Default value is 'Present'. + +.PARAMETER Path + Key - String + Active Directory path of the target object to add or remove the permission entry, specified as a Distinguished Name. + +.PARAMETER IdentityReference + Key - String + Indicates the identity of the principal for the ACE. Use the notation DOMAIN\SamAccountName for the identity. + +.PARAMETER ActiveDirectoryRights + Write - StringArray + Allowed values: AccessSystemSecurity, CreateChild, Delete, DeleteChild, DeleteTree, ExtendedRight, GenericAll, GenericExecute, GenericRead, GenericWrite, ListChildren, ListObject, ReadControl, ReadProperty, Self, Synchronize, WriteDacl, WriteOwner, WriteProperty + A combination of one or more of the ActiveDirectoryRights enumeration values that specifies the rights of the access rule. Default value is 'GenericAll'. + +.PARAMETER AccessControlType + Key - String + Allowed values: Allow, Deny + Indicates whether to Allow or Deny access to the target object. + +.PARAMETER ObjectType + Key - String + The schema GUID of the object to which the access rule applies. If the permission entry shouldn't be restricted to a specific object type, use the zero guid (00000000-0000-0000-0000-000000000000). + +.PARAMETER ActiveDirectorySecurityInheritance + Key - String + Allowed values: All, Children, Descendents, None, SelfAndChildren + One of the 'ActiveDirectorySecurityInheritance' enumeration values that specifies the inheritance type of the access rule. + +.PARAMETER InheritedObjectType + Key - String + The schema GUID of the child object type that can inherit this access rule. If the permission entry shouldn't be restricted to a specific inherited object type, use the zero guid (00000000-0000-0000-0000-000000000000). + +.EXAMPLE 1 + +This configuration will add full control (GenericAll) permissions to +the virtual computer object (VCO) ROLE01 for a cluster name object (CNO) +CONTOSO\CLUSTER01$. This is used so that the Windows Failover Cluster +can control the roles AD objects. + +Configuration ADObjectPermissionEntry_DelegateFullControl_Config +{ + Import-DscResource -Module ActiveDirectoryDsc + + Node localhost + { + ADObjectPermissionEntry 'ADObjectPermissionEntry' + { + Ensure = 'Present' + Path = 'CN=ROLE01,CN=Computers,DC=contoso,DC=com' + IdentityReference = 'CONTOSO\CLUSTER01$' + ActiveDirectoryRights = 'GenericAll' + AccessControlType = 'Allow' + ObjectType = '00000000-0000-0000-0000-000000000000' + ActiveDirectorySecurityInheritance = 'None' + InheritedObjectType = '00000000-0000-0000-0000-000000000000' + } + } +} + +.EXAMPLE 2 + +This configuration will add a group permission to create and delete +(CreateChild,DeleteChild) computer objects in an OU and any sub-OUs that +may get created. + +Configuration ADObjectPermissionEntry_CreateDeleteComputerObject_Config +{ + Import-DscResource -Module ActiveDirectoryDsc + + Node localhost + { + ADObjectPermissionEntry 'ADObjectPermissionEntry' + { + Ensure = 'Present' + Path = 'OU=ContosoComputers,DC=contoso,DC=com' + IdentityReference = 'CONTOSO\ComputerAdminGroup' + ActiveDirectoryRights = 'CreateChild', 'DeleteChild' + AccessControlType = 'Allow' + ObjectType = 'bf967a86-0de6-11d0-a285-00aa003049e2' # Computer objects + ActiveDirectorySecurityInheritance = 'All' + InheritedObjectType = '00000000-0000-0000-0000-000000000000' + } + } +} + +.EXAMPLE 3 + +This configuration will add a group permission to allow read and write +(ReadProperty, WriteProperty) of all properties of computer objects in +an OU and any sub-OUs that may get created. + +Configuration ADObjectPermissionEntry_ReadWriteComputerObjectProperties_Config +{ + Import-DscResource -Module ActiveDirectoryDsc + + Node localhost + { + ADObjectPermissionEntry 'ADObjectPermissionEntry' + { + Ensure = 'Present' + Path = 'OU=ContosoComputers,DC=contoso,DC=com' + IdentityReference = 'CONTOSO\ComputerAdminGroup' + ActiveDirectoryRights = 'ReadProperty', 'WriteProperty' + AccessControlType = 'Allow' + ObjectType = '00000000-0000-0000-0000-000000000000' + ActiveDirectorySecurityInheritance = 'Descendents' + InheritedObjectType = 'bf967a86-0de6-11d0-a285-00aa003049e2' # Computer objects + } + } +} + + diff --git a/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_ADOptionalFeature/MSFT_ADOptionalFeature.psm1 b/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_ADOptionalFeature/MSFT_ADOptionalFeature.psm1 new file mode 100644 index 0000000..b48e62d --- /dev/null +++ b/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_ADOptionalFeature/MSFT_ADOptionalFeature.psm1 @@ -0,0 +1,226 @@ +$script:resourceModulePath = Split-Path -Path (Split-Path -Path $PSScriptRoot -Parent) -Parent +$script:modulesFolderPath = Join-Path -Path $script:resourceModulePath -ChildPath 'Modules' + +$script:localizationModulePath = Join-Path -Path $script:modulesFolderPath -ChildPath 'ActiveDirectoryDsc.Common' +Import-Module -Name (Join-Path -Path $script:localizationModulePath -ChildPath 'ActiveDirectoryDsc.Common.psm1') + +$script:localizedData = Get-LocalizedData -ResourceName 'MSFT_ADOptionalFeature' + +<# + .SYNOPSIS + Gets the state of the Active Directory Optional Feature. + + .PARAMETER FeatureName + The name of the Optional feature to be enabled. + + .PARAMETER ForestFQDN + The fully qualified domain name (FQDN) of the forest in which to change the Optional feature. + + .PARAMETER EnterpriseAdministratorCredential + The user account credentials to use to perform this task. + +#> +function Get-TargetResource +{ + [CmdletBinding()] + [OutputType([System.Collections.Hashtable])] + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $FeatureName, + + [Parameter(Mandatory = $true)] + [System.String] + $ForestFQDN, + + [Parameter(Mandatory = $true)] + [System.Management.Automation.PSCredential] + $EnterpriseAdministratorCredential + ) + + $previousErrorActionPreference = $ErrorActionPreference + + try + { + # AD cmdlets generate non-terminating errors. + $ErrorActionPreference = 'Stop' + + $forest = Get-ADForest -Server $ForestFQDN -Credential $EnterpriseAdministratorCredential + + $feature = Get-ADOptionalFeature -Identity $FeatureName -Server $forest.DomainNamingMaster -Credential $EnterpriseAdministratorCredential + + if ($feature.EnabledScopes.Count -gt 0) + { + Write-Verbose -Message ($script:localizedData.OptionalFeatureEnabled -f $FeatureName) + $featureEnabled = $True + } + else + { + Write-Verbose -Message ($script:localizedData.OptionalFeatureNotEnabled -f $FeatureName) + $featureEnabled = $False + } + } + catch [Microsoft.ActiveDirectory.Management.ADIdentityNotFoundException], [Microsoft.ActiveDirectory.Management.ADServerDownException] + { + $errorMessage = $script:localizedData.ForestNotFound -f $ForestFQDN + New-ObjectNotFoundException -Message $errorMessage -ErrorRecord $_ + } + catch [System.Security.Authentication.AuthenticationException] + { + $errorMessage = $script:localizedData.CredentialError + New-InvalidArgumentException -Message $errorMessage -ArgumentName 'EnterpriseAdministratorCredential' + } + catch + { + $errorMessage = $script:localizedData.GetUnhandledException -f $ForestFQDN + New-InvalidOperationException -Message $errorMessage -ErrorRecord $_ + } + finally + { + $ErrorActionPreference = $previousErrorActionPreference + } + + # Return a credential object without the password. + $cimCredentialInstance = New-CimCredentialInstance -Credential $EnterpriseAdministratorCredential + + return @{ + ForestFQDN = $ForestFQDN + FeatureName = $FeatureName + Enabled = $featureEnabled + EnterpriseAdministratorCredential = $cimCredentialInstance + } +} + +<# + .SYNOPSIS + Sets the state of the Active Directory Optional Feature. + + .PARAMETER FeatureName + The name of the Optional feature to be enabled. + + .PARAMETER ForestFQDN + The fully qualified domain name (FQDN) of the forest in which to change the Optional feature. + + .PARAMETER EnterpriseAdministratorCredential + The user account credentials to use to perform this task. + +#> +function Set-TargetResource +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $FeatureName, + + [Parameter(Mandatory = $true)] + [System.String] + $ForestFQDN, + + [Parameter(Mandatory = $true)] + [System.Management.Automation.PSCredential] + $EnterpriseAdministratorCredential + ) + + $previousErrorActionPreference = $ErrorActionPreference + + try + { + # AD cmdlets generate non-terminating errors. + $ErrorActionPreference = 'Stop' + + $forest = Get-ADForest -Server $ForestFQDN -Credential $EnterpriseAdministratorCredential + $domain = Get-ADDomain -Server $ForestFQDN -Credential $EnterpriseAdministratorCredential + + $feature = Get-ADOptionalFeature -Identity $FeatureName -Server $forest.DomainNamingMaster -Credential $EnterpriseAdministratorCredential + + # Check minimum forest level and throw if not + if (($forest.ForestMode -as [int]) -lt ($feature.RequiredForestMode -as [int])) + { + throw ($script:localizedData.ForestFunctionalLevelError -f $forest.ForestMode) + } + + # Check minimum domain level and throw if not + if (($domain.DomainMode -as [int]) -lt ($feature.RequiredDomainMode -as [int])) + { + throw ($script:localizedData.DomainFunctionalLevelError -f $domain.DomainMode) + } + + Write-Verbose -Message ($script:localizedData.EnablingOptionalFeature -f $forest.RootDomain, $FeatureName) + + Enable-ADOptionalFeature -Identity $FeatureName -Scope ForestOrConfigurationSet ` + -Target $forest.RootDomain -Server $forest.DomainNamingMaster ` + -Credential $EnterpriseAdministratorCredential ` + -Verbose:$VerbosePreference + } + catch [Microsoft.ActiveDirectory.Management.ADIdentityNotFoundException], [Microsoft.ActiveDirectory.Management.ADServerDownException] + { + $errorMessage = $script:localizedData.ForestNotFound -f $ForestFQDN + New-ObjectNotFoundException -Message $errorMessage -ErrorRecord $_ + } + catch [System.Security.Authentication.AuthenticationException] + { + $errorMessage = $script:localizedData.CredentialError + New-InvalidArgumentException -Message $errorMessage -ArgumentName 'EnterpriseAdministratorCredential' + } + catch + { + $errorMessage = $script:localizedData.SetUnhandledException -f $ForestFQDN + New-InvalidOperationException -Message $errorMessage -ErrorRecord $_ + } + finally + { + $ErrorActionPreference = $previousErrorActionPreference + } +} + +<# + .SYNOPSIS + Tests the state of the Active Directory Optional Feature. + + .PARAMETER FeatureName + The name of the Optional feature to be enabled. + + .PARAMETER ForestFQDN + The fully qualified domain name (FQDN) of the forest in which to change the Optional feature. + + .PARAMETER EnterpriseAdministratorCredential + The user account credentials to use to perform this task. + +#> +function Test-TargetResource +{ + [CmdletBinding()] + [OutputType([System.Boolean])] + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $FeatureName, + + [Parameter(Mandatory = $true)] + [System.String] + $ForestFQDN, + + [Parameter(Mandatory = $true)] + [System.Management.Automation.PSCredential] + $EnterpriseAdministratorCredential + ) + + $state = Get-TargetResource @PSBoundParameters + + if ($true -eq $state.Enabled) + { + Write-Verbose -Message ($script:localizedData.OptionalFeatureEnabled -f $FeatureName) + return $true + } + else + { + Write-Verbose -Message ($script:localizedData.OptionalFeatureNotEnabled -f $FeatureName) + return $false + } +} + +Export-ModuleMember -Function *-TargetResource diff --git a/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_ADOptionalFeature/MSFT_ADOptionalFeature.schema.mof b/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_ADOptionalFeature/MSFT_ADOptionalFeature.schema.mof new file mode 100644 index 0000000..97b6fc6 --- /dev/null +++ b/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_ADOptionalFeature/MSFT_ADOptionalFeature.schema.mof @@ -0,0 +1,8 @@ +[ClassVersion("1.0.0.0"), FriendlyName("ADOptionalFeature")] +class MSFT_ADOptionalFeature : OMI_BaseResource +{ + [Key, Description("Specifies the target Active Directory forest for the change.")] String ForestFQDN; + [Key, Description("Specifies the feature to be activated")] String FeatureName; + [Required, EmbeddedInstance("MSFT_Credential"), Description("Specifies the user account credentials to use to perform this task.")] String EnterpriseAdministratorCredential; + [Read, Description("Shows the current state of the feature i.e. enabled or not")] Boolean Enabled; +}; diff --git a/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_ADOptionalFeature/README.md b/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_ADOptionalFeature/README.md new file mode 100644 index 0000000..c9f02d9 --- /dev/null +++ b/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_ADOptionalFeature/README.md @@ -0,0 +1,10 @@ +# Description + +The ADOptionalFeature DSC resource will enable the Active Directory Optional Feature of choice for the target forest. +This resource first verifies that the forest and domain modes match or exceed the requirements. If the forest or domain mode +is insufficient, then the resource will exit with an error message. The change is executed against the +Domain Naming Master FSMO of the forest. + +## Requirements + +* Target machine must be running Windows Server 2008 R2 or later, depending on the feature. diff --git a/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_ADOptionalFeature/en-US/MSFT_ADOptionalFeature.strings.psd1 b/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_ADOptionalFeature/en-US/MSFT_ADOptionalFeature.strings.psd1 new file mode 100644 index 0000000..2716bc4 --- /dev/null +++ b/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_ADOptionalFeature/en-US/MSFT_ADOptionalFeature.strings.psd1 @@ -0,0 +1,12 @@ +# culture='en-US' +ConvertFrom-StringData @' + ForestNotFound = Cannot contact forest '{0}'. Check the spelling of the Forest FQDN and make sure that a domain controller is available on the network. (ADOF0001) + CredentialError = Credential error. Check the username and password used. (ADOF0002) + GetUnhandledException = Unhandled exception getting Optional Feature status for forest '{0}'. (ADOF0003) + SetUnhandledException = Unhandled exception setting Optional Feature status for forest '{0}'. (ADOF0004) + ForestFunctionalLevelError = Forest functional level '{0}' does not meet minimum requirement of Windows2008R2Forest or greater. (ADOF0005) + DomainFunctionalLevelError = Domain functional level '{0}' does not meet minimum requirement of Windows2008R2Forest or greater. (ADOF0006) + OptionalFeatureEnabled = Active Directory {0} is enabled. (ADOF0007) + OptionalFeatureNotEnabled = Active Directory {0} is not enabled. (ADOF0008) + EnablingOptionalFeature = Enabling Active Directory {1} in the forest '{0}'. (ADOF0009) +'@ diff --git a/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_ADOptionalFeature/en-US/about_ADOptionalFeature.help.txt b/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_ADOptionalFeature/en-US/about_ADOptionalFeature.help.txt new file mode 100644 index 0000000..5bd6b3c --- /dev/null +++ b/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_ADOptionalFeature/en-US/about_ADOptionalFeature.help.txt @@ -0,0 +1,61 @@ +.NAME + ADOptionalFeature + +.DESCRIPTION + The ADOptionalFeature DSC resource will enable the Active Directory Optional Feature of choice for the target forest. + This resource first verifies that the forest and domain modes match or exceed the requirements. If the forest or domain mode + is insufficient, then the resource will exit with an error message. The change is executed against the + Domain Naming Master FSMO of the forest. + + ## Requirements + + * Target machine must be running Windows Server 2008 R2 or later, depending on the feature. + +.PARAMETER ForestFQDN + Key - String + Specifies the target Active Directory forest for the change. + +.PARAMETER FeatureName + Key - String + Specifies the feature to be activated + +.PARAMETER EnterpriseAdministratorCredential + Required - PSCredential + Specifies the user account credentials to use to perform this task. + +.PARAMETER Enabled + Read - Boolean + Shows the current state of the feature i.e. enabled or not + +.EXAMPLE 1 + +This configuration will enable the Active Directory Recycle Bin for a +specified Domain + +Configuration ADOptionalFeature_EnableADRecycleBin_Config +{ + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $ForestFQDN, + + [Parameter(Mandatory = $true)] + [System.Management.Automation.PSCredential] + $EnterpriseAdministratorCredential + ) + + Import-DscResource -Module ActiveDirectoryDsc + + Node localhost + { + ADOptionalFeature RecycleBin + { + FeatureName = "Recycle Bin Feature" + EnterpriseAdministratorCredential = $EnterpriseAdministratorCredential + ForestFQDN = $ForestFQDN + } + } +} + + diff --git a/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_ADOrganizationalUnit/MSFT_ADOrganizationalUnit.psm1 b/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_ADOrganizationalUnit/MSFT_ADOrganizationalUnit.psm1 new file mode 100644 index 0000000..67dcacf --- /dev/null +++ b/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_ADOrganizationalUnit/MSFT_ADOrganizationalUnit.psm1 @@ -0,0 +1,429 @@ +$script:resourceModulePath = Split-Path -Path (Split-Path -Path $PSScriptRoot -Parent) -Parent +$script:modulesFolderPath = Join-Path -Path $script:resourceModulePath -ChildPath 'Modules' + +$script:localizationModulePath = Join-Path -Path $script:modulesFolderPath -ChildPath 'ActiveDirectoryDsc.Common' +Import-Module -Name (Join-Path -Path $script:localizationModulePath -ChildPath 'ActiveDirectoryDsc.Common.psm1') + +$script:localizedData = Get-LocalizedData -ResourceName 'MSFT_ADOrganizationalUnit' + +<# + .SYNOPSIS + Gets the Organizational Unit (OU) from Active Directory + + .PARAMETER Name + Specifies the name of the Organizational Unit (OU). + + .PARAMETER Path + Specifies the X.500 path of the OrganizationalUnit (OU) or container where the new object is created. + + .NOTES + Used Functions: + Name | Module + ------------------------------|-------------------------- + Get-ADOrganizationalUnit | ActiveDirectory + Assert-Module | ActiveDirectoryDsc.Common + New-InvalidOperationException | ActiveDirectoryDsc.Common +#> +function Get-TargetResource +{ + [CmdletBinding()] + [OutputType([System.Collections.Hashtable])] + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $Name, + + [Parameter(Mandatory = $true)] + [System.String] + $Path + ) + + Assert-Module -ModuleName 'ActiveDirectory' + + Write-Verbose ($script:localizedData.RetrievingOU -f $Name, $Path) + + try + { + $ou = Get-ADOrganizationalUnit -Filter "Name -eq '$Name'" -SearchBase $Path ` + -SearchScope OneLevel -Properties ProtectedFromAccidentalDeletion, Description + } + catch [Microsoft.ActiveDirectory.Management.ADIdentityNotFoundException] + { + Write-Verbose -Message ($script:localizedData.OUPathIsAbsent -f $Path) + $ou = $null + } + catch + { + $errorMessage = $script:localizedData.GetResourceError -f $Name + New-InvalidOperationException -Message $errorMessage -ErrorRecord $_ + } + + if ($ou) + { + Write-Verbose -Message ($script:localizedData.OUIsPresent -f $Name) + + $targetResource = @{ + Name = $Name + Path = $Path + ProtectedFromAccidentalDeletion = $ou.ProtectedFromAccidentalDeletion + Description = $ou.Description + DistinguishedName = $ou.DistinguishedName + Ensure = 'Present' + } + } + else + { + Write-Verbose -Message ($script:localizedData.OUIsAbsent -f $Name) + + $targetResource = @{ + Name = $Name + Path = $Path + ProtectedFromAccidentalDeletion = $null + Description = $null + DistinguishedName = $null + Ensure = 'Absent' + } + } + + return $targetResource +} # end function Get-TargetResource + +<# + .SYNOPSIS + Tests the state of the specified Organizational Unit (OU). + + .PARAMETER Name + Specifies the name of the Organizational Unit (OU). + + .PARAMETER Path + Specifies the X.500 path of the Organizational Unit (OU) or container where the new object is created. + + .PARAMETER Ensure + Specifies whether the Organizational Unit (OU) should be present or absent. Default value is 'Present'. + + .PARAMETER Credential + The credential to be used to perform the operation on Active Directory. + + .PARAMETER ProtectedFromAccidentalDeletion + Specifies if the Organizational Unit (OU) container should be protected from deletion. Default value is $true. + + .PARAMETER Description + Specifies the description of the Organizational Unit (OU). Default value is empty (''). + + .PARAMETER RestoreFromRecycleBin + Try to restore the Organizational Unit (OU) from the recycle bin before creating a new one. + + .NOTES + Used Functions: + Name | Module + ------------------------------|-------------------------- + Compare-ResourcePropertyState | ActiveDirectoryDsc.Common +#> +function Test-TargetResource +{ + [CmdletBinding()] + [OutputType([System.Boolean])] + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $Name, + + [Parameter(Mandatory = $true)] + [System.String] + $Path, + + [Parameter()] + [ValidateSet('Present', 'Absent')] + [System.String] + $Ensure = 'Present', + + [Parameter()] + [ValidateNotNull()] + [System.Management.Automation.PSCredential] + [System.Management.Automation.CredentialAttribute()] + $Credential, + + [Parameter()] + [ValidateNotNull()] + [System.Boolean] + $ProtectedFromAccidentalDeletion = $true, + + [Parameter()] + [ValidateNotNull()] + [System.String] + $Description = '', + + [Parameter()] + [ValidateNotNull()] + [System.Boolean] + $RestoreFromRecycleBin + ) + + $targetResource = Get-TargetResource -Name $Name -Path $Path + + if ($targetResource.Ensure -eq 'Present') + { + # Resource exists + if ($Ensure -eq 'Present') + { + # Resource should exist + $propertiesNotInDesiredState = ( + Compare-ResourcePropertyState -CurrentValue $targetResource -DesiredValues $PSBoundParameters | + Where-Object -Property InDesiredState -eq $false) + + if ($propertiesNotInDesiredState) + { + $inDesiredState = $false + } + else + { + # Resource is in the desired state + Write-Verbose ($script:localizedData.OUInDesiredState -f $Name) + + $inDesiredState = $true + } + } + else + { + # Resource should not exist + Write-Verbose ($script:localizedData.OUExistsButShouldNot -f $Name) + + $inDesiredState = $false + } + } + else + { + # Resource does not exist + if ($Ensure -eq 'Present') + { + # Resource should exist + Write-Verbose ($script:localizedData.OUDoesNotExistButShould -f $Name) + + $inDesiredState = $false + } + else + { + # Resource should not exist + Write-Verbose ($script:localizedData.OUDoesNotExistAndShouldNot -f $Name) + + $inDesiredState = $true + } + } + + return $inDesiredState +} #end function Test-TargetResource + +<# + .SYNOPSIS + Sets the state of the Organizational Unit (OU) in Active Directory. + + .PARAMETER Name + Specifies the name of the Organizational Unit (OU). + + .PARAMETER Path + Specifies the X.500 path of the Organizational Unit (OU) or container where the new object is created. + + .PARAMETER Ensure + Specifies whether the Organizational Unit (OU) should be present or absent. Default value is 'Present'. + + .PARAMETER Credential + The credential to be used to perform the operation on Active Directory. + + .PARAMETER ProtectedFromAccidentalDeletion + Specifies if the Organizational Unit (OU) container should be protected from deletion. Default value is $true. + + .PARAMETER Description + Specifies the description of the Organizational Unit (OU). Default value is empty (''). + + .PARAMETER RestoreFromRecycleBin + Try to restore the Organizational Unit (OU) from the recycle bin before creating a new one. + + .NOTES + Used Functions: + Name | Module + ------------------------------|-------------------------- + New-ADOrganizationalUnit | ActiveDirectory + Set-ADOrganizationalUnit | ActiveDirectory + Remove-ADOrganizationalUnit | ActiveDirectory + New-InvalidOperationException | ActiveDirectoryDsc.Common + Restore-ADCommonObject | ActiveDirectoryDsc.Common + New-ObjectNotFoundException | ActiveDirectoryDsc.Common +#> +function Set-TargetResource +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $Name, + + [Parameter(Mandatory = $true)] + [System.String] + $Path, + + [Parameter()] + [ValidateSet('Present', 'Absent')] + [System.String] + $Ensure = 'Present', + + [Parameter()] + [ValidateNotNull()] + [System.Management.Automation.PSCredential] + [System.Management.Automation.CredentialAttribute()] + $Credential, + + [Parameter()] + [ValidateNotNull()] + [System.Boolean] + $ProtectedFromAccidentalDeletion = $true, + + [Parameter()] + [ValidateNotNull()] + [System.String] + $Description = '', + + [Parameter()] + [ValidateNotNull()] + [System.Boolean] + $RestoreFromRecycleBin + ) + + $targetResource = Get-TargetResource -Name $Name -Path $Path + + if ($targetResource.Ensure -eq 'Present') + { + if ($Ensure -eq 'Present') + { + Write-Verbose ($script:localizedData.UpdatingOU -f $Name) + + $setADOrganizationalUnitParams = @{ + Identity = $targetResource.DistinguishedName + Description = $Description + ProtectedFromAccidentalDeletion = $ProtectedFromAccidentalDeletion + } + + if ($Credential) + { + $setADOrganizationalUnitParams['Credential'] = $Credential + } + + try + { + Set-ADOrganizationalUnit @setADOrganizationalUnitParams + } + catch + { + $errorMessage = $script:localizedData.SetResourceError -f $Name + New-InvalidOperationException -Message $errorMessage -ErrorRecord $_ + } + + } + else + { + Write-Verbose ($script:localizedData.DeletingOU -f $Name) + + # Disable 'ProtectedFromAccidentalDeletion' if it is set. + if ($targetResource.ProtectedFromAccidentalDeletion) + { + $setADOrganizationalUnitParams = @{ + Identity = $targetResource.DistinguishedName + ProtectedFromAccidentalDeletion = $false + } + + if ($Credential) + { + $setADOrganizationalUnitParams['Credential'] = $Credential + } + + try + { + Set-ADOrganizationalUnit @setADOrganizationalUnitParams + } + catch + { + $errorMessage = $script:localizedData.SetResourceError -f $Name + New-InvalidOperationException -Message $errorMessage -ErrorRecord $_ + } + } + + $removeADOrganizationalUnitParams = @{ + Identity = $targetResource.DistinguishedName + } + + if ($Credential) + { + $removeADOrganizationalUnitParams['Credential'] = $Credential + } + + try + { + Remove-ADOrganizationalUnit @removeADOrganizationalUnitParams + } + catch + { + $errorMessage = $script:localizedData.RemoveResourceError -f $Name + New-InvalidOperationException -Message $errorMessage -ErrorRecord $_ + } + } + } + else + { + if ($Ensure -eq 'Present') + { + if ($RestoreFromRecycleBin) + { + Write-Verbose -Message ($script:localizedData.RestoringOu -f $Name) + + $restoreParams = @{ + Identity = $Name + ObjectClass = 'OrganizationalUnit' + ErrorAction = 'Stop' + } + + if ($Credential) + { + $restoreParams['Credential'] = $Credential + } + + $restoreSuccessful = Restore-ADCommonObject @restoreParams + } + + if (-not $RestoreFromRecycleBin -or ($RestoreFromRecycleBin -and -not $restoreSuccessful)) + { + Write-Verbose ($script:localizedData.CreatingOU -f $Name) + + $newADOrganizationalUnitParams = @{ + Name = $Name + Path = $Path + Description = $Description + ProtectedFromAccidentalDeletion = $ProtectedFromAccidentalDeletion + } + + if ($Credential) + { + $newADOrganizationalUnitParams['Credential'] = $Credential + } + + try + { + New-ADOrganizationalUnit @newADOrganizationalUnitParams + } + catch [Microsoft.ActiveDirectory.Management.ADIdentityNotFoundException] + { + $errorMessage = $script:localizedData.PathNotFoundError -f $Path + New-ObjectNotFoundException -Message $errorMessage + } + catch + { + $errorMessage = $script:localizedData.NewResourceError -f $Name + New-InvalidOperationException -Message $errorMessage -ErrorRecord $_ + } + } + } + } +} #end function Set-TargetResource + +Export-ModuleMember -Function *-TargetResource diff --git a/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_ADOrganizationalUnit/MSFT_ADOrganizationalUnit.schema.mof b/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_ADOrganizationalUnit/MSFT_ADOrganizationalUnit.schema.mof new file mode 100644 index 0000000..b139e69 --- /dev/null +++ b/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_ADOrganizationalUnit/MSFT_ADOrganizationalUnit.schema.mof @@ -0,0 +1,12 @@ +[ClassVersion("1.0.0.0"), FriendlyName("ADOrganizationalUnit")] +class MSFT_ADOrganizationalUnit : OMI_BaseResource +{ + [Key, Description("The name of the Organizational Unit (OU).")] String Name; + [Key, Description("Specifies the X.500 path of the Organizational Unit (OU) or container where the new object is created.")] String Path; + [Write, Description("Specifies whether the Organizational Unit (OU) should be present or absent. Default value is 'Present'."), ValueMap{"Present", "Absent"}, Values{"Present", "Absent"}] String Ensure; + [Write, Description("The credential to be used to perform the operation on Active Directory."), EmbeddedInstance("MSFT_Credential")] String Credential; + [Write, Description("Specifies if the Organizational Unit (OU) container should be protected from deletion. Default value is $true.")] Boolean ProtectedFromAccidentalDeletion; + [Write, Description("Specifies the description of the Organizational Unit (OU). Default value is empty ('').")] String Description; + [Write, Description("Try to restore the Organizational Unit (OU) from the recycle bin before creating a new one.")] Boolean RestoreFromRecycleBin; + [Read, Description("Returns the X.500 distinguished name of the Organizational Unit.")] String DistinguishedName; +}; diff --git a/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_ADOrganizationalUnit/README.md b/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_ADOrganizationalUnit/README.md new file mode 100644 index 0000000..66887e9 --- /dev/null +++ b/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_ADOrganizationalUnit/README.md @@ -0,0 +1,11 @@ +# Description + +The ADOrganizational Unit DSC resource will manage Organizational Units (OUs) within Active Directory. An OU is a +subdivision within an Active Directory into which you can place users, groups, computers, and other organizational +units. + +## Requirements + +* Target machine must be running Windows Server 2008 R2 or later. +* The parameter `RestoreFromRecycleBin` requires that the feature Recycle Bin has been enabled prior to an object being + deleted. If the Recycle Bin feature is disabled then the property `msDS-LastKnownRDN` is not added the deleted object. diff --git a/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_ADOrganizationalUnit/en-US/MSFT_ADOrganizationalUnit.strings.psd1 b/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_ADOrganizationalUnit/en-US/MSFT_ADOrganizationalUnit.strings.psd1 new file mode 100644 index 0000000..09d4977 --- /dev/null +++ b/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_ADOrganizationalUnit/en-US/MSFT_ADOrganizationalUnit.strings.psd1 @@ -0,0 +1,20 @@ +# culture="en-US" +ConvertFrom-StringData @' + RetrievingOU = Retrieving OU '{0}' from path '{1}'. (ADOU0001) + UpdatingOU = Updating OU '{0}'. (ADOU0002) + DeletingOU = Deleting OU '{0}'. (ADOU0003) + CreatingOU = Creating OU '{0}'. (ADOU0004) + RestoringOU = Attempting to restore the organizational unit object' {0}' from the recycle bin. (ADOU0005) + OUInDesiredState = OU '{0}' exists and is in the desired state. (ADOU0006) + OUExistsButShouldNot = OU '{0}' exists when it should not exist. (ADOU0007) + OUDoesNotExistButShould = OU '{0}' does not exist when it should exist. (ADOU0008) + OUDoesNotExistAndShouldNot = OU '{0}' does not exist and is in the desired state. (ADOU00090) + PathNotFoundError = The Path '{0}' was not found. (ADOU0010) + OUIsPresent = The OU '{0}' is present. (ADOU0011) + OUIsAbsent = The OU '{0}' is absent. (ADOU0012) + OUPathIsAbsent = The OU Parent Path '{0}' is absent. (ADOU0013) + GetResourceError = Error getting OU '{0}'. (ADOU0014) + NewResourceError = Error adding OU '{0}'. (ADOU0015) + SetResourceError = Error updating OU '{0}'. (ADOU0016) + RemoveResourceError = Error removing OU '{0}'. (ADOU0017) +'@ diff --git a/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_ADOrganizationalUnit/en-US/about_ADOrganizationalUnit.help.txt b/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_ADOrganizationalUnit/en-US/about_ADOrganizationalUnit.help.txt new file mode 100644 index 0000000..6bd4b38 --- /dev/null +++ b/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_ADOrganizationalUnit/en-US/about_ADOrganizationalUnit.help.txt @@ -0,0 +1,85 @@ +.NAME + ADOrganizationalUnit + +.DESCRIPTION + The ADOrganizational Unit DSC resource will manage Organizational Units (OUs) within Active Directory. An OU is a + subdivision within an Active Directory into which you can place users, groups, computers, and other organizational + units. + + ## Requirements + + * Target machine must be running Windows Server 2008 R2 or later. + * The parameter `RestoreFromRecycleBin` requires that the feature Recycle Bin has been enabled prior to an object being + deleted. If the Recycle Bin feature is disabled then the property `msDS-LastKnownRDN` is not added the deleted object. + +.PARAMETER Name + Key - String + The name of the Organizational Unit (OU). + +.PARAMETER Path + Key - String + Specifies the X.500 path of the Organizational Unit (OU) or container where the new object is created. + +.PARAMETER Ensure + Write - String + Allowed values: Present, Absent + Specifies whether the Organizational Unit (OU) should be present or absent. Default value is 'Present'. + +.PARAMETER Credential + Write - PSCredential + The credential to be used to perform the operation on Active Directory. + +.PARAMETER ProtectedFromAccidentalDeletion + Write - Boolean + Specifies if the Organizational Unit (OU) container should be protected from deletion. Default value is $true. + +.PARAMETER Description + Write - String + Specifies the description of the Organizational Unit (OU). Default value is empty (''). + +.PARAMETER RestoreFromRecycleBin + Write - Boolean + Try to restore the Organizational Unit (OU) from the recycle bin before creating a new one. + +.EXAMPLE 1 + +This configuration will add an Active Directory organizational unit to the domain. + +Configuration ADOrganizationalUnit_CreateADOU_Config +{ + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $Name, + + [Parameter(Mandatory = $true)] + [System.String] + $Path, + + [Parameter()] + [System.Boolean] + $ProtectedFromAccidentalDeletion = $true, + + [Parameter()] + [ValidateNotNull()] + [System.String] + $Description = '' + ) + + Import-DscResource -Module ActiveDirectoryDsc + + Node localhost + { + ADOrganizationalUnit 'ExampleOU' + { + Name = $Name + Path = $Path + ProtectedFromAccidentalDeletion = $ProtectedFromAccidentalDeletion + Description = $Description + Ensure = 'Present' + } + } +} + + diff --git a/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_ADReplicationSite/MSFT_ADReplicationSite.psm1 b/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_ADReplicationSite/MSFT_ADReplicationSite.psm1 new file mode 100644 index 0000000..e1ddb96 --- /dev/null +++ b/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_ADReplicationSite/MSFT_ADReplicationSite.psm1 @@ -0,0 +1,237 @@ +$script:resourceModulePath = Split-Path -Path (Split-Path -Path $PSScriptRoot -Parent) -Parent +$script:modulesFolderPath = Join-Path -Path $script:resourceModulePath -ChildPath 'Modules' + +$script:localizationModulePath = Join-Path -Path $script:modulesFolderPath -ChildPath 'ActiveDirectoryDsc.Common' +Import-Module -Name (Join-Path -Path $script:localizationModulePath -ChildPath 'ActiveDirectoryDsc.Common.psm1') + +$script:localizedData = Get-LocalizedData -ResourceName 'MSFT_ADReplicationSite' + +<# + .SYNOPSIS + Returns the current state of the AD replication site. + + .PARAMETER Name + Specifies the name of the AD replication site. +#> +function Get-TargetResource +{ + [CmdletBinding()] + [OutputType([System.Collections.Hashtable])] + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $Name, + + [Parameter()] + [System.Boolean] + $RenameDefaultFirstSiteName + ) + + # Get the replication site filtered by it's name. If the site is not + # present, the command will return $null. + Write-Verbose -Message ($script:localizedData.GetReplicationSite -f $Name) + $replicationSite = Get-ADReplicationSite -Filter { Name -eq $Name } -ErrorAction SilentlyContinue + + if ($null -eq $replicationSite) + { + Write-Verbose -Message ($script:localizedData.ReplicationSiteAbsent -f $Name) + $returnValue = @{ + Ensure = 'Absent' + Name = $Name + Description = $null + RenameDefaultFirstSiteName = $RenameDefaultFirstSiteName + } + } + else + { + Write-Verbose -Message ($script:localizedData.ReplicationSitePresent -f $Name) + $returnValue = @{ + Ensure = 'Present' + Name = $Name + Description = $replicationSite.Description + RenameDefaultFirstSiteName = $RenameDefaultFirstSiteName + } + } + + return $returnValue +} + +<# + .SYNOPSIS + Add, remove or rename the AD replication site. + + .PARAMETER Ensure + Specifies if the AD replication site should be added or remove. Default + value is 'Present'. + + .PARAMETER Name + Specifies the name of the AD replication site. + + .PARAMETER RenameDefaultFirstSiteName + Specify if the Default-First-Site-Name should be renamed, if it exists. + Dafult value is 'false'. +#> +function Set-TargetResource +{ + [CmdletBinding()] + param + ( + [Parameter()] + [ValidateSet('Present', 'Absent')] + [System.String] + $Ensure = 'Present', + + [Parameter(Mandatory = $true)] + [System.String] + $Name, + + [Parameter()] + [System.Boolean] + $RenameDefaultFirstSiteName = $false, + + [Parameter()] + [System.String] + $Description + ) + + $getTargetResourceResult = Get-TargetResource -Name $Name -RenameDefaultFirstSiteName $RenameDefaultFirstSiteName + + if ($Ensure -eq 'Present') + { + if ($getTargetResourceResult.Ensure -eq 'Absent') + { + $defaultFirstSiteName = Get-ADReplicationSite -Filter { Name -eq 'Default-First-Site-Name' } -ErrorAction SilentlyContinue + + <# + Check if the user specified to rename the Default-First-Site-Name + and if it still exists. If both is true, rename the replication site + instead of creating a new site. + #> + if ($RenameDefaultFirstSiteName -and $null -ne $defaultFirstSiteName) + { + Write-Verbose -Message ($script:localizedData.AddReplicationSiteDefaultFirstSiteName -f $Name) + + Rename-ADObject -Identity $defaultFirstSiteName.DistinguishedName -NewName $Name -ErrorAction Stop + } + else + { + Write-Verbose -Message ($script:localizedData.AddReplicationSite -f $Name) + + $newADReplicationSiteParameters = @{ + Name = $Name + ErrorAction = 'Stop' + } + + if ($PSBoundParameters.ContainsKey('Description')) + { + $newADReplicationSiteParameters['Description'] = $Description + } + + New-ADReplicationSite @newADReplicationSiteParameters + } + } + + if ($PSBoundParameters.ContainsKey('Description') -and $getTargetResourceResult.Description -ne $Description) + { + Write-Verbose -Message ($script:localizedData.UpdateReplicationSite -f $Name) + Set-ADReplicationSite -Identity $Name -Description $Description + } + } + + if ($Ensure -eq 'Absent') + { + Write-Verbose -Message ($script:localizedData.RemoveReplicationSite -f $Name) + + Remove-ADReplicationSite -Identity $Name -Confirm:$false -ErrorAction Stop + } +} + +<# + .SYNOPSIS + Test the AD replication site. + + .PARAMETER Ensure + Specifies if the AD replication site should be added or remove. Default + value is 'Present'. + + .PARAMETER Name + Specifies the name of the AD replication site. + + .PARAMETER RenameDefaultFirstSiteName + Specify if the Default-First-Site-Name should be renamed, if it exists. + Dafult value is 'false'. + + .PARAMETER Description + Specifies a description of the object. This parameter sets the value of + the Description property for the object. The LDAP Display Name + (ldapDisplayName) for this property is 'description'. +#> +function Test-TargetResource +{ + [CmdletBinding()] + [OutputType([System.Boolean])] + param + ( + [Parameter()] + [ValidateSet('Present', 'Absent')] + [System.String] + $Ensure = 'Present', + + [Parameter(Mandatory = $true)] + [System.String] + $Name, + + [Parameter()] + [System.Boolean] + $RenameDefaultFirstSiteName = $false, + + [Parameter()] + [System.String] + $Description + ) + + $getTargetResourceResult = Get-TargetResource -Name $Name -RenameDefaultFirstSiteName $RenameDefaultFirstSiteName + $configurationCompliant = $true + + if ($getTargetResourceResult.Ensure -eq 'Absent') + { + # Site doesn't exist + if ($getTargetResourceResult.Ensure -eq $Ensure) + { + # Site should not exist + Write-Verbose -Message ($script:localizedData.ReplicationSiteInDesiredState -f $Name) + } + else + { + #Site should exist + Write-Verbose -Message ($script:localizedData.ReplicationSiteNotInDesiredState -f $Name) + $configurationCompliant = $false + } + } + else + { + # Site Exists + if ($getTargetResourceResult.Ensure -eq $Ensure) + { + # Site should exist + if ($PSBoundParameters.ContainsKey('Description') -and $getTargetResourceResult.Description -ne $Description) + { + Write-Verbose -Message ($script:localizedData.ReplicationSiteNotInDesiredState -f $Name) + $configurationCompliant = $false + } + else + { + Write-Verbose -Message ($script:localizedData.ReplicationSiteInDesiredState -f $Name) + } + } + else + { + # Site should not exist + Write-Verbose -Message ($script:localizedData.ReplicationSiteNotInDesiredState -f $Name) + $configurationCompliant = $false + } + } + + return $configurationCompliant +} diff --git a/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_ADReplicationSite/MSFT_ADReplicationSite.schema.mof b/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_ADReplicationSite/MSFT_ADReplicationSite.schema.mof new file mode 100644 index 0000000..2993c35 --- /dev/null +++ b/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_ADReplicationSite/MSFT_ADReplicationSite.schema.mof @@ -0,0 +1,8 @@ +[ClassVersion("1.0.0.0"), FriendlyName("ADReplicationSite")] +class MSFT_ADReplicationSite : OMI_BaseResource +{ + [Write, Description("Specifies if the Active Directory replication site should be present or absent. Default value is 'Present'."), ValueMap{"Present", "Absent"}, Values{"Present", "Absent"}] String Ensure; + [Key, Description("Specifies the name of the Active Directory replication site.")] String Name; + [Write, Description("Specifies if the Default-First-Site-Name should be renamed if it exists. Default value is $false.")] Boolean RenameDefaultFirstSiteName; + [Write, Description("Specifies a description of the object. This parameter sets the value of the Description property for the object. The LDAP Display Name (ldapDisplayName) for this property is 'description'.")] String Description; +}; diff --git a/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_ADReplicationSite/README.md b/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_ADReplicationSite/README.md new file mode 100644 index 0000000..43ef6b0 --- /dev/null +++ b/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_ADReplicationSite/README.md @@ -0,0 +1,7 @@ +# Description + +The ADReplicationSite DSC resource will manage Replication Sites within Active Directory. Sites are used in Active Directory to either enable clients to discover network resources (published shares, domain controllers) close to the physical location of a client computer or to reduce network traffic over wide area network (WAN) links. Sites can also be used to optimize replication between domain controllers. + +## Requirements + +* Target machine must be running Windows Server 2008 R2 or later. diff --git a/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_ADReplicationSite/en-US/MSFT_ADReplicationSite.strings.psd1 b/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_ADReplicationSite/en-US/MSFT_ADReplicationSite.strings.psd1 new file mode 100644 index 0000000..7a30f4d --- /dev/null +++ b/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_ADReplicationSite/en-US/MSFT_ADReplicationSite.strings.psd1 @@ -0,0 +1,12 @@ +# culture='en-US' +ConvertFrom-StringData @' + AddReplicationSiteDefaultFirstSiteName = Add the replication site 'Default-First-Site-Name' to '{0}'. (ADRS0001) + AddReplicationSite = Add the replication site '{0}'. (ADRS0002) + RemoveReplicationSite = Remove the replication site '{0}'. (ADRS0003) + GetReplicationSite = Getting replication site '{0}'. (ADRS0004) + ReplicationSiteAbsent = Replication site '{0}' is not present. (ADRS0005) + ReplicationSitePresent = Replication site '{0}' is present. (ADRS0006) + ReplicationSiteInDesiredState = The replication site '{0}' is in the desired state. (ADRS0007) + ReplicationSiteNotInDesiredState = The replication site '{0}' is not in the desired state. (ADRS0008) + UpdateReplicationSite = The replication site '{0}' needs to be updated. +'@ diff --git a/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_ADReplicationSite/en-US/about_ADReplicationSite.help.txt b/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_ADReplicationSite/en-US/about_ADReplicationSite.help.txt new file mode 100644 index 0000000..eb08cab --- /dev/null +++ b/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_ADReplicationSite/en-US/about_ADReplicationSite.help.txt @@ -0,0 +1,87 @@ +.NAME + ADReplicationSite + +.DESCRIPTION + The ADReplicationSite DSC resource will manage Replication Sites within Active Directory. Sites are used in Active Directory to either enable clients to discover network resources (published shares, domain controllers) close to the physical location of a client computer or to reduce network traffic over wide area network (WAN) links. Sites can also be used to optimize replication between domain controllers. + + ## Requirements + + * Target machine must be running Windows Server 2008 R2 or later. + +.PARAMETER Ensure + Write - String + Allowed values: Present, Absent + Specifies if the Active Directory replication site should be present or absent. Default value is 'Present'. + +.PARAMETER Name + Key - String + Specifies the name of the Active Directory replication site. + +.PARAMETER RenameDefaultFirstSiteName + Write - Boolean + Specifies if the Default-First-Site-Name should be renamed if it exists. Default value is $false. + +.PARAMETER Description + Write - String + Specifies a description of the object. This parameter sets the value of the Description property for the object. The LDAP Display Name (ldapDisplayName) for this property is 'description'. + +.EXAMPLE 1 + +This configuration will create an Active Directory replication site +called 'Seattle'. + +Configuration ADReplicationSite_CreateADReplicationSite_Config +{ + Import-DscResource -Module ActiveDirectoryDsc + + Node localhost + { + ADReplicationSite 'SeattleSite' + { + Ensure = 'Present' + Name = 'Seattle' + } + } +} + +.EXAMPLE 2 + +This configuration will create an Active Directory replication site called +'Seattle'. If the 'Default-First-Site-Name' site exists, it will rename +this site instead of create a new one. + +Configuration ADReplicationSite_CreateADReplicationSiteRenameDefault_Config +{ + Import-DscResource -Module ActiveDirectoryDsc + + Node localhost + { + ADReplicationSite 'SeattleSite' + { + Ensure = 'Present' + Name = 'Seattle' + RenameDefaultFirstSiteName = $true + } + } +} + +.EXAMPLE 3 + +This configuration will remove the Active Directory replication site +called 'Cupertino'. + +Configuration ADReplicationSite_RemoveADReplicationSite_Config +{ + Import-DscResource -Module ActiveDirectoryDsc + + Node localhost + { + ADReplicationSite 'CupertinoSite' + { + Ensure = 'Absent' + Name = 'Cupertino' + } + } +} + + diff --git a/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_ADReplicationSiteLink/MSFT_ADReplicationSiteLink.psm1 b/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_ADReplicationSiteLink/MSFT_ADReplicationSiteLink.psm1 new file mode 100644 index 0000000..b5ecd8e --- /dev/null +++ b/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_ADReplicationSiteLink/MSFT_ADReplicationSiteLink.psm1 @@ -0,0 +1,605 @@ +$script:resourceModulePath = Split-Path -Path (Split-Path -Path $PSScriptRoot -Parent) -Parent +$script:modulesFolderPath = Join-Path -Path $script:resourceModulePath -ChildPath 'Modules' + +$script:localizationModulePath = Join-Path -Path $script:modulesFolderPath -ChildPath 'ActiveDirectoryDsc.Common' +Import-Module -Name (Join-Path -Path $script:localizationModulePath -ChildPath 'ActiveDirectoryDsc.Common.psm1') + +$script:localizedData = Get-LocalizedData -ResourceName 'MSFT_ADReplicationSiteLink' + +<# + .SYNOPSIS + Gets the current configuration on an AD Replication Site Link. + + .PARAMETER Name + Specifies the name of the AD Replication Site Link. + + .PARAMETER SitesExcluded + Specifies the list of sites to remove from a site link. +#> +function Get-TargetResource +{ + [CmdletBinding()] + [OutputType([System.Collections.Hashtable])] + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $Name, + + [Parameter()] + [System.String[]] + $SitesExcluded + ) + + try + { + $siteLink = Get-ADReplicationSiteLink -Identity $Name -Properties 'Description', 'Options' + } + catch [Microsoft.ActiveDirectory.Management.ADIdentityNotFoundException] + { + Write-Verbose -Message ($script:localizedData.SiteLinkNotFound -f $Name) + + $siteLink = $null + } + catch + { + $errorMessage = $script:localizedData.GetSiteLinkUnexpectedError -f $Name + New-InvalidOperationException -Message $errorMessage -ErrorRecord $_ + } + + if ($null -ne $siteLink) + { + $siteCommonNames = @() + + if ($siteLink.SitesIncluded) + { + foreach ($siteDN in $siteLink.SitesIncluded) + { + $siteCommonNames += Resolve-SiteLinkName -SiteName $siteDn + } + } + + if ($null -eq $siteLink.Options) + { + $siteLinkOptions = Get-EnabledOptions -OptionValue 0 + } + else + { + $siteLinkOptions = Get-EnabledOptions -OptionValue $siteLink.Options + } + + $sitesExcludedEvaluated = $SitesExcluded | + Where-Object -FilterScript { $_ -notin $siteCommonNames } + + $returnValue = @{ + Name = $Name + Cost = $siteLink.Cost + Description = $siteLink.Description + ReplicationFrequencyInMinutes = $siteLink.ReplicationFrequencyInMinutes + SitesIncluded = $siteCommonNames + SitesExcluded = $sitesExcludedEvaluated + OptionChangeNotification = $siteLinkOptions.USE_NOTIFY + OptionTwoWaySync = $siteLinkOptions.TWOWAY_SYNC + OptionDisableCompression = $siteLinkOptions.DISABLE_COMPRESSION + Ensure = 'Present' + } + } + else + { + $returnValue = @{ + Name = $Name + Cost = $null + Description = $null + ReplicationFrequencyInMinutes = $null + SitesIncluded = $null + SitesExcluded = $SitesExcluded + OptionChangeNotification = $false + OptionTwoWaySync = $false + OptionDisableCompression = $false + Ensure = 'Absent' + } + } + + return $returnValue +} + +<# + .SYNOPSIS + Sets the desired configuration on an AD Replication Site Link. + + .PARAMETER Name + Specifies the name of the AD Replication Site Link. + + .PARAMETER Cost + Specifies the cost to be placed on the site link. + + .PARAMETER Description + Specifies a description of the object. + + .PARAMETER ReplicationFrequencyInMinutes + Specifies the frequency (in minutes) for which replication will occur where this site link is in use between sites. + + .PARAMETER SitesIncluded + Specifies the list of sites included in the site link. + + .PARAMETER SitesExcluded + Specifies the list of sites to remove from a site link. + + .PARAMETER OptionChangeNotification + Enables or disables Change Notification Replication on a site link. Default value is $false. + + .PARAMETER OptionTwoWaySync + Two Way Sync on a site link. Default value is $false. + + .PARAMETER OptionDisableCompression + Enables or disables Compression on a site link. Default value is $false. + + .PARAMETER Ensure + Specifies if the site link is created or deleted. +#> +function Set-TargetResource +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $Name, + + [Parameter()] + [System.Int32] + $Cost, + + [Parameter()] + [System.String] + $Description, + + [Parameter()] + [System.Int32] + $ReplicationFrequencyInMinutes, + + [Parameter()] + [System.String[]] + $SitesIncluded, + + [Parameter()] + [System.String[]] + $SitesExcluded, + + [Parameter()] + [System.Boolean] + $OptionChangeNotification = $false, + + [Parameter()] + [System.Boolean] + $OptionTwoWaySync = $false, + + [Parameter()] + [System.Boolean] + $OptionDisableCompression = $false, + + [Parameter()] + [ValidateSet('Present', 'Absent')] + [System.String] + $Ensure = 'Present' + ) + + if ($Ensure -eq 'Present') + { + # Resource should be Present + $currentADSiteLink = Get-TargetResource -Name $Name + + <# + Since Set and New have different parameters we have to test if the + site link exists to determine what cmdlet we need to use. + #> + if ( $currentADSiteLink.Ensure -eq 'Absent' ) + { + # Resource is Absent + + # Modify parameters for splatting to New-ADReplicationSiteLink. + $newADReplicationSiteLinkParameters = @{ } + $PSBoundParameters + $newADReplicationSiteLinkParameters.Remove('Ensure') + $newADReplicationSiteLinkParameters.Remove('SitesExcluded') + $newADReplicationSiteLinkParameters.Remove('OptionChangeNotification') + $newADReplicationSiteLinkParameters.Remove('OptionTwoWaySync') + $newADReplicationSiteLinkParameters.Remove('OptionDisableCompression') + $newADReplicationSiteLinkParameters.Remove('Verbose') + + $optionsValue = ConvertTo-EnabledOptions -OptionChangeNotification $optionChangeNotification ` + -OptionTwoWaySync $optionTwoWaySync -OptionDisableCompression $optionDisableCompression + + if ($optionsValue -gt 0) + { + $newADReplicationSiteLinkParameters['OtherAttributes'] = @{ + options = $optionsValue + } + } + + Write-Verbose -Message ($script:localizedData.NewSiteLink -f $Name) + New-ADReplicationSiteLink @newADReplicationSiteLinkParameters + } + else + { + # Resource is Present + + $setADReplicationSiteLinkParameters = @{ } + $setADReplicationSiteLinkParameters['Identity'] = $Name + + $replaceParameters = @{ } + + # now we have to determine if we need to add or remove sites from SitesIncluded. + if (-not (Test-Members -ExistingMembers $currentADSiteLink.SitesIncluded ` + -MembersToInclude $SitesIncluded -MembersToExclude $SitesExcluded)) + { + # build the SitesIncluded hashtable. + $sitesIncludedParameters = @{ } + if ($SitesExcluded) + { + Write-Verbose -Message ($script:localizedData.RemovingSites -f $($SitesExcluded -join ', '), $Name) + + <# + Wrapped in $() as we were getting some weird results without it, + results were not being added into Hashtable as strings. + #> + $sitesIncludedParameters.Add('Remove', $($SitesExcluded)) + } + + if ($SitesIncluded) + { + Write-Verbose -Message ($script:localizedData.AddingSites -f $($SitesIncluded -join ', '), $Name) + + <# + Wrapped in $() as we were getting some weird results without it, + results were not being added into Hashtable as strings. + #> + $sitesIncludedParameters.Add('Add', $($SitesIncluded)) + } + + if ($null -ne $($sitesIncludedParameters.Keys)) + { + $setADReplicationSiteLinkParameters['SitesIncluded'] = $sitesIncludedParameters + } + } + + if ($PSBoundParameters.ContainsKey('Cost') -and ` + $Cost -ne $currentADSiteLink.Cost) + { + Write-Verbose -Message ($script:localizedData.SettingProperty -f + 'Cost', $Cost, $Name) + $setADReplicationSiteLinkParameters['Cost'] = $Cost + } + + if ($PSBoundParameters.ContainsKey('Description') -and ` + $Description -ne $currentADSiteLink.Description) + { + Write-Verbose -Message ($script:localizedData.SettingProperty -f + 'Description', $Description, $Name) + $setADReplicationSiteLinkParameters['Description'] = $Description + } + + if ($PSBoundParameters.ContainsKey('ReplicationFrequencyInMinutes') -and ` + $ReplicationFrequencyInMinutes -ne $currentADSiteLink.ReplicationFrequencyInMinutes) + { + Write-Verbose -Message ($script:localizedData.SettingProperty -f + 'ReplicationFrequencyInMinutes', $ReplicationFrequencyInMinutes, $Name) + $setADReplicationSiteLinkParameters['ReplicationFrequencyInMinutes'] = $ReplicationFrequencyInMinutes + } + + if ($PSBoundParameters.ContainsKey('OptionChangeNotification') -and ` + $OptionChangeNotification -ne $currentADSiteLink.OptionChangeNotification) + { + Write-Verbose -Message ($script:localizedData.SettingProperty -f + 'OptionChangeNotification', $OptionChangeNotification, $Name) + $changeNotification = $OptionChangeNotification + } + else + { + $changeNotification = $currentADSiteLink.OptionChangeNotification + } + + if ($PSBoundParameters.ContainsKey('OptionTwoWaySync') -and ` + $OptionTwoWaySync -ne $currentADSiteLink.OptionTwoWaySync) + { + Write-Verbose -Message ($script:localizedData.SettingProperty -f + 'TwoWaySync', $OptionTwoWaySync, $Name) + $twoWaySync = $OptionTwoWaySync + } + else + { + $twoWaySync = $currentADSiteLink.OptionTwoWaySync + } + + if ($PSBoundParameters.ContainsKey('OptionDisableCompression') -and ` + $OptionDisableCompression -ne $currentADSiteLink.OptionDisableCompression) + { + Write-Verbose -Message ($script:localizedData.SettingProperty -f + 'OptionDisableCompression', $OptionDisableCompression, $Name) + $disableCompression = $OptionDisableCompression + } + else + { + $disableCompression = $currentADSiteLink.OptionDisableCompression + } + + $optionsValue = ConvertTo-EnabledOptions -OptionChangeNotification $changeNotification ` + -OptionTwoWaySync $twoWaySync -OptionDisableCompression $disableCompression + + if ($optionsValue -gt 0) + { + $setADReplicationSiteLinkParameters.Add('Clear', 'Options') + } + else + { + $replaceParameters.Add('Options', $optionsValue) + } + + if ($replaceParameters.Count -gt 0) + { + $setADReplicationSiteLinkParameters.Add('Replace', $replaceParameters) + } + + Set-ADReplicationSiteLink @setADReplicationSiteLinkParameters + } + } + else + { + # Resource should be absent + + Write-Verbose -Message ($script:localizedData.RemoveSiteLink -f $Name) + + Remove-ADReplicationSiteLink -Identity $Name + } +} + +<# + .SYNOPSIS + Tests if the AD Replication Site Link is in a desired state. + + .PARAMETER Name + Specifies the name of the AD Replication Site Link. + + .PARAMETER Cost + Specifies the cost to be placed on the site link. + + .PARAMETER Description + Specifies a description of the object. + + .PARAMETER ReplicationFrequencyInMinutes + Specifies the frequency (in minutes) for which replication will occur where this site link is in use between sites. + + .PARAMETER SitesIncluded + Specifies the list of sites included in the site link. + + .PARAMETER SitesExcluded + Specifies the list of sites to remove from a site link. + + .PARAMETER OptionChangeNotification + Enables or disables Change Notification Replication on a site link. Default value is $false. + + .PARAMETER OptionTwoWaySync + Two Way Sync on a site link. Default value is $false. + + .PARAMETER OptionDisableCompression + Enables or disables Compression on a site link. Default value is $false. + + .PARAMETER Ensure + Specifies if the site link is created or deleted. +#> +function Test-TargetResource +{ + [CmdletBinding()] + [OutputType([System.Boolean])] + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $Name, + + [Parameter()] + [System.Int32] + $Cost, + + [Parameter()] + [System.String] + $Description, + + [Parameter()] + [System.Int32] + $ReplicationFrequencyInMinutes, + + [Parameter()] + [System.String[]] + $SitesIncluded, + + [Parameter()] + [System.String[]] + $SitesExcluded, + + [Parameter()] + [System.Boolean] + $OptionChangeNotification = $false, + + [Parameter()] + [System.Boolean] + $OptionTwoWaySync = $false, + + [Parameter()] + [System.Boolean] + $OptionDisableCompression = $false, + + [Parameter()] + [ValidateSet('Present', 'Absent')] + [System.String] + $Ensure = 'Present' + ) + + $isCompliant = $true + $currentSiteLink = Get-TargetResource -Name $Name + + # Test for Ensure. + if ($Ensure -ne $currentSiteLink.Ensure) + { + return $false + } + + # Test for SitesIncluded. + foreach ($desiredIncludedSite in $SitesIncluded) + { + if ($desiredIncludedSite -notin $currentSiteLink.SitesIncluded) + { + Write-Verbose -Message ($script:localizedData.SiteNotFound -f $desiredIncludedSite, $($currentSiteLink.SitesIncluded -join ', ')) + $isCompliant = $false + } + } + + # Test for SitesExcluded. + foreach ($desiredExcludedSite in $SitesExcluded) + { + if ($desiredExcludedSite -in $currentSiteLink.SitesIncluded) + { + Write-Verbose -Message ($script:localizedData.SiteFoundInExcluded -f $desiredExcludedSite, $($currentSiteLink.SitesIncluded -join ', ')) + $isCompliant = $false + } + } + + foreach ($parameter in $PSBoundParameters.Keys) + { + # Test for Description|ReplicationFrequencyInMinutes|Cost. + if ($parameter -match 'Description|ReplicationFrequencyInMinutes|Cost|OptionChangeNotification|OptionTwoWaySync|OptionDisableCompression') + { + if ($PSBoundParameters[$parameter] -ne $currentSiteLink[$parameter]) + { + Write-Verbose -Message ($script:localizedData.PropertyNotInDesiredState -f $parameter, $($currentSiteLink[$parameter]), $($PSBoundParameters[$parameter])) + $isCompliant = $false + } + } + } + + return $isCompliant +} + +<# + .SYNOPSIS + Resolves the AD replication site link distinguished names to short names + + .PARAMETER SiteName + Specifies the distinguished name of a AD replication site link + + .EXAMPLE + PS C:\> Resolve-SiteLinkName -SiteName 'CN=Site1,CN=Sites,CN=Configuration,DC=contoso,DC=com' + Site1 +#> +function Resolve-SiteLinkName +{ + [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseCmdletCorrectly", "")] + [OutputType([System.String])] + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $SiteName + ) + + $adSite = Get-ADReplicationSite -Identity $SiteName + + return $adSite.Name +} + +<# + .SYNOPSIS + Calculates the options enabled on a Site Link + + .PARAMETER OptionValue + The value of currently enabled options +#> +function Get-EnabledOptions +{ + [OutputType([System.Collections.Hashtable])] + [CmdletBinding()] + + param + ( + [Parameter(Mandatory = $true)] + [System.Int32] + $OptionValue + ) + + $returnValue = @{ + USE_NOTIFY = $false + TWOWAY_SYNC = $false + DISABLE_COMPRESSION = $false + } + + if (1 -band $optionValue) + { + $returnValue.USE_NOTIFY = $true + } + + if (2 -band $optionValue) + { + $returnValue.TWOWAY_SYNC = $true + } + + if (4 -band $optionValue) + { + $returnValue.DISABLE_COMPRESSION = $true + } + + return $returnValue +} + +<# + .SYNOPSIS + Calculates the options value for the given choices + + .PARAMETER OptionChangeNotification + Enable/Disable Change notification replication + + .PARAMETER OptionTwoWaySync + Enable/Disable Two Way sync + + .PARAMETER OptionDisableCompression + Enable/Disable Compression +#> +function ConvertTo-EnabledOptions +{ + [OutputType([System.Int32])] + [CmdletBinding()] + param + ( + [Parameter()] + [System.Boolean] + $OptionChangeNotification, + + [Parameter()] + [System.Boolean] + $OptionTwoWaySync, + + [Parameter()] + [System.Boolean] + $OptionDisableCompression + ) + + $returnValue = 0 + + if ($OptionChangeNotification) + { + $returnValue = $returnValue + 1 + } + + if ($OptionTwoWaySync) + { + $returnValue = $returnValue + 2 + } + + if ($OptionDisableCompression) + { + $returnValue = $returnValue + 4 + } + + return $returnValue +} + +Export-ModuleMember -Function *-TargetResource diff --git a/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_ADReplicationSiteLink/MSFT_ADReplicationSiteLink.schema.mof b/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_ADReplicationSiteLink/MSFT_ADReplicationSiteLink.schema.mof new file mode 100644 index 0000000..533d01a --- /dev/null +++ b/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_ADReplicationSiteLink/MSFT_ADReplicationSiteLink.schema.mof @@ -0,0 +1,15 @@ + +[ClassVersion("1.0.0.0"), FriendlyName("ADReplicationSiteLink")] +class MSFT_ADReplicationSiteLink : OMI_BaseResource +{ + [Key, Description("Specifies the name of the site link.")] String Name; + [Write, Description("Specifies the cost to be placed on the site link.")] SInt32 Cost; + [Write, Description("This parameter sets the value of the Description property for the object.")] String Description; + [Write, Description("Species the frequency (in minutes) for which replication will occur where this site link is in use between sites.")] Sint32 ReplicationFrequencyInMinutes; + [Write, Description("Specifies the list of sites included in the site link.")] String SitesIncluded[]; + [Write, Description("Specifies the list of sites to exclude from the site link.")] String SitesExcluded[]; + [Write, Description("Specifies if the site link should be present or absent. Default value is 'Present'."), ValueMap{"Present","Absent"}, Values{"Present","Absent"}] String Ensure; + [Write, Description("Enables or disables Change Notification Replication on a site link. Default value is $false.")] Boolean OptionChangeNotification; + [Write, Description("Enables or disables Two Way Sync on a site link. Default value is $false.")] Boolean OptionTwoWaySync; + [Write, Description("Enables or disables Compression on a site link. Default value is $false.")] Boolean OptionDisableCompression; +}; diff --git a/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_ADReplicationSiteLink/README.md b/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_ADReplicationSiteLink/README.md new file mode 100644 index 0000000..dc6e75b --- /dev/null +++ b/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_ADReplicationSiteLink/README.md @@ -0,0 +1,7 @@ +# Description + +The ADReplicationSiteLink DSC resource will manage Replication Site Links within Active Directory. A site link connects two or more sites. Site links reflect the administrative policy for how sites are to be interconnected and the methods used to transfer replication traffic. You must connect sites with site links so that domain controllers at each site can replicate Active Directory changes. + +## Requirements + +* Target machine must be running Windows Server 2008 R2 or later. diff --git a/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_ADReplicationSiteLink/en-US/MSFT_ADReplicationSiteLink.strings.psd1 b/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_ADReplicationSiteLink/en-US/MSFT_ADReplicationSiteLink.strings.psd1 new file mode 100644 index 0000000..c603ff7 --- /dev/null +++ b/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_ADReplicationSiteLink/en-US/MSFT_ADReplicationSiteLink.strings.psd1 @@ -0,0 +1,12 @@ +ConvertFrom-StringData @' + SiteNotFound = Site: '{0}' not found in SitesIncluded. Current SitesIncluded: '{1}'. (ADRSL0001) + SiteFoundInExcluded = Excluded '{0}' site found in SitesIncluded. Current SitesIncluded: '{1}'. (ADRSL0002) + PropertyNotInDesiredState = '{0}' is not in desired state Current: '{1}' Desired: '{2}'. (ADRSL0003) + SettingProperty = Setting property '{0}' to '{1}' on site link '{2}'. (ADRSL0004) + RemovingSites = Removing sites '{0}' from site link '{1}'. (ADRSL0005) + AddingSites = Adding sites '{0}' to site link '{1}'. (ADRSL0006) + NewSiteLink = Creating AD Site Link '{0}'. (ADRSL0007) + RemoveSiteLink = Removing AD Site Link '{0}'. (ADRSL0008) + SiteLinkNotFound = Could not find '{0}' site link. (ADRSL0009) + GetSiteLinkUnexpectedError = Unexpected error getting site link '{0}'. (ADRSL0010) +'@ diff --git a/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_ADReplicationSiteLink/en-US/about_ADReplicationSiteLink.help.txt b/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_ADReplicationSiteLink/en-US/about_ADReplicationSiteLink.help.txt new file mode 100644 index 0000000..a8fcd52 --- /dev/null +++ b/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_ADReplicationSiteLink/en-US/about_ADReplicationSiteLink.help.txt @@ -0,0 +1,120 @@ +.NAME + ADReplicationSiteLink + +.DESCRIPTION + The ADReplicationSiteLink DSC resource will manage Replication Site Links within Active Directory. A site link connects two or more sites. Site links reflect the administrative policy for how sites are to be interconnected and the methods used to transfer replication traffic. You must connect sites with site links so that domain controllers at each site can replicate Active Directory changes. + + ## Requirements + + * Target machine must be running Windows Server 2008 R2 or later. + +.PARAMETER Name + Key - String + Specifies the name of the site link. + +.PARAMETER Cost + Write - SInt32 + Specifies the cost to be placed on the site link. + +.PARAMETER Description + Write - String + This parameter sets the value of the Description property for the object. + +.PARAMETER ReplicationFrequencyInMinutes + Write - SInt32 + Species the frequency (in minutes) for which replication will occur where this site link is in use between sites. + +.PARAMETER SitesIncluded + Write - StringArray + Specifies the list of sites included in the site link. + +.PARAMETER SitesExcluded + Write - StringArray + Specifies the list of sites to exclude from the site link. + +.PARAMETER Ensure + Write - String + Allowed values: Present, Absent + Specifies if the site link should be present or absent. Default value is 'Present'. + +.PARAMETER OptionChangeNotification + Write - Boolean + Enables or disables Change Notification Replication on a site link. Default value is $false. + +.PARAMETER OptionTwoWaySync + Write - Boolean + Enables or disables Two Way Sync on a site link. Default value is $false. + +.PARAMETER OptionDisableCompression + Write - Boolean + Enables or disables Compression on a site link. Default value is $false. + +.EXAMPLE 1 + +This configuration will create an AD Replication Site Link. + +Configuration ADReplicationSiteLink_CreateReplicationSiteLink_Config +{ + Import-DscResource -Module ActiveDirectoryDsc + + Node localhost + { + ADReplicationSiteLink 'HQSiteLink' + { + Name = 'HQSiteLInk' + SitesIncluded = @('site1', 'site2') + Cost = 100 + ReplicationFrequencyInMinutes = 15 + Ensure = 'Present' + } + } +} + +.EXAMPLE 2 + +This configuration will modify an existing AD Replication Site Link. + +Configuration ADReplicationSiteLink_ModifyExistingReplicationSiteLink_Config +{ + Import-DscResource -Module ActiveDirectoryDsc + + Node localhost + { + ADReplicationSiteLink 'HQSiteLink' + { + Name = 'HQSiteLInk' + SitesIncluded = 'site1' + SitesExcluded = 'site2' + Cost = 100 + ReplicationFrequencyInMinutes = 20 + Ensure = 'Present' + } + } +} + +.EXAMPLE 3 + +This configuration will modify an existing AD Replication Site Link by enabling Replication Options. + +Configuration ADReplicationSiteLink_EnableOptions_Config +{ + Import-DscResource -Module ActiveDirectoryDsc + + Node localhost + { + ADReplicationSiteLink 'HQSiteLink' + { + Name = 'HQSiteLInk' + SitesIncluded = 'site1' + SitesExcluded = 'site2' + Cost = 100 + ReplicationFrequencyInMinutes = 20 + OptionChangeNotification = $true + OptionTwoWaySync = $true + OptionDisableCompression = $true + Ensure = 'Present' + } + } +} + + diff --git a/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_ADReplicationSubnet/MSFT_ADReplicationSubnet.psm1 b/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_ADReplicationSubnet/MSFT_ADReplicationSubnet.psm1 new file mode 100644 index 0000000..9584c34 --- /dev/null +++ b/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_ADReplicationSubnet/MSFT_ADReplicationSubnet.psm1 @@ -0,0 +1,273 @@ +$script:resourceModulePath = Split-Path -Path (Split-Path -Path $PSScriptRoot -Parent) -Parent +$script:modulesFolderPath = Join-Path -Path $script:resourceModulePath -ChildPath 'Modules' + +$script:localizationModulePath = Join-Path -Path $script:modulesFolderPath -ChildPath 'ActiveDirectoryDsc.Common' +Import-Module -Name (Join-Path -Path $script:localizationModulePath -ChildPath 'ActiveDirectoryDsc.Common.psm1') + +$script:localizedData = Get-LocalizedData -ResourceName 'MSFT_ADReplicationSubnet' + +<# + .SYNOPSIS + Returns the current state of the replication subnet. + + .PARAMETER Name + The name of the AD replication subnet, e.g. 10.0.0.0/24. + + .PARAMETER Site + The name of the assigned AD replication site, e.g. Default-First-Site-Name. +#> +function Get-TargetResource +{ + [CmdletBinding()] + [OutputType([System.Collections.Hashtable])] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $Name, + + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $Site + ) + + <# + Get the replication subnet filtered by it's name. If the subnet is not + present, the command will return $null. + #> + Write-Verbose -Message ($script:localizedData.GetReplicationSubnet -f $Name) + + $replicationSubnet = Get-ADReplicationSubnet -Filter { Name -eq $Name } -Properties Description + + if ($null -eq $replicationSubnet) + { + # Replication subnet not found, return absent. + Write-Verbose -Message ($script:localizedData.ReplicationSubnetAbsent -f $Name) + + $returnValue = @{ + Ensure = 'Absent' + Name = $Name + Site = '' + Location = $null + Description = $null + } + } + else + { + # Get the name of the replication site, if it's not empty. + $replicationSiteName = '' + + if ($null -ne $replicationSubnet.Site) + { + $replicationSiteName = Get-ADObject -Identity $replicationSubnet.Site | + Select-Object -ExpandProperty 'Name' + } + + # Replication subnet found, return present. + Write-Verbose -Message ($script:localizedData.ReplicationSubnetPresent -f $Name) + + $returnValue = @{ + Ensure = 'Present' + Name = $Name + Site = $replicationSiteName + Location = [System.String] $replicationSubnet.Location + Description = [System.String] $replicationSubnet.Description + } + } + + return $returnValue +} + +<# + .SYNOPSIS + Add, remove or update the replication subnet. + + .PARAMETER Ensure + Specifies if the AD replication subnet should be added or remove. Default value is 'Present'. + + .PARAMETER Name + The name of the AD replication subnet, e.g. 10.0.0.0/24. + + .PARAMETER Site + The name of the assigned AD replication site, e.g. Default-First-Site-Name. + + .PARAMETER Location + The location for the AD replication site. Default value is empty. +#> +function Set-TargetResource +{ + [CmdletBinding()] + param + ( + [Parameter()] + [ValidateSet('Present', 'Absent')] + [System.String] + $Ensure = 'Present', + + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $Name, + + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $Site, + + [Parameter()] + [System.String] + $Location = '', + + [Parameter()] + [System.String] + $Description + ) + + <# + Get the replication subnet filtered by it's name. If the subnet is not + present, the command will return $null. + #> + $replicationSubnet = Get-ADReplicationSubnet -Filter { Name -eq $Name } + + if ($Ensure -eq 'Present') + { + # Add the replication subnet, if it does not exist. + if ($null -eq $replicationSubnet) + { + Write-Verbose -Message ($script:localizedData.CreateReplicationSubnet -f $Name) + + $replicationSubnet = New-ADReplicationSubnet -Name $Name -Site $Site -PassThru + } + + <# + Get the name of the replication site, if it's not empty and update the + site if it's not vaild. + #> + if ($null -ne $replicationSubnet.Site) + { + $replicationSiteName = Get-ADObject -Identity $replicationSubnet.Site | + Select-Object -ExpandProperty 'Name' + } + + if ($replicationSiteName -ne $Site) + { + Write-Verbose -Message ($script:localizedData.SetReplicationSubnetSite -f $Name, $Site) + + Set-ADReplicationSubnet -Identity $replicationSubnet.DistinguishedName -Site $Site -PassThru + } + + <# + Update the location, if it's not valid. Ensure an empty location + string is converted to $null, because the Set-ADReplicationSubnet + does not accept an empty string for the location, but $null. + #> + $nullableLocation = $Location + if ([System.String]::IsNullOrEmpty($Location)) + { + $nullableLocation = $null + } + + if ($replicationSubnet.Location -ne $nullableLocation) + { + Write-Verbose -Message ($script:localizedData.SetReplicationSubnetLocation -f $Name, $nullableLocation) + + Set-ADReplicationSubnet -Identity $replicationSubnet.DistinguishedName -Location $nullableLocation -PassThru + } + + if ($PSBoundParameters.ContainsKey('Description')) + { + if ($replicationSubnet.Description -ne $Description) + { + Write-Verbose -Message ($script:localizedData.SetReplicationSubnetDescription -f $Name, $Description) + + Set-ADReplicationSubnet -Identity $replicationSubnet.DistinguishedName -Description $Description + } + } + } + + if ($Ensure -eq 'Absent') + { + # Remove the replication subnet, if it exists. + if ($null -ne $replicationSubnet) + { + Write-Verbose -Message ($script:localizedData.RemoveReplicationSubnet -f $Name) + + Remove-ADReplicationSubnet -Identity $replicationSubnet.DistinguishedName -Confirm:$false + } + } +} + +<# + .SYNOPSIS + Test the replication subnet. + + .PARAMETER Ensure + Specifies if the AD replication subnet should be added or remove. Default value is 'Present'. + + .PARAMETER Name + The name of the AD replication subnet, e.g. 10.0.0.0/24. + + .PARAMETER Site + The name of the assigned AD replication site, e.g. Default-First-Site-Name. + + .PARAMETER Location + The location for the AD replication site. Default value is empty. + + .PARAMETER Description + Specifies a description of the object. This parameter sets the value of the Description property for the object. +#> +function Test-TargetResource +{ + [CmdletBinding()] + [OutputType([System.Boolean])] + param + ( + [Parameter()] + [ValidateSet('Present', 'Absent')] + [System.String] + $Ensure = 'Present', + + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $Name, + + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $Site, + + [Parameter()] + [System.String] + $Location = '', + + [Parameter()] + [System.String] + $Description + ) + + $currentConfiguration = Get-TargetResource -Name $Name -Site $Site + + $desiredConfigurationMatch = $currentConfiguration.Ensure -eq $Ensure + + if ($Ensure -eq 'Present') + { + $desiredConfigurationMatch = $desiredConfigurationMatch -and + $currentConfiguration.Site -eq $Site -and + $currentConfiguration.Location -eq $Location -and + $currentConfiguration.Description -eq $Description + } + + if ($desiredConfigurationMatch) + { + Write-Verbose -Message ($script:localizedData.ReplicationSubnetInDesiredState -f $Name) + } + else + { + Write-Verbose -Message ($script:localizedData.ReplicationSubnetNotInDesiredState -f $Name) + } + + return $desiredConfigurationMatch +} diff --git a/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_ADReplicationSubnet/MSFT_ADReplicationSubnet.schema.mof b/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_ADReplicationSubnet/MSFT_ADReplicationSubnet.schema.mof new file mode 100644 index 0000000..e3535e9 --- /dev/null +++ b/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_ADReplicationSubnet/MSFT_ADReplicationSubnet.schema.mof @@ -0,0 +1,9 @@ +[ClassVersion("1.0.0.0"), FriendlyName("ADReplicationSubnet")] +class MSFT_ADReplicationSubnet : OMI_BaseResource +{ + [Write, Description("Specifies if the Active Directory replication subnet should be present or absent. Default value is 'Present'."), ValueMap{"Present", "Absent"}, Values{"Present", "Absent"}] String Ensure; + [Key, Description("The name of the Active Directory replication subnet, e.g. 10.0.0.0/24.")] String Name; + [Required, Description("The name of the assigned Active Directory replication site, e.g. Default-First-Site-Name.")] String Site; + [Write, Description("The location for the Active Directory replication site. Default value is empty ('') location.")] String Location; + [Write, Description("Specifies a description of the object. This parameter sets the value of the Description property for the object.")] String Description; +}; diff --git a/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_ADReplicationSubnet/README.md b/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_ADReplicationSubnet/README.md new file mode 100644 index 0000000..1826571 --- /dev/null +++ b/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_ADReplicationSubnet/README.md @@ -0,0 +1,7 @@ +# Description + +The ADReplicationSubnet DSC resource will manage replication subnets. + +## Requirements + +* Target machine must be running Windows Server 2008 R2 or later. diff --git a/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_ADReplicationSubnet/en-US/MSFT_ADReplicationSubnet.strings.psd1 b/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_ADReplicationSubnet/en-US/MSFT_ADReplicationSubnet.strings.psd1 new file mode 100644 index 0000000..ea55efb --- /dev/null +++ b/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_ADReplicationSubnet/en-US/MSFT_ADReplicationSubnet.strings.psd1 @@ -0,0 +1,13 @@ +# culture='en-US' +ConvertFrom-StringData @' + CreateReplicationSubnet = Create the replication subnet '{0}'. (ADRS0001) + RemoveReplicationSubnet = Remove the replication subnet '{0}'. (ADRS0002) + GetReplicationSubnet = Getting replication subnet '{0}'. (ADRS0003) + SetReplicationSubnetSite = Set the replication subnet '{0}' site to '{1}'. (ADRS0004) + SetReplicationSubnetLocation = Set the replication subnet '{0}' location to '{1}'. (ADRS0005) + ReplicationSubnetAbsent = Replication subnet '{0}' is absent. (ADRS0006) + ReplicationSubnetPresent = Replication subnet '{0}' is present. (ADRS0007) + ReplicationSubnetInDesiredState = The replication subnet '{0}' is in the desired state. (ADRS0008) + ReplicationSubnetNotInDesiredState = The replication subnet '{0}' is not in the desired state. (ADRS0009) + SetReplicationSubnetDescription = Set the replication subnet '{0}' description to '{1}'. (ADRS0010) +'@ diff --git a/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_ADReplicationSubnet/en-US/about_ADReplicationSubnet.help.txt b/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_ADReplicationSubnet/en-US/about_ADReplicationSubnet.help.txt new file mode 100644 index 0000000..6e9d4c0 --- /dev/null +++ b/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_ADReplicationSubnet/en-US/about_ADReplicationSubnet.help.txt @@ -0,0 +1,52 @@ +.NAME + ADReplicationSubnet + +.DESCRIPTION + The ADReplicationSubnet DSC resource will manage replication subnets. + + ## Requirements + + * Target machine must be running Windows Server 2008 R2 or later. + +.PARAMETER Ensure + Write - String + Allowed values: Present, Absent + Specifies if the Active Directory replication subnet should be present or absent. Default value is 'Present'. + +.PARAMETER Name + Key - String + The name of the Active Directory replication subnet, e.g. 10.0.0.0/24. + +.PARAMETER Site + Required - String + The name of the assigned Active Directory replication site, e.g. Default-First-Site-Name. + +.PARAMETER Location + Write - String + The location for the Active Directory replication site. Default value is empty ('') location. + +.PARAMETER Description + Write - String + Specifies a description of the object. This parameter sets the value of the Description property for the object. + +.EXAMPLE 1 + +This configuration will create an AD Replication Subnet. + +Configuration ADReplicationSubnet_CreateReplicationSubnet_Config +{ + Import-DscResource -Module ActiveDirectoryDsc + + Node localhost + { + ADReplicationSubnet 'LondonSubnet' + { + Name = '10.0.0.0/24' + Site = 'London' + Location = 'Datacenter 3' + Description = 'Datacenter Management Subnet' + } + } +} + + diff --git a/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_ADServicePrincipalName/MSFT_ADServicePrincipalName.psm1 b/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_ADServicePrincipalName/MSFT_ADServicePrincipalName.psm1 new file mode 100644 index 0000000..a2b6a28 --- /dev/null +++ b/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_ADServicePrincipalName/MSFT_ADServicePrincipalName.psm1 @@ -0,0 +1,207 @@ +$script:resourceModulePath = Split-Path -Path (Split-Path -Path $PSScriptRoot -Parent) -Parent +$script:modulesFolderPath = Join-Path -Path $script:resourceModulePath -ChildPath 'Modules' + +$script:localizationModulePath = Join-Path -Path $script:modulesFolderPath -ChildPath 'ActiveDirectoryDsc.Common' +Import-Module -Name (Join-Path -Path $script:localizationModulePath -ChildPath 'ActiveDirectoryDsc.Common.psm1') + +$script:localizedData = Get-LocalizedData -ResourceName 'MSFT_ADServicePrincipalName' + +<# + .SYNOPSIS + Returns the current state of the specified service principal name. + + .PARAMETER ServicePrincipalName + The full SPN to add or remove, e.g. HOST/LON-DC1. +#> +function Get-TargetResource +{ + [CmdletBinding()] + [OutputType([System.Collections.Hashtable])] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $ServicePrincipalName + ) + + Write-Verbose -Message ($script:localizedData.GetServicePrincipalName -f $ServicePrincipalName) + + $spnAccounts = Get-ADObject -Filter { ServicePrincipalName -eq $ServicePrincipalName } -Properties 'SamAccountName' | + Select-Object -ExpandProperty 'SamAccountName' + + if ($spnAccounts.Count -eq 0) + { + # No SPN found + Write-Verbose -Message ($script:localizedData.ServicePrincipalNameAbsent -f $ServicePrincipalName) + + $returnValue = @{ + Ensure = 'Absent' + ServicePrincipalName = $ServicePrincipalName + Account = '' + } + } + else + { + # One or more SPN(s) found, return the account name(s) + Write-Verbose -Message ($script:localizedData.ServicePrincipalNamePresent -f $ServicePrincipalName, ($spnAccounts -join ';')) + + $returnValue = @{ + Ensure = 'Present' + ServicePrincipalName = $ServicePrincipalName + Account = $spnAccounts -join ';' + } + } + + return $returnValue +} + +<# + .SYNOPSIS + Add or remove the service principal name. + + .PARAMETER Ensure + Specifies if the service principal name should be added or removed. + + .PARAMETER ServicePrincipalName + The full SPN to add or remove, e.g. HOST/LON-DC1. + + .PARAMETER Account + The user or computer account to add or remove the SPN to , e.g. User1 or + LON-DC1$. +#> +function Set-TargetResource +{ + [CmdletBinding()] + param + ( + [Parameter()] + [ValidateSet('Present', 'Absent')] + [System.String] + $Ensure = 'Present', + + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $ServicePrincipalName, + + [Parameter()] + [AllowEmptyString()] + [System.String] + $Account + ) + + # Get all Active Directory object having the target SPN configured. + $spnAccounts = Get-ADObject -Filter { ServicePrincipalName -eq $ServicePrincipalName } -Properties 'SamAccountName', 'DistinguishedName' + + if ($Ensure -eq 'Present') + { + <# + Throw an exception, if no account was specified or the account does + not exist. + #> + if ([System.String]::IsNullOrEmpty($Account) -or ($null -eq (Get-ADObject -Filter { SamAccountName -eq $Account }))) + { + throw ($script:localizedData.AccountNotFound -f $Account) + } + + # Remove the SPN(s) from any extra account. + foreach ($spnAccount in $spnAccounts) + { + if ($spnAccount.SamAccountName -ne $Account) + { + Write-Verbose -Message ($script:localizedData.RemoveServicePrincipalName -f $ServicePrincipalName, $spnAccount.SamAccountName) + + Set-ADObject -Identity $spnAccount.DistinguishedName -Remove @{ + ServicePrincipalName = $ServicePrincipalName + } + } + } + + <# + Add the SPN to the target account. Use Get-ADObject to get the target + object filtered by SamAccountName. Set-ADObject does not support the + field SamAccountName as Identifier. + #> + if ($spnAccounts.SamAccountName -notcontains $Account) + { + Write-Verbose -Message ($script:localizedData.AddServicePrincipalName -f $ServicePrincipalName, $Account) + + Get-ADObject -Filter { SamAccountName -eq $Account } | + Set-ADObject -Add @{ + ServicePrincipalName = $ServicePrincipalName + } + } + } + + # Remove the SPN from any account + if ($Ensure -eq 'Absent') + { + foreach ($spnAccount in $spnAccounts) + { + Write-Verbose -Message ($script:localizedData.RemoveServicePrincipalName -f $ServicePrincipalName, $spnAccount.SamAccountName) + + Set-ADObject -Identity $spnAccount.DistinguishedName -Remove @{ + ServicePrincipalName = $ServicePrincipalName + } + } + } +} + +<# + .SYNOPSIS + Tests the service principal name. + + .PARAMETER Ensure + Specifies if the service principal name should be added or removed. + + .PARAMETER ServicePrincipalName + The full SPN to add or remove, e.g. HOST/LON-DC1. + + .PARAMETER Account + The user or computer account to add or remove the SPN to, e.g. User1 or + LON-DC1$. +#> +function Test-TargetResource +{ + [CmdletBinding()] + [OutputType([System.Boolean])] + param + ( + [Parameter()] + [ValidateSet('Present', 'Absent')] + [System.String] + $Ensure = 'Present', + + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $ServicePrincipalName, + + [Parameter()] + [AllowEmptyString()] + [System.String] + $Account + ) + + $currentConfiguration = Get-TargetResource -ServicePrincipalName $ServicePrincipalName + + $desiredConfigurationMatch = $currentConfiguration.Ensure -eq $Ensure + + if ($Ensure -eq 'Present') + { + $desiredConfigurationMatch = $desiredConfigurationMatch -and + $currentConfiguration.Account -eq $Account + } + + if ($desiredConfigurationMatch) + { + Write-Verbose -Message ($script:localizedData.ServicePrincipalNameInDesiredState -f $ServicePrincipalName) + } + else + { + Write-Verbose -Message ($script:localizedData.ServicePrincipalNameNotInDesiredState -f $ServicePrincipalName) + } + + return $desiredConfigurationMatch +} diff --git a/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_ADServicePrincipalName/MSFT_ADServicePrincipalName.schema.mof b/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_ADServicePrincipalName/MSFT_ADServicePrincipalName.schema.mof new file mode 100644 index 0000000..2634d76 --- /dev/null +++ b/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_ADServicePrincipalName/MSFT_ADServicePrincipalName.schema.mof @@ -0,0 +1,7 @@ +[ClassVersion("1.0.0.0"), FriendlyName("ADServicePrincipalName")] +class MSFT_ADServicePrincipalName : OMI_BaseResource +{ + [Write, Description("Specifies if the service principal name should be added or removed. Default value is 'Present'."), ValueMap{"Present", "Absent"}, Values{"Present", "Absent"}] String Ensure; + [Key, Description("The full SPN to add or remove, e.g. HOST/LON-DC1.")] String ServicePrincipalName; + [Write, Description("The user or computer account to add or remove the SPN to, e.g. User1 or LON-DC1$. Default value is ''. If Ensure is set to Present, a value must be specified.")] String Account; +}; diff --git a/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_ADServicePrincipalName/README.md b/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_ADServicePrincipalName/README.md new file mode 100644 index 0000000..fbad07f --- /dev/null +++ b/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_ADServicePrincipalName/README.md @@ -0,0 +1,7 @@ +# Description + +The ADServicePrincipalName DSC resource will manage service principal names. A service principal name (SPN) is a unique identifier of a service instance. SPNs are used by Kerberos authentication to associate a service instance with a service logon account. This allows a client application to request that the service authenticate an account even if the client does not have the account name. + +## Requirements + +* Target machine must be running Windows Server 2008 R2 or later. diff --git a/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_ADServicePrincipalName/en-US/MSFT_ADServicePrincipalName.strings.psd1 b/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_ADServicePrincipalName/en-US/MSFT_ADServicePrincipalName.strings.psd1 new file mode 100644 index 0000000..7fddbf1 --- /dev/null +++ b/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_ADServicePrincipalName/en-US/MSFT_ADServicePrincipalName.strings.psd1 @@ -0,0 +1,11 @@ +# culture='en-US' +ConvertFrom-StringData @' + GetServicePrincipalName = Getting service principal name '{0}'. (ADSPN0001) + ServicePrincipalNameAbsent = Service principal name '{0}' is absent. (ADSPN0002) + ServicePrincipalNamePresent = Service principal name '{0}' is present on account(s) '{1}'. (ADSPN0003) + AccountNotFound = Active Directory object with SamAccountName '{0}' not found. (ADSPN0004) + RemoveServicePrincipalName = Removing service principal name '{0}' from account '{1}'. (ADSPN0005) + AddServicePrincipalName = Adding service principal name '{0}' to account '{1}. (ADSPN0006) + ServicePrincipalNameInDesiredState = Service principal name '{0}' is in the desired state. (ADSPN0007) + ServicePrincipalNameNotInDesiredState = Service principal name '{0}' is not in the desired state. (ADSPN0008) +'@ diff --git a/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_ADServicePrincipalName/en-US/about_ADServicePrincipalName.help.txt b/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_ADServicePrincipalName/en-US/about_ADServicePrincipalName.help.txt new file mode 100644 index 0000000..bbe4184 --- /dev/null +++ b/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_ADServicePrincipalName/en-US/about_ADServicePrincipalName.help.txt @@ -0,0 +1,60 @@ +.NAME + ADServicePrincipalName + +.DESCRIPTION + The ADServicePrincipalName DSC resource will manage service principal names. A service principal name (SPN) is a unique identifier of a service instance. SPNs are used by Kerberos authentication to associate a service instance with a service logon account. This allows a client application to request that the service authenticate an account even if the client does not have the account name. + + ## Requirements + + * Target machine must be running Windows Server 2008 R2 or later. + +.PARAMETER Ensure + Write - String + Allowed values: Present, Absent + Specifies if the service principal name should be added or removed. Default value is 'Present'. + +.PARAMETER ServicePrincipalName + Key - String + The full SPN to add or remove, e.g. HOST/LON-DC1. + +.PARAMETER Account + Write - String + The user or computer account to add or remove the SPN to, e.g. User1 or LON-DC1$. Default value is ''. If Ensure is set to Present, a value must be specified. + +.EXAMPLE 1 + +This configuration will add a Service Principal Name to a user account. + +Configuration ADServicePrincipalName_AddUserServicePrincipalName_Config +{ + Import-DscResource -Module ActiveDirectoryDsc + + Node localhost + { + ADServicePrincipalName 'SQL01Svc' + { + ServicePrincipalName = 'MSSQLSvc/sql01.contoso.com:1433' + Account = 'SQL01Svc' + } + } +} + +.EXAMPLE 2 + +This configuration will add a Service Principal Name to a computer account. + +Configuration ADServicePrincipalName_AddComputerServicePrincipalName_Config +{ + Import-DscResource -Module ActiveDirectoryDsc + + Node localhost + { + ADServicePrincipalName 'web.contoso.com' + { + ServicePrincipalName = 'HTTP/web.contoso.com' + Account = 'IIS01$' + } + } +} + + diff --git a/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_ADUser/MSFT_ADUser.PropertyMap.psd1 b/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_ADUser/MSFT_ADUser.PropertyMap.psd1 new file mode 100644 index 0000000..b9b27d9 --- /dev/null +++ b/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_ADUser/MSFT_ADUser.PropertyMap.psd1 @@ -0,0 +1,310 @@ +@{ + Parameters = @( + @{ + Parameter = 'CommonName' + ADProperty = 'cn' + UseCmdletParameter = $false + Array = $false + } + @{ + Parameter = 'UserPrincipalName' + ADProperty = 'UserPrincipalName' + UseCmdletParameter = $false + Array = $false + } + @{ + Parameter = 'DisplayName' + ADProperty = 'DisplayName' + UseCmdletParameter = $false + Array = $false + } + @{ + Parameter = 'Path' + ADProperty = 'distinguishedName' + UseCmdletParameter = $true + Array = $false + } + @{ + Parameter = 'GivenName' + ADProperty = 'GivenName' + UseCmdletParameter = $false + Array = $false + } + @{ + Parameter = 'Initials' + ADProperty = 'Initials' + UseCmdletParameter = $false + Array = $false + } + @{ + Parameter = 'Surname' + ADProperty = 'sn' + UseCmdletParameter = $false + Array = $false + } + @{ + Parameter = 'Description' + ADProperty = 'Description' + UseCmdletParameter = $false + Array = $false + } + @{ + Parameter = 'StreetAddress' + ADProperty = 'StreetAddress' + UseCmdletParameter = $false + Array = $false + } + @{ + Parameter = 'POBox' + ADProperty = 'PostOfficeBox' + UseCmdletParameter = $false + Array = $false + } + @{ + Parameter = 'City' + ADProperty = 'l' + UseCmdletParameter = $false + Array = $false + } + @{ + Parameter = 'State' + ADProperty = 'st' + UseCmdletParameter = $false + Array = $false + } + @{ + Parameter = 'PostalCode' + ADProperty = 'PostalCode' + UseCmdletParameter = $false + Array = $false + } + @{ + Parameter = 'Country' + ADProperty = 'c' + UseCmdletParameter = $false + Array = $false + } + @{ + Parameter = 'Department' + ADProperty = 'Department' + UseCmdletParameter = $false + Array = $false + } + @{ + Parameter = 'Division' + ADProperty = 'Division' + UseCmdletParameter = $false + Array = $false + } + @{ + Parameter = 'Company' + ADProperty = 'Company' + UseCmdletParameter = $false + Array = $false + } + @{ + Parameter = 'Office' + ADProperty = 'physicalDeliveryOfficeName' + UseCmdletParameter = $false + Array = $false + } + @{ + Parameter = 'JobTitle' + ADProperty = 'title' + UseCmdletParameter = $false + Array = $false + } + @{ + Parameter = 'EmailAddress' + ADProperty = 'mail' + UseCmdletParameter = $false + Array = $false + } + @{ + Parameter = 'EmployeeID' + ADProperty = 'EmployeeID' + UseCmdletParameter = $false + Array = $false + } + @{ + Parameter = 'EmployeeNumber' + ADProperty = 'EmployeeNumber' + UseCmdletParameter = $false + Array = $false + } + @{ + Parameter = 'HomeDirectory' + ADProperty = 'HomeDirectory' + UseCmdletParameter = $false + Array = $false + } + @{ + Parameter = 'HomeDrive' + ADProperty = 'HomeDrive' + UseCmdletParameter = $false + Array = $false + } + @{ + Parameter = 'HomePage' + ADProperty = 'wWWHomePage' + UseCmdletParameter = $false + Array = $false + } + @{ + Parameter = 'ProfilePath' + ADProperty = 'ProfilePath' + UseCmdletParameter = $false + Array = $false + } + @{ + Parameter = 'LogonScript' + ADProperty = 'scriptPath' + UseCmdletParameter = $false + Array = $false + } + @{ + Parameter = 'Notes' + ADProperty = 'info' + UseCmdletParameter = $false + Array = $false + } + @{ + Parameter = 'OfficePhone' + ADProperty = 'telephoneNumber' + UseCmdletParameter = $false + Array = $false + } + @{ + Parameter = 'MobilePhone' + ADProperty = 'mobile' + UseCmdletParameter = $false + Array = $false + } + @{ + Parameter = 'Fax' + ADProperty = 'facsimileTelephoneNumber' + UseCmdletParameter = $false + Array = $false + } + @{ + Parameter = 'Pager' + ADProperty = 'Pager' + UseCmdletParameter = $false + Array = $false + } + @{ + Parameter = 'IPPhone' + ADProperty = 'IPPhone' + UseCmdletParameter = $false + Array = $false + } + @{ + Parameter = 'HomePhone' + ADProperty = 'HomePhone' + UseCmdletParameter = $false + Array = $false + } + @{ + Parameter = 'Enabled' + ADProperty = 'Enabled' + UseCmdletParameter = $true + Array = $false + } + @{ + Parameter = 'Manager' + ADProperty = 'Manager' + UseCmdletParameter = $false + Array = $false + } + @{ + Parameter = 'LogonWorkstations' + ADProperty = 'userWorkStations' + UseCmdletParameter = $false + Array = $false + } + @{ + Parameter = 'Organization' + ADProperty = 'o' + UseCmdletParameter = $false + Array = $false + } + @{ + Parameter = 'OtherName' + ADProperty = 'middleName' + UseCmdletParameter = $false + Array = $false + } + @{ + Parameter = 'ThumbnailPhoto' + ADProperty = 'thumbnailPhoto' + UseCmdletParameter = $false + Array = $false + } + @{ + Parameter = 'PasswordNeverExpires' + ADProperty = 'PasswordNeverExpires' + UseCmdletParameter = $true + Array = $false + } + @{ + Parameter = 'CannotChangePassword' + ADProperty = 'CannotChangePassword' + UseCmdletParameter = $true + Array = $false + } + @{ + Parameter = 'ChangePasswordAtLogon' + ADProperty = 'pwdLastSet' + UseCmdletParameter = $true + Array = $false + } + @{ + Parameter = 'TrustedForDelegation' + ADProperty = 'TrustedForDelegation' + UseCmdletParameter = $true + Array = $false + } + @{ + Parameter = 'AccountNotDelegated' + ADProperty = 'AccountNotDelegated' + UseCmdletParameter = $true + Array = $false + } + @{ + Parameter = 'AllowReversiblePasswordEncryption' + ADProperty = 'AllowReversiblePasswordEncryption' + UseCmdletParameter = $true + Array = $false + } + @{ + Parameter = 'CompoundIdentitySupported' + ADProperty = 'CompoundIdentitySupported' + UseCmdletParameter = $true + Array = $false + } + @{ + Parameter = 'PasswordNotRequired' + ADProperty = 'PasswordNotRequired' + UseCmdletParameter = $true + Array = $false + } + @{ + Parameter = 'SmartcardLogonRequired' + ADProperty = 'SmartcardLogonRequired' + UseCmdletParameter = $true + Array = $false + } + @{ + Parameter = 'ServicePrincipalNames' + ADProperty = 'ServicePrincipalName' + UseCmdletParameter = $false + Array = $true + } + @{ + Parameter = 'ProxyAddresses' + ADProperty = 'ProxyAddresses' + UseCmdletParameter = $false + Array = $true + } + ) +} diff --git a/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_ADUser/MSFT_ADUser.psm1 b/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_ADUser/MSFT_ADUser.psm1 new file mode 100644 index 0000000..308ad35 --- /dev/null +++ b/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_ADUser/MSFT_ADUser.psm1 @@ -0,0 +1,1934 @@ +[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingUserNameAndPassWordParams', '')] +[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingPlainTextForPassword", "PasswordAuthentication")] +param () + +$script:resourceModulePath = Split-Path -Path (Split-Path -Path $PSScriptRoot -Parent) -Parent +$script:modulesFolderPath = Join-Path -Path $script:resourceModulePath -ChildPath 'Modules' + +$script:localizationModulePath = Join-Path -Path $script:modulesFolderPath -ChildPath 'ActiveDirectoryDsc.Common' +Import-Module -Name (Join-Path -Path $script:localizationModulePath -ChildPath 'ActiveDirectoryDsc.Common.psm1') + +$script:dscResourceName = [System.IO.Path]::GetFileNameWithoutExtension($MyInvocation.MyCommand.Name) + +$script:localizedData = Get-LocalizedData -ResourceName $script:dscResourceName + +# Import a property map that maps the DSC resource parameters to the Active Directory user attributes. +$adPropertyMapPath = Join-Path -Path $PSScriptRoot -ChildPath "$($script:dscResourceName).PropertyMap.psd1" +$adPropertyMap = (Import-PowerShellDataFile -Path $adPropertyMapPath).Parameters + +<# + .SYNOPSIS + Returns the current state of the Active Directory User + + .PARAMETER DomainName + Name of the domain where the user account is located (only used if password is managed). + + .PARAMETER UserName + Specifies the Security Account Manager (SAM) account name of the user (ldapDisplayName 'sAMAccountName'). + + .PARAMETER DomainController + Specifies the Active Directory Domain Services instance to use to perform the task. + + .PARAMETER Credential + Specifies the user account credentials to use to perform this task. + + .NOTES + Used Functions: + Name | Module + ------------------------------|-------------------------- + Get-ADUser | ActiveDirectory + Assert-Module | ActiveDirectoryDsc.Common + Get-ADCommonParameters | ActiveDirectoryDsc.Common + New-InvalidOperationException | ActiveDirectoryDsc.Common + Get-ADObjectParentDN | ActiveDirectoryDsc.Common + Get-MD5HashString | MSFT_ADUser +#> +function Get-TargetResource +{ + [CmdletBinding()] + [OutputType([System.Collections.Hashtable])] + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $DomainName, + + [Parameter(Mandatory = $true)] + [System.String] + $UserName, + + [Parameter()] + [ValidateNotNull()] + [System.String] + $DomainController, + + [Parameter()] + [ValidateNotNull()] + [System.Management.Automation.PSCredential] + [System.Management.Automation.CredentialAttribute()] + $Credential + ) + + Assert-Module -ModuleName 'ActiveDirectory' + + $adCommonParameters = Get-ADCommonParameters @PSBoundParameters + + Write-Verbose -Message ($script:localizedData.RetrievingADUser -f $UserName, $DomainName) + + try + { + $adUser = Get-ADUser @adCommonParameters -Properties $adPropertyMap.ADProperty + } + catch [Microsoft.ActiveDirectory.Management.ADIdentityNotFoundException] + { + Write-Verbose -Message ($script:localizedData.ADUserNotPresent -f $UserName, $DomainName) + + $adUser = $null + } + catch + { + $errorMessage = $script:localizedData.RetrievingADUserError -f $UserName, $DomainName + New-InvalidOperationException -Message $errorMessage -ErrorRecord $_ + } + + if ($adUser) + { + Write-Verbose -Message ($script:localizedData.ADUserIsPresent -f $UserName, $DomainName) + + $targetResource = @{ + DistinguishedName = $adUser.DistinguishedName # Read-only property + DomainController = $DomainController + DomainName = $DomainName + Ensure = 'Present' + Password = $null + UserName = $UserName + } + + # Retrieve each property from the ADPropertyMap and add to the hashtable + foreach ($property in $adPropertyMap) + { + $parameter = $property.Parameter + if ($parameter -eq 'Path') + { + # The path returned is not the parent container + if (-not [System.String]::IsNullOrEmpty($adUser.DistinguishedName)) + { + $targetResource[$parameter] = Get-ADObjectParentDN -DN $adUser.DistinguishedName + } + } + elseif ($parameter -eq 'ChangePasswordAtLogon') + { + if ($adUser.pwdlastset -eq 0) + { + $targetResource[$parameter] = $true + } + else + { + $targetResource[$parameter] = $false + } + } + elseif ($parameter -eq 'ThumbnailPhoto') + { + if ([System.String]::IsNullOrEmpty($adUser.$parameter)) + { + $targetResource[$parameter] = $null + $targetResource['ThumbnailPhotoHash'] = $null + } + else + { + $targetResource[$parameter] = [System.Convert]::ToBase64String($adUser.$parameter) + $targetResource['ThumbnailPhotoHash'] = Get-MD5HashString -Bytes $adUser.$parameter + } + } + else + { + $aDProperty = $property.ADProperty + if ($property.Array) + { + $targetResource[$parameter] = [System.String[]] $adUser.$ADProperty + } + else + { + $targetResource[$parameter] = $adUser.$aDProperty + } + } + } + } + else + { + $targetResource = @{ + DistinguishedName = $null + DomainController = $DomainController + DomainName = $DomainName + Ensure = 'Absent' + Password = $null + UserName = $UserName + } + + foreach ($property in $adPropertyMap) + { + $targetResource[$property.Parameter] = $null + } + } + + return $targetResource +} # end function Get-TargetResource + +<# + .SYNOPSIS + Tests the state of the Active Directory user account. + + .PARAMETER DomainName + Name of the domain where the user account is located (only used if password is managed). + + .PARAMETER UserName + Specifies the Security Account Manager (SAM) account name of the user (ldapDisplayName 'sAMAccountName'). + + .PARAMETER Password + Specifies a new password value for the account. + + .PARAMETER Ensure + Specifies whether the user account should be present or absent. Default value is 'Present'. + + .PARAMETER CommonName + Specifies the common name assigned to the user account (ldapDisplayName 'cn'). If not specified the default + value will be the same value provided in parameter UserName. + + .PARAMETER UserPrincipalName + Specifies the User Principal Name (UPN) assigned to the user account (ldapDisplayName 'userPrincipalName'). + + .PARAMETER DisplayName + Specifies the display name of the object (ldapDisplayName 'displayName'). + + .PARAMETER Path + Specifies the X.500 path of the Organizational Unit (OU) or container where the new object is created. + + .PARAMETER GivenName + Specifies the user's given name (ldapDisplayName 'givenName'). + + .PARAMETER Initials + Specifies the initials that represent part of a user's name (ldapDisplayName 'initials'). + + .PARAMETER Surname + Specifies the user's last name or surname (ldapDisplayName 'sn'). + + .PARAMETER Description + Specifies a description of the object (ldapDisplayName 'description'). + + .PARAMETER StreetAddress + Specifies the user's street address (ldapDisplayName 'streetAddress'). + + .PARAMETER POBox + Specifies the user's post office box number (ldapDisplayName 'postOfficeBox'). + + .PARAMETER City + Specifies the user's town or city (ldapDisplayName 'l'). + + .PARAMETER State + Specifies the user's or Organizational Unit's state or province (ldapDisplayName 'st'). + + .PARAMETER PostalCode + Specifies the user's postal code or zip code (ldapDisplayName 'postalCode'). + + .PARAMETER Country + Specifies the country or region code for the user's language of choice (ldapDisplayName 'c'). + + .PARAMETER Department + Specifies the user's department (ldapDisplayName 'department'). + + .PARAMETER Division + Specifies the user's division (ldapDisplayName 'division'). + + .PARAMETER Company + Specifies the user's company (ldapDisplayName 'company'). + + .PARAMETER Office + Specifies the location of the user's office or place of business (ldapDisplayName 'physicalDeliveryOfficeName'). + + .PARAMETER JobTitle + Specifies the user's title (ldapDisplayName 'title'). + + .PARAMETER EmailAddress + Specifies the user's e-mail address (ldapDisplayName 'mail'). + + .PARAMETER EmployeeID + Specifies the user's employee ID (ldapDisplayName 'employeeID'). + + .PARAMETER EmployeeNumber + Specifies the user's employee number (ldapDisplayName 'employeeNumber'). + + .PARAMETER HomeDirectory + Specifies a user's home directory path (ldapDisplayName 'homeDirectory'). + + .PARAMETER HomeDrive + Specifies a drive that is associated with the UNC path defined by the HomeDirectory property (ldapDisplayName + 'homeDrive'). + + .PARAMETER HomePage + Specifies the URL of the home page of the object (ldapDisplayName 'wWWHomePage'). + + .PARAMETER ProfilePath + Specifies a path to the user's profile (ldapDisplayName 'profilePath'). + + .PARAMETER LogonScript + Specifies a path to the user's log on script (ldapDisplayName 'scriptPath'). + + .PARAMETER Notes + Specifies the notes attached to the user's account (ldapDisplayName 'info'). + + .PARAMETER OfficePhone + Specifies the user's office telephone number (ldapDisplayName 'telephoneNumber'). + + .PARAMETER MobilePhone + Specifies the user's mobile phone number (ldapDisplayName 'mobile'). + + .PARAMETER Fax + Specifies the user's fax phone number (ldapDisplayName 'facsimileTelephoneNumber'). + + .PARAMETER HomePhone + Specifies the user's home telephone number (ldapDisplayName 'homePhone'). + + .PARAMETER Pager + Specifies the user's pager number (ldapDisplayName 'pager'). + + .PARAMETER IPPhone + Specifies the user's IP telephony phone number (ldapDisplayName 'ipPhone'). + + .PARAMETER Manager + Specifies the user's manager specified as a Distinguished Name (ldapDisplayName 'manager'). + + .PARAMETER LogonWorkstations + Specifies the computers that the user can access. To specify more than one computer, create a single + comma-separated list. You can identify a computer by using the Security Account Manager (SAM) account name + (sAMAccountName) or the DNS host name of the computer. The SAM account name is the same as the NetBIOS name of + the computer (ldapDisplayName 'userWorkStations'). + + .PARAMETER Organization + Specifies the user's organization. This parameter sets the Organization property of a user object + (ldapDisplayName 'o'). + + .PARAMETER OtherName + Specifies a name in addition to a user's given name and surname, such as the user's middle name. This parameter + sets the OtherName property of a user object (ldapDisplayName 'middleName'). + + .PARAMETER Enabled + Specifies if the account is enabled. Default value is $true. + + .PARAMETER CannotChangePassword + Specifies whether the account password can be changed. + + .PARAMETER ChangePasswordAtLogon + Specifies whether the account password must be changed during the next logon attempt. This will only be enabled + when the user is initially created. This parameter cannot be set to $true if the parameter PasswordNeverExpires + is also set to $true. + + .PARAMETER PasswordNeverExpires + Specifies whether the password of an account can expire. + + .PARAMETER TrustedForDelegation + Specifies whether an account is trusted for Kerberos delegation. Default value is $false. + + .PARAMETER AccountNotDelegated + Indicates whether the security context of the user is delegated to a service. When this parameter is set to + true, the security context of the account is not delegated to a service even when the service account is set as + trusted for Kerberos delegation. This parameter sets the AccountNotDelegated property for an Active Directory + account. This parameter also sets the ADS_UF_NOT_DELEGATED flag of the Active Directory User Account Control + (UAC) attribute. + + .PARAMETER AllowReversiblePasswordEncryption + Indicates whether reversible password encryption is allowed for the account. This parameter sets the + AllowReversiblePasswordEncryption property of the account. This parameter also sets the + ADS_UF_ENCRYPTED_TEXT_PASSWORD_ALLOWED flag of the Active Directory User Account Control (UAC) attribute. + + .PARAMETER CompoundIdentitySupported + Specifies whether an account supports Kerberos service tickets which includes the authorization data for the + user's device. This value sets the compound identity supported flag of the Active Directory + msDS-SupportedEncryptionTypes attribute. + + .PARAMETER PasswordNotRequired + Specifies whether the account requires a password. A password is not required for a new account. This parameter + sets the PasswordNotRequired property of an account object. + + .PARAMETER SmartcardLogonRequired + Specifies whether a smart card is required to logon. This parameter sets the SmartCardLoginRequired property + for a user object. This parameter also sets the ADS_UF_SMARTCARD_REQUIRED flag of the Active Directory + User Account Control attribute. + + .PARAMETER DomainController + Specifies the Active Directory Domain Services instance to use to perform the task. + + .PARAMETER Credential + Specifies the user account credentials to use to perform this task. + + .PARAMETER PasswordAuthentication + Specifies the authentication context type used when testing passwords. Default value is 'Default'. + + .PARAMETER PasswordNeverResets + Specifies whether existing user's password should be reset. Default value is $false. + + .PARAMETER RestoreFromRecycleBin + Try to restore the user object from the recycle bin before creating a new one. + + .PARAMETER ServicePrincipalNames + Specifies the service principal names for the user account. + + .PARAMETER ProxyAddresses + Specifies the proxy addresses for the user account. + + .PARAMETER ThumbnailPhoto + Specifies the thumbnail photo to be used for the user object. Can be set either to a path pointing to a + .jpg-file, or to a Base64-encoded jpeg image. If set to an empty string ('') the current thumbnail photo will + be removed. The property ThumbnailPhoto will always return the image as a Base64-encoded string even if the + configuration specified a file path. + + .NOTES + Used Functions: + Name | Module + -----------------------|-------------------------- + Assert-Parameters | MSFT_ADUser + Test-Password | ActiveDirectoryDsc.Common + Compare-ThumbnailPhoto | MSFT_ADUser +#> +function Test-TargetResource +{ + [CmdletBinding()] + [OutputType([System.Boolean])] + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $DomainName, + + [Parameter(Mandatory = $true)] + [System.String] + $UserName, + + [Parameter()] + [ValidateNotNull()] + [System.Management.Automation.PSCredential] + [System.Management.Automation.CredentialAttribute()] + $Password, + + [Parameter()] + [ValidateSet('Present', 'Absent')] + [System.String] + $Ensure = 'Present', + + [Parameter()] + [ValidateNotNull()] + [System.String] + $CommonName, + + [Parameter()] + [ValidateNotNull()] + [System.String] + $UserPrincipalName, + + [Parameter()] + [ValidateNotNull()] + [System.String] + $DisplayName, + + [Parameter()] + [ValidateNotNull()] + [System.String] + $Path, + + [Parameter()] + [ValidateNotNull()] + [System.String] + $GivenName, + + [Parameter()] + [ValidateNotNull()] + [System.String] + $Initials, + + [Parameter()] + [ValidateNotNull()] + [System.String] + $Surname, + + [Parameter()] + [ValidateNotNull()] + [System.String] + $Description, + + [Parameter()] + [ValidateNotNull()] + [System.String] + $StreetAddress, + + [Parameter()] + [ValidateNotNull()] + [System.String] + $POBox, + + [Parameter()] + [ValidateNotNull()] + [System.String] + $City, + + [Parameter()] + [ValidateNotNull()] + [System.String] + $State, + + [Parameter()] + [ValidateNotNull()] + [System.String] + $PostalCode, + + [Parameter()] + [ValidateNotNull()] + [System.String] + $Country, + + [Parameter()] + [ValidateNotNull()] + [System.String] + $Department, + + [Parameter()] + [ValidateNotNull()] + [System.String] + $Division, + + [Parameter()] + [ValidateNotNull()] + [System.String] + $Company, + + [Parameter()] + [ValidateNotNull()] + [System.String] + $Office, + + [Parameter()] + [ValidateNotNull()] + [System.String] + $JobTitle, + + [Parameter()] + [ValidateNotNull()] + [System.String] + $EmailAddress, + + [Parameter()] + [ValidateNotNull()] + [System.String] + $EmployeeID, + + [Parameter()] + [ValidateNotNull()] + [System.String] + $EmployeeNumber, + + [Parameter()] + [ValidateNotNull()] + [System.String] + $HomeDirectory, + + [Parameter()] + [ValidateNotNull()] + [System.String] + $HomeDrive, + + [Parameter()] + [ValidateNotNull()] + [System.String] + $HomePage, + + [Parameter()] + [ValidateNotNull()] + [System.String] + $ProfilePath, + + [Parameter()] + [ValidateNotNull()] + [System.String] + $LogonScript, + + [Parameter()] + [ValidateNotNull()] + [System.String] + $Notes, + + [Parameter()] + [ValidateNotNull()] + [System.String] + $OfficePhone, + + [Parameter()] + [ValidateNotNull()] + [System.String] + $MobilePhone, + + [Parameter()] + [ValidateNotNull()] + [System.String] + $Fax, + + [Parameter()] + [ValidateNotNull()] + [System.String] + $HomePhone, + + [Parameter()] + [ValidateNotNull()] + [System.String] + $Pager, + + [Parameter()] + [ValidateNotNull()] + [System.String] + $IPPhone, + + [Parameter()] + [ValidateNotNull()] + [System.String] + $Manager, + + [Parameter()] + [ValidateNotNull()] + [System.String] + $LogonWorkstations, + + [Parameter()] + [ValidateNotNull()] + [System.String] + $Organization, + + [Parameter()] + [ValidateNotNull()] + [System.String] + $OtherName, + + [Parameter()] + [ValidateNotNull()] + [System.Boolean] + $Enabled = $true, + + [Parameter()] + [ValidateNotNull()] + [System.Boolean] + $CannotChangePassword, + + [Parameter()] + [ValidateNotNull()] + [System.Boolean] + $ChangePasswordAtLogon, + + [Parameter()] + [ValidateNotNull()] + [System.Boolean] + $PasswordNeverExpires, + + [Parameter()] + [ValidateNotNull()] + [System.Boolean] + $TrustedForDelegation, + + [Parameter()] + [ValidateNotNull()] + [System.Boolean] + $AccountNotDelegated, + + [Parameter()] + [ValidateNotNull()] + [System.Boolean] + $AllowReversiblePasswordEncryption, + + [Parameter()] + [ValidateNotNull()] + [System.Boolean] + $CompoundIdentitySupported, + + [Parameter()] + [ValidateNotNull()] + [System.Boolean] + $PasswordNotRequired, + + [Parameter()] + [ValidateNotNull()] + [System.Boolean] + $SmartcardLogonRequired, + + [Parameter()] + [ValidateNotNull()] + [System.String] + $DomainController, + + [Parameter()] + [ValidateNotNull()] + [System.Management.Automation.PSCredential] + [System.Management.Automation.CredentialAttribute()] + $Credential, + + [Parameter()] + [ValidateSet('Default', 'Negotiate')] + [System.String] + $PasswordAuthentication = 'Default', + + [Parameter()] + [ValidateNotNull()] + [System.Boolean] + $PasswordNeverResets = $false, + + [Parameter()] + [ValidateNotNull()] + [System.Boolean] + $RestoreFromRecycleBin, + + [Parameter()] + [ValidateNotNull()] + [System.String[]] + $ServicePrincipalNames, + + [Parameter()] + [ValidateNotNull()] + [System.String[]] + $ProxyAddresses, + + [Parameter()] + [System.String] + $ThumbnailPhoto + ) + + <# + This is a workaround to set the CommonName default to UserName to make the resource able to enter debug mode. + For more information see issue https://github.com/PowerShell/ActiveDirectoryDsc/issues/427. + #> + if (-not $PSBoundParameters.ContainsKey('CommonName')) + { + $CommonName = $UserName + } + Assert-Parameters @PSBoundParameters + + $parameters = @{} + $PSBoundParameters + $parameters.Remove('DomainName') + $parameters.Remove('UserName') + $parameters.Remove('RestoreFromRecycleBin') + $parameters.Remove('PasswordNeverResets') + $parameters.Remove('PasswordAuthentication') + $parameters.Remove('DomainController') + $parameters.Remove('Credential') + $parameters.Remove('Ensure') + $parameters.Remove('Verbose') + $parameters.Remove('Debug') + + # Add parameters with default values as they may not be explicitly passed + $parameters['Enabled'] = $Enabled + + $getParameters = @{ + DomainName = $DomainName + UserName = $UserName + } + + if ($PSBoundParameters.ContainsKey('DomainController')) + { + $getParameters['DomainController'] = $DomainController + } + + if ($PSBoundParameters.ContainsKey('Credential')) + { + $getParameters['Credential'] = $Credential + } + + $targetResource = Get-TargetResource @getParameters + + $inDesiredState = $true + + if ($targetResource.Ensure -eq 'Present') + { + if ($Ensure -eq 'Present') + { + foreach ($parameter in $parameters.Keys) + { + if ($parameter -eq 'Password') + { + # Only process the Password parameter if the PasswordNeverResets parameter is false + if ($PasswordNeverResets -eq $false) + { + $testPasswordParams = @{ + Username = $UserName + Password = $Password + DomainName = $DomainName + PasswordAuthentication = $PasswordAuthentication + } + + if ($Credential) + { + $testPasswordParams['Credential'] = $Credential + } + + if (-not (Test-Password @testPasswordParams)) + { + Write-Verbose -Message ($script:localizedData.ADUserNotDesiredPropertyState -f + 'Password', '', '') + + $inDesiredState = $false + } + } + } + elseif ($parameter -eq 'ChangePasswordAtLogon' -and $parameters.$parameter -eq $true) + { + # Only process the 'ChangePasswordAtLogon = $true' parameter during new user creation + continue + } + elseif ($parameter -eq 'ThumbnailPhoto') + { + <# + Compare thumbnail hash, if they are the same the function + Compare-ThumbnailPhoto returns $null if they are the same. + #> + $compareThumbnailPhotoResult = Compare-ThumbnailPhoto -DesiredThumbnailPhoto $ThumbnailPhoto ` + -CurrentThumbnailPhotoHash $targetResource.ThumbnailPhotoHash + + if ($compareThumbnailPhotoResult) + { + Write-Verbose -Message ($script:localizedData.ADUserNotDesiredPropertyState -f + $parameter, ('Hash: ' + $compareThumbnailPhotoResult.DesiredThumbnailPhotoHash), + ('Hash: ' + $compareThumbnailPhotoResult.CurrentThumbnailPhotoHash)) + + $inDesiredState = $false + } + } + + # This check is required to be able to explicitly remove values with an empty string, if required + elseif (([System.String]::IsNullOrEmpty($parameters.$parameter)) -and ` + ([System.String]::IsNullOrEmpty($targetResource.$parameter))) + { + <# + Both values are null/empty and therefore we are compliant + Must catch this scenario separately, as Compare-Object can't compare Null objects + #> + continue + } + elseif (($null -ne $parameters.$parameter -and $null -eq $targetResource.$parameter) -or + ($null -eq $parameters.$parameter -and $null -ne $targetResource.$parameter) -or + (Compare-Object -ReferenceObject $parameters.$parameter ` + -DifferenceObject $targetResource.$parameter)) + { + Write-Verbose -Message ($script:localizedData.ADUserNotDesiredPropertyState -f + $parameter, ($parameters.$parameter -join '; '), ($targetResource.$parameter -join '; ')) + + $inDesiredState = $false + } + } #end foreach PSBoundParameter + + if ($inDesiredState) + { + # Resource is in desired state + Write-Verbose -Message ($script:localizedData.ADUserInDesiredState -f $UserName) + } + else + { + # Resource is not in the desired state + Write-Verbose -Message ($script:localizedData.ADUserNotInDesiredState -f $UserName) + } + } + else + { + # Resource should be Absent + Write-Verbose -Message ($script:localizedData.ADUserIsPresentButShouldBeAbsent -f $UserName) + + $inDesiredState = $false + } + } + else + { + # Resource is Absent + if ($Ensure -eq 'Present') + { + # Resource should be Present + Write-Verbose -Message ($script:localizedData.ADUserIsAbsentButShouldBePresent -f $UserName) + + $inDesiredState = $false + } + else + { + # Resource should be Absent + Write-Verbose ($script:localizedData.ADUserInDesiredState -f $UserName) + + $inDesiredState = $true + } + } + + return $inDesiredState +} # end function Test-TargetResource + +<# + .SYNOPSIS + Sets the properties of the Active Directory user account. + + .PARAMETER DomainName + Name of the domain where the user account is located (only used if password is managed). + + .PARAMETER UserName + Specifies the Security Account Manager (SAM) account name of the user (ldapDisplayName 'sAMAccountName'). + + .PARAMETER Password + Specifies a new password value for the account. + + .PARAMETER Ensure + Specifies whether the user account should be present or absent. Default value is 'Present'. + + .PARAMETER CommonName + Specifies the common name assigned to the user account (ldapDisplayName 'cn'). If not specified the default + value will be the same value provided in parameter UserName. + + .PARAMETER UserPrincipalName + Specifies the User Principal Name (UPN) assigned to the user account (ldapDisplayName 'userPrincipalName'). + + .PARAMETER DisplayName + Specifies the display name of the object (ldapDisplayName 'displayName'). + + .PARAMETER Path + Specifies the X.500 path of the Organizational Unit (OU) or container where the new object is created. + + .PARAMETER GivenName + Specifies the user's given name (ldapDisplayName 'givenName'). + + .PARAMETER Initials + Specifies the initials that represent part of a user's name (ldapDisplayName 'initials'). + + .PARAMETER Surname + Specifies the user's last name or surname (ldapDisplayName 'sn'). + + .PARAMETER Description + Specifies a description of the object (ldapDisplayName 'description'). + + .PARAMETER StreetAddress + Specifies the user's street address (ldapDisplayName 'streetAddress'). + + .PARAMETER POBox + Specifies the user's post office box number (ldapDisplayName 'postOfficeBox'). + + .PARAMETER City + Specifies the user's town or city (ldapDisplayName 'l'). + + .PARAMETER State + Specifies the user's or Organizational Unit's state or province (ldapDisplayName 'st'). + + .PARAMETER PostalCode + Specifies the user's postal code or zip code (ldapDisplayName 'postalCode'). + + .PARAMETER Country + Specifies the country or region code for the user's language of choice (ldapDisplayName 'c'). + + .PARAMETER Department + Specifies the user's department (ldapDisplayName 'department'). + + .PARAMETER Division + Specifies the user's division (ldapDisplayName 'division'). + + .PARAMETER Company + Specifies the user's company (ldapDisplayName 'company'). + + .PARAMETER Office + Specifies the location of the user's office or place of business (ldapDisplayName 'physicalDeliveryOfficeName'). + + .PARAMETER JobTitle + Specifies the user's title (ldapDisplayName 'title'). + + .PARAMETER EmailAddress + Specifies the user's e-mail address (ldapDisplayName 'mail'). + + .PARAMETER EmployeeID + Specifies the user's employee ID (ldapDisplayName 'employeeID'). + + .PARAMETER EmployeeNumber + Specifies the user's employee number (ldapDisplayName 'employeeNumber'). + + .PARAMETER HomeDirectory + Specifies a user's home directory path (ldapDisplayName 'homeDirectory'). + + .PARAMETER HomeDrive + Specifies a drive that is associated with the UNC path defined by the HomeDirectory property (ldapDisplayName + 'homeDrive'). + + .PARAMETER HomePage + Specifies the URL of the home page of the object (ldapDisplayName 'wWWHomePage'). + + .PARAMETER ProfilePath + Specifies a path to the user's profile (ldapDisplayName 'profilePath'). + + .PARAMETER LogonScript + Specifies a path to the user's log on script (ldapDisplayName 'scriptPath'). + + .PARAMETER Notes + Specifies the notes attached to the user's account (ldapDisplayName 'info'). + + .PARAMETER OfficePhone + Specifies the user's office telephone number (ldapDisplayName 'telephoneNumber'). + + .PARAMETER MobilePhone + Specifies the user's mobile phone number (ldapDisplayName 'mobile'). + + .PARAMETER Fax + Specifies the user's fax phone number (ldapDisplayName 'facsimileTelephoneNumber'). + + .PARAMETER HomePhone + Specifies the user's home telephone number (ldapDisplayName 'homePhone'). + + .PARAMETER Pager + Specifies the user's pager number (ldapDisplayName 'pager'). + + .PARAMETER IPPhone + Specifies the user's IP telephony phone number (ldapDisplayName 'ipPhone'). + + .PARAMETER Manager + Specifies the user's manager specified as a Distinguished Name (ldapDisplayName 'manager'). + + .PARAMETER LogonWorkstations + Specifies the computers that the user can access. To specify more than one computer, create a single + comma-separated list. You can identify a computer by using the Security Account Manager (SAM) account name + (sAMAccountName) or the DNS host name of the computer. The SAM account name is the same as the NetBIOS name of + the computer (ldapDisplayName 'userWorkStations'). + + .PARAMETER Organization + Specifies the user's organization. This parameter sets the Organization property of a user object + (ldapDisplayName 'o'). + + .PARAMETER OtherName + Specifies a name in addition to a user's given name and surname, such as the user's middle name. This parameter + sets the OtherName property of a user object (ldapDisplayName 'middleName'). + + .PARAMETER Enabled + Specifies if the account is enabled. Default value is $true. + + .PARAMETER CannotChangePassword + Specifies whether the account password can be changed. + + .PARAMETER ChangePasswordAtLogon + Specifies whether the account password must be changed during the next logon attempt. This will only be enabled + when the user is initially created. This parameter cannot be set to $true if the parameter PasswordNeverExpires + is also set to $true. + + .PARAMETER PasswordNeverExpires + Specifies whether the password of an account can expire. + + .PARAMETER TrustedForDelegation + Specifies whether an account is trusted for Kerberos delegation. Default value is $false. + + .PARAMETER AccountNotDelegated + Indicates whether the security context of the user is delegated to a service. When this parameter is set to + true, the security context of the account is not delegated to a service even when the service account is set as + trusted for Kerberos delegation. This parameter sets the AccountNotDelegated property for an Active Directory + account. This parameter also sets the ADS_UF_NOT_DELEGATED flag of the Active Directory User Account Control + (UAC) attribute. + + .PARAMETER AllowReversiblePasswordEncryption + Indicates whether reversible password encryption is allowed for the account. This parameter sets the + AllowReversiblePasswordEncryption property of the account. This parameter also sets the + ADS_UF_ENCRYPTED_TEXT_PASSWORD_ALLOWED flag of the Active Directory User Account Control (UAC) attribute. + + .PARAMETER CompoundIdentitySupported + Specifies whether an account supports Kerberos service tickets which includes the authorization data for the + user's device. This value sets the compound identity supported flag of the Active Directory + msDS-SupportedEncryptionTypes attribute. + + .PARAMETER PasswordNotRequired + Specifies whether the account requires a password. A password is not required for a new account. This parameter + sets the PasswordNotRequired property of an account object. + + .PARAMETER SmartcardLogonRequired + Specifies whether a smart card is required to logon. This parameter sets the SmartCardLoginRequired property + for a user object. This parameter also sets the ADS_UF_SMARTCARD_REQUIRED flag of the Active Directory + User Account Control attribute. + + .PARAMETER DomainController + Specifies the Active Directory Domain Services instance to use to perform the task. + + .PARAMETER Credential + Specifies the user account credentials to use to perform this task. + + .PARAMETER PasswordAuthentication + Specifies the authentication context type used when testing passwords. Default value is 'Default'. + + .PARAMETER PasswordNeverResets + Specifies whether existing user's password should be reset. Default value is $false. + + .PARAMETER RestoreFromRecycleBin + Try to restore the user object from the recycle bin before creating a new one. + + .PARAMETER ServicePrincipalNames + Specifies the service principal names for the user account. + + .PARAMETER ProxyAddresses + Specifies the proxy addresses for the user account. + + .PARAMETER ThumbnailPhoto + Specifies the thumbnail photo to be used for the user object. Can be set either to a path pointing to a + .jpg-file, or to a Base64-encoded jpeg image. If set to an empty string ('') the current thumbnail photo will + be removed. The property ThumbnailPhoto will always return the image as a Base64-encoded string even if the + configuration specified a file path. + + .NOTES + Used Functions: + Name | Module + -----------------------|-------------------------- + Assert-Parameters | MSFT_ADUser + Compare-ThumbnailPhoto | MSFT_ADUser + Get-ThumbnailByteArray | MSFT_ADUser + Get-MD5HashString | MSFT_ADUser + Get-ADCommonParameters | ActiveDirectoryDsc.Common + Restore-ADCommonObject | ActiveDirectoryDsc.Common + Test-Password | ActiveDirectoryDsc.Common + New-ADUser | ActiveDirectory + Set-ADAccountPassword | ActiveDirectory + Set-ADUser | ActiveDirectory + Move-ADObject | ActiveDirectory + Rename-ADObject | ActiveDirectory + Remove-ADUser | ActiveDirectory +#> +function Set-TargetResource +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $DomainName, + + [Parameter(Mandatory = $true)] + [System.String] + $UserName, + + [Parameter()] + [ValidateNotNull()] + [System.Management.Automation.PSCredential] + [System.Management.Automation.CredentialAttribute()] + $Password, + + [Parameter()] + [ValidateSet('Present', 'Absent')] + [System.String] + $Ensure = 'Present', + + [Parameter()] + [ValidateNotNull()] + [System.String] + $CommonName, + + [Parameter()] + [ValidateNotNull()] + [System.String] + $UserPrincipalName, + + [Parameter()] + [ValidateNotNull()] + [System.String] + $DisplayName, + + [Parameter()] + [ValidateNotNull()] + [System.String] + $Path, + + [Parameter()] + [ValidateNotNull()] + [System.String] + $GivenName, + + [Parameter()] + [ValidateNotNull()] + [System.String] + $Initials, + + [Parameter()] + [ValidateNotNull()] + [System.String] + $Surname, + + [Parameter()] + [ValidateNotNull()] + [System.String] + $Description, + + [Parameter()] + [ValidateNotNull()] + [System.String] + $StreetAddress, + + [Parameter()] + [ValidateNotNull()] + [System.String] + $POBox, + + [Parameter()] + [ValidateNotNull()] + [System.String] + $City, + + [Parameter()] + [ValidateNotNull()] + [System.String] + $State, + + [Parameter()] + [ValidateNotNull()] + [System.String] + $PostalCode, + + [Parameter()] + [ValidateNotNull()] + [System.String] + $Country, + + [Parameter()] + [ValidateNotNull()] + [System.String] + $Department, + + [Parameter()] + [ValidateNotNull()] + [System.String] + $Division, + + [Parameter()] + [ValidateNotNull()] + [System.String] + $Company, + + [Parameter()] + [ValidateNotNull()] + [System.String] + $Office, + + [Parameter()] + [ValidateNotNull()] + [System.String] + $JobTitle, + + [Parameter()] + [ValidateNotNull()] + [System.String] + $EmailAddress, + + [Parameter()] + [ValidateNotNull()] + [System.String] + $EmployeeID, + + [Parameter()] + [ValidateNotNull()] + [System.String] + $EmployeeNumber, + + [Parameter()] + [ValidateNotNull()] + [System.String] + $HomeDirectory, + + [Parameter()] + [ValidateNotNull()] + [System.String] + $HomeDrive, + + [Parameter()] + [ValidateNotNull()] + [System.String] + $HomePage, + + [Parameter()] + [ValidateNotNull()] + [System.String] + $ProfilePath, + + [Parameter()] + [ValidateNotNull()] + [System.String] + $LogonScript, + + [Parameter()] + [ValidateNotNull()] + [System.String] + $Notes, + + [Parameter()] + [ValidateNotNull()] + [System.String] + $OfficePhone, + + [Parameter()] + [ValidateNotNull()] + [System.String] + $MobilePhone, + + [Parameter()] + [ValidateNotNull()] + [System.String] + $Fax, + + [Parameter()] + [ValidateNotNull()] + [System.String] + $HomePhone, + + [Parameter()] + [ValidateNotNull()] + [System.String] + $Pager, + + [Parameter()] + [ValidateNotNull()] + [System.String] + $IPPhone, + + [Parameter()] + [ValidateNotNull()] + [System.String] + $Manager, + + [Parameter()] + [ValidateNotNull()] + [System.String] + $LogonWorkstations, + + [Parameter()] + [ValidateNotNull()] + [System.String] + $Organization, + + [Parameter()] + [ValidateNotNull()] + [System.String] + $OtherName, + + [Parameter()] + [ValidateNotNull()] + [System.Boolean] + $Enabled = $true, + + [Parameter()] + [ValidateNotNull()] + [System.Boolean] + $CannotChangePassword, + + [Parameter()] + [ValidateNotNull()] + [System.Boolean] + $ChangePasswordAtLogon, + + [Parameter()] + [ValidateNotNull()] + [System.Boolean] + $PasswordNeverExpires, + + [Parameter()] + [ValidateNotNull()] + [System.Boolean] + $TrustedForDelegation, + + [Parameter()] + [ValidateNotNull()] + [System.Boolean] + $AccountNotDelegated, + + [Parameter()] + [ValidateNotNull()] + [System.Boolean] + $AllowReversiblePasswordEncryption, + + [Parameter()] + [ValidateNotNull()] + [System.Boolean] + $CompoundIdentitySupported, + + [Parameter()] + [ValidateNotNull()] + [System.Boolean] + $PasswordNotRequired, + + [Parameter()] + [ValidateNotNull()] + [System.Boolean] + $SmartcardLogonRequired, + + [Parameter()] + [ValidateNotNull()] + [System.String] + $DomainController, + + [Parameter()] + [ValidateNotNull()] + [System.Management.Automation.PSCredential] + [System.Management.Automation.CredentialAttribute()] + $Credential, + + [Parameter()] + [ValidateSet('Default', 'Negotiate')] + [System.String] + $PasswordAuthentication = 'Default', + + [Parameter()] + [ValidateNotNull()] + [System.Boolean] + $PasswordNeverResets = $false, + + [Parameter()] + [ValidateNotNull()] + [System.Boolean] + $RestoreFromRecycleBin, + + [Parameter()] + [ValidateNotNull()] + [System.String[]] + $ServicePrincipalNames, + + [Parameter()] + [ValidateNotNull()] + [System.String[]] + $ProxyAddresses, + + [Parameter()] + [System.String] + $ThumbnailPhoto + ) + + <# + This is a workaround to set the CommonName default to UserName to make the resource able to enter debug mode. + For more information see issue https://github.com/PowerShell/ActiveDirectoryDsc/issues/427. + #> + if (-not $PSBoundParameters.ContainsKey('CommonName')) + { + $CommonName = $UserName + } + + Assert-Parameters @PSBoundParameters + + $parameters = @{} + $PSBoundParameters + $parameters.Remove('DomainName') + $parameters.Remove('UserName') + $parameters.Remove('PasswordNeverResets') + $parameters.Remove('PasswordAuthentication') + $parameters.Remove('RestoreFromRecycleBin') + $parameters.Remove('DomainController') + $parameters.Remove('Credential') + $parameters.Remove('Ensure') + $parameters.Remove('Verbose') + $parameters.Remove('Debug') + + # Add parameters with default values as they may not be explicitly passed + $parameters['Enabled'] = $Enabled + + $getParameters = @{ + DomainName = $DomainName + UserName = $UserName + } + + if ($PSBoundParameters.ContainsKey('DomainController')) + { + $getParameters['DomainController'] = $DomainController + } + + if ($PSBoundParameters.ContainsKey('Credential')) + { + $getParameters['Credential'] = $Credential + } + + $targetResource = Get-TargetResource @getParameters + + $restorationSuccessful = $false + + if ($Ensure -eq 'Present') + { + # Resource should be Present + if ($targetResource.Ensure -eq 'Absent') + { + # Resource is Absent + if ($RestoreFromRecycleBin) + { + # Try to restore account if it exists + Write-Verbose -Message ($script:localizedData.RestoringUser -f $UserName) + + $restoreParams = Get-ADCommonParameters @PSBoundParameters + $restorationSuccessful = Restore-ADCommonObject @restoreParams -ObjectClass User -ErrorAction Stop + } + + if (-not $RestoreFromRecycleBin -or ($RestoreFromRecycleBin -and -not $restorationSuccessful)) + { + # User does not exist and needs creating + $newADUserParams = Get-ADCommonParameters @PSBoundParameters -UseNameParameter + + $otherUserAttributes = @{} + $updateCnRequired = $false + + foreach ($parameter in $parameters.keys) + { + $adProperty = $adPropertyMap | + Where-Object -FilterScript { $_.Parameter -eq $parameter } + + if ($parameter -eq 'Password') + { + $newADUserParams['AccountPassword'] = $Password.Password + } + elseif ($parameter -eq 'CommonName') + { + if ($CommonName -ne $UserName) + { + # Need to set different CN using Rename after user creation + $updateCnRequired = $true + } + } + elseif ($parameter -eq 'ThumbnailPhoto') + { + [System.Byte[]] $thumbnailPhotoBytes = Get-ThumbnailByteArray ` + -ThumbnailPhoto $ThumbnailPhoto -Verbose:$false + + $otherUserAttributes[$adProperty.ADProperty] = $thumbnailPhotoBytes + } + else + { + if ($adProperty.UseCmdletParameter -eq $true) + { + # We need to pass the parameter explicitly to New-ADUser, not via -OtherAttributes + $newADUserParams[$adProperty.Parameter] = $parameters.$parameter + } + else + { + $otherUserAttributes[$adProperty.ADProperty] = $parameters.$parameter + } + } + } + + if ($otherUserAttributes.Keys.Count -gt 0) + { + $newADUserParams['OtherAttributes'] = $otherUserAttributes + } + + Write-Verbose -Message ($script:localizedData.AddingADUser -f $UserName, $DomainName) + + Write-Debug -Message ('New-ADUser Parameters:' + ($newADUserParams | Out-String)) + + $newADUser = New-ADUser @newADUserParams -SamAccountName $UserName -Passthru + + if ($updateCnRequired) + { + $renameAdObjectParameters = Get-ADCommonParameters @PSBoundParameters + + # Using the SamAccountName for identity with Rename-ADObject does not work, use the DN instead + $renameAdObjectParameters['Identity'] = $newADUser.DistinguishedName + + Rename-ADObject @renameAdObjectParameters -NewName $CommonName + } + } + } + if ($targetResource.Ensure -eq 'Present' -or $restorationSuccessful) + { + # Resource is Present or has just been restored from the recycle bin + $setADUserParams = @{} + $replaceUserProperties = @{} + $clearUserProperties = @() + $moveUserRequired = $false + $updateCnRequired = $false + + foreach ($parameter in $parameters.Keys) + { + # Find the associated AD property + $adProperty = $adPropertyMap | + Where-Object -FilterScript { $_.Parameter -eq $parameter } + + if ($parameter -eq 'Path') + { + if ($parameters.Path -ne $targetResource.Path) + { + # Move user after any property changes + $moveUserRequired = $true + } + } + elseif ($parameter -eq 'CommonName') + { + if ($parameters.CommonName -ne $targetResource.CommonName) + { + # Update CN after any property changes + $updateCnRequired = $true + } + } + elseif ($parameter -eq 'Password') + { + # Only process the Password parameter if the PasswordNeverResets parameter is false + if ($PasswordNeverResets -eq $false) + { + $adCommonParameters = Get-ADCommonParameters @PSBoundParameters + $testPasswordParams = @{ + Username = $UserName + Password = $Password + DomainName = $DomainName + PasswordAuthentication = $PasswordAuthentication + } + + if ($Credential) + { + $testPasswordParams['Credential'] = $Credential + } + if (-not (Test-Password @testPasswordParams)) + { + Write-Verbose -Message ($script:localizedData.SettingADUserPassword -f $UserName) + + Set-ADAccountPassword @adCommonParameters -Reset -NewPassword $Password.Password + } + } + } + elseif ($parameter -eq 'ChangePasswordAtLogon') + { + # Only process the 'ChangePasswordAtLogon = $true' parameter during new user creation + continue + } + elseif ($parameter -eq 'ThumbnailPhoto') + { + # Compare thumbnail hash, if they are the same the function Compare-ThumbnailPhoto returns $null. + if (Compare-ThumbnailPhoto -DesiredThumbnailPhoto $ThumbnailPhoto ` + -CurrentThumbnailPhotoHash $targetResource.ThumbnailPhotoHash) + { + if ($ThumbnailPhoto -eq [System.String]::Empty) + { + $clearUserProperties += $adProperty.ADProperty + + Write-Verbose -Message ($script:localizedData.ClearingADUserProperty -f + $adProperty.ADProperty) + } + else + { + [System.Byte[]] $thumbnailPhotoBytes = Get-ThumbnailByteArray ` + -ThumbnailPhoto $ThumbnailPhoto -Verbose:$false + + $thumbnailPhotoHash = Get-MD5HashString -Bytes $thumbnailPhotoBytes + + Write-Verbose -Message ($script:localizedData.UpdatingThumbnailPhotoProperty -f + $adProperty.ADProperty, $thumbnailPhotoHash) + + $replaceUserProperties[$adProperty.ADProperty] = $thumbnailPhotoBytes + } + } + } + elseif (([System.String]::IsNullOrEmpty($parameters.$parameter)) -and ` + ([System.String]::IsNullOrEmpty($targetResource.$parameter))) + { + <# + Both values are null/empty and therefore we are compliant + Must catch this scenario separately, as Compare-Object can't compare Null objects + #> + continue + } + # Use Compare-Object to allow comparison of string and array parameters + elseif (($null -ne $parameters.$parameter -and $null -eq $targetResource.$parameter) -or + ($null -eq $parameters.$parameter -and $null -ne $targetResource.$parameter) -or + (Compare-Object -ReferenceObject $parameters.$parameter ` + -DifferenceObject $targetResource.$parameter)) + { + if ([System.String]::IsNullOrEmpty($parameters.$parameter) -and ` + (-not ([System.String]::IsNullOrEmpty($targetResource.$parameter)))) + { + # We are clearing the existing value + Write-Verbose -Message ($script:localizedData.ClearingADUserProperty -f $parameter) + + $clearUserProperties += $adProperty.ADProperty + } #end if clear existing value + else + { + # We are replacing the existing value + Write-Verbose -Message ($script:localizedData.UpdatingADUserProperty -f + $parameter, ($parameters.$parameter -join ',')) + + if ($adProperty.UseCmdletParameter -eq $true) + { + # We need to pass the parameter explicitly to Set-ADUser, not via -Replace + $setADUserParams[$adProperty.ADProperty] = $parameters.$parameter + } + else + { + $replaceUserProperties[$adProperty.ADProperty] = $parameters.$parameter + } + } + } + } + + # Only pass -Clear and/or -Replace if we have something to set/change + if ($replaceUserProperties.Count -gt 0) + { + $setADUserParams['Replace'] = $replaceUserProperties + } + + if ($clearUserProperties.Count -gt 0) + { + $setADUserParams['Clear'] = $clearUserProperties; + } + + # Only call Set-ADUser if there are properties to change + if ($setADUserParams.Keys.Count -gt 0) + { + $setADUserParams += Get-ADCommonParameters @PSBoundParameters + + Write-Verbose -Message ($script:localizedData.UpdatingADUser -f $UserName, $DomainName) + + Write-Debug ('Set-ADUser Parameters: ' + ($setADUserParams | Out-String)) + + Set-ADUser @setADUserParams | Out-Null + } + + if ($moveUserRequired) + { + # Cannot move users by updating the DistinguishedName property + $moveAdObjectParameters = Get-ADCommonParameters @PSBoundParameters + + # Using the SamAccountName for identity with Move-ADObject does not work, use the DN instead + $moveAdObjectParameters['Identity'] = $targetResource.DistinguishedName + + Write-Verbose -Message ($script:localizedData.MovingADUser -f + $targetResource.Path, $parameters.Path) + + Move-ADObject @moveAdObjectParameters -TargetPath $parameters.Path + + # Set new target resource DN in case a rename is also required + $targetResource.DistinguishedName = "cn=$($targetResource.CommonName),$($parameters.Path)" + } + + if ($updateCnRequired) + { + # Cannot update the CN property directly. Must use Rename-ADObject + $renameAdObjectParameters = Get-ADCommonParameters @PSBoundParameters + + # Using the SamAccountName for identity with Rename-ADObject does not work, use the DN instead + $renameAdObjectParameters['Identity'] = $targetResource.DistinguishedName + + Write-Verbose -Message ($script:localizedData.UpdatingADUserProperty -f + 'CommonName', $parameters.CommonName) + + Rename-ADObject @renameAdObjectParameters -NewName $parameters.CommonName + } + } + } + elseif (($Ensure -eq 'Absent') -and ($targetResource.Ensure -eq 'Present')) + { + # User exists and needs removing + Write-Verbose ($script:localizedData.RemovingADUser -f $UserName, $DomainName) + + $adCommonParameters = Get-ADCommonParameters @PSBoundParameters + + Remove-ADUser @adCommonParameters -Confirm:$false | Out-Null + } + +} # end function Set-TargetResource + +<# + .SYNOPSIS + Internal function to validate unsupported options/configurations. + + .PARAMETER Password + Specifies a new password value for the account. + + .PARAMETER Enabled + Specifies if the account is enabled. Default value is $true. + + .PARAMETER ChangePasswordAtLogon + Specifies whether the account password must be changed during the next + logon attempt. This will only be enabled when the user is initially + created. This parameter cannot be set to $true if the parameter + PasswordNeverExpires is also set to $true. + + .PARAMETER PasswordNeverExpires + Specifies whether the password of an account can expire. + + .PARAMETER IgnoredArguments + Sets the rest of the arguments that are not passed into the this + function. +#> +function Assert-Parameters +{ + [CmdletBinding()] + param + ( + [Parameter()] + [ValidateNotNull()] + [System.Management.Automation.PSCredential] + $Password, + + [Parameter()] + [ValidateNotNull()] + [System.Boolean] + $Enabled = $true, + + [Parameter()] + [ValidateNotNull()] + [System.Boolean] + $ChangePasswordAtLogon, + + [Parameter()] + [ValidateNotNull()] + [System.Boolean] + $PasswordNeverExpires, + + [Parameter(ValueFromRemainingArguments)] + $IgnoredArguments + ) + + # We cannot test/set passwords on disabled AD accounts + if (($PSBoundParameters.ContainsKey('Password')) -and ($Enabled -eq $false)) + { + $errorMessage = $script:localizedData.PasswordParameterConflictError -f 'Enabled', $false, 'Password' + New-InvalidArgumentException -ArgumentName 'Password' -Message $errorMessage + } + + # ChangePasswordAtLogon cannot be set for an account that also has PasswordNeverExpires set + if ($PSBoundParameters.ContainsKey('ChangePasswordAtLogon') -and ` + $PSBoundParameters['ChangePasswordAtLogon'] -eq $true -and ` + $PSBoundParameters.ContainsKey('PasswordNeverExpires') -and ` + $PSBoundParameters['PasswordNeverExpires'] -eq $true) + { + $errorMessage = $script:localizedData.ChangePasswordParameterConflictError + New-InvalidArgumentException -ArgumentName 'ChangePasswordAtLogon, PasswordNeverExpires' -Message $errorMessage + } + +} #end function Assert-Parameters + +<# + .SYNOPSIS + Internal function to calculate the thumbnailPhoto hash. + + .PARAMETER Bytes + A Byte array that will be hashed. + + .OUTPUTS + Returns the MD5 hash of the bytes past in parameter Bytes, or $null if + the value of parameter is $null. +#> +function Get-MD5HashString +{ + [CmdletBinding()] + [OutputType([System.Byte[]])] + param + ( + [Parameter(Mandatory = $true)] + [AllowNull()] + [System.Byte[]] + $Bytes + ) + + $md5ReturnValue = $null + + if ($null -ne $Bytes) + { + $md5 = [System.Security.Cryptography.MD5]::Create() + $hashBytes = $md5.ComputeHash($Bytes) + + $md5ReturnValue = [System.BitConverter]::ToString($hashBytes).Replace('-', '') + } + + return $md5ReturnValue +} # end function Get-MD5HashString + +<# + .SYNOPSIS + Internal function to convert either a .jpg-file or a Base64-encoded jpeg + image to a Byte array. + + .PARAMETER ThumbnailPhoto + A string of either a .jpg-file or the string of a Base64-encoded jpeg image. + + .OUTPUTS + Returns a byte array of the image specified in the parameter ThumbnailPhoto. +#> +function Get-ThumbnailByteArray +{ + [CmdletBinding()] + [OutputType([System.Byte[]])] + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $ThumbnailPhoto + ) + + # If $ThumbnailPhoto contains '.' or '\' then we assume that we have a file path + if ($ThumbnailPhoto -match '\.|\\') + { + if (Test-Path -Path $ThumbnailPhoto) + { + Write-Verbose -Message ($script:localizedData.LoadingThumbnailFromFile -f $ThumbnailPhoto) + $thumbnailPhotoAsByteArray = Get-Content -Path $ThumbnailPhoto -Encoding Byte + } + else + { + $errorMessage = $script:localizedData.ThumbnailPhotoNotAFile + New-InvalidOperationException -Message $errorMessage + } + } + else + { + $thumbnailPhotoAsByteArray = [System.Convert]::FromBase64String($ThumbnailPhoto) + } + + return $thumbnailPhotoAsByteArray +} # end function Get-ThumbnailByteArray + +<# + .SYNOPSIS + Internal function to compare two thumbnail photos. + + .PARAMETER DesiredThumbnailPhoto + The desired thumbnail photo. Can be set to either a path to a .jpg-file, + a Base64-encoded jpeg image, an empty string, or $null. + + .PARAMETER CurrentThumbnailPhotoHash + The current thumbnail photo MD5 hash, or an empty string or $null if there + is no current thumbnail photo. + + .OUTPUTS + Returns $null if the thumbnail photos are the same, or a hashtable with + the hashes if the thumbnail photos do not match. +#> +function Compare-ThumbnailPhoto +{ + [CmdletBinding()] + [OutputType([System.Collections.Hashtable])] + param + ( + [Parameter(Mandatory = $true)] + [AllowEmptyString()] + [System.String] + $DesiredThumbnailPhoto, + + [Parameter(Mandatory = $true)] + [AllowEmptyString()] + [System.String] + $CurrentThumbnailPhotoHash + ) + + if ([System.String]::IsNullOrEmpty($DesiredThumbnailPhoto)) + { + $desiredThumbnailPhotoHash = $null + } + else + { + $desiredThumbnailPhotoHash = Get-MD5HashString ` + -Bytes (Get-ThumbnailByteArray -ThumbnailPhoto $DesiredThumbnailPhoto) + } + + <# + Compare thumbnail hashes. Must [System.String]::IsNullOrEmpty() to + compare empty values correctly. + #> + if ($desiredThumbnailPhotoHash -eq $CurrentThumbnailPhotoHash ` + -or ( + [System.String]::IsNullOrEmpty($desiredThumbnailPhotoHash) ` + -and [System.String]::IsNullOrEmpty($CurrentThumbnailPhotoHash) + ) + ) + { + $returnValue = $null + } + else + { + $returnValue = @{ + CurrentThumbnailPhotoHash = $CurrentThumbnailPhotoHash + DesiredThumbnailPhotoHash = $desiredThumbnailPhotoHash + } + } + + return $returnValue +} + +Export-ModuleMember -Function *-TargetResource diff --git a/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_ADUser/MSFT_ADUser.schema.mof b/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_ADUser/MSFT_ADUser.schema.mof new file mode 100644 index 0000000..be736b0 --- /dev/null +++ b/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_ADUser/MSFT_ADUser.schema.mof @@ -0,0 +1,66 @@ +[ClassVersion("1.0.0.0"), FriendlyName("ADUser")] +class MSFT_ADUser : OMI_BaseResource +{ + [Key, Description("Name of the domain where the user account is located (only used if password is managed).")] String DomainName; + [Key, Description("Specifies the Security Account Manager (SAM) account name of the user (ldapDisplayName 'sAMAccountName').")] String UserName; + [Write, Description("Specifies a new password value for the account."), EmbeddedInstance("MSFT_Credential")] String Password; + [Write, Description("Specifies whether the user account should be present or absent. Default value is 'Present'."), ValueMap{"Present", "Absent"},Values{"Present", "Absent"}] String Ensure; + [Write, Description("Specifies the common name assigned to the user account (ldapDisplayName 'cn'). If not specified the default value will be the same value provided in parameter UserName.")] String CommonName; + [Write, Description("Specifies the User Principal Name (UPN) assigned to the user account (ldapDisplayName 'userPrincipalName').")] String UserPrincipalName; + [Write, Description("Specifies the display name of the object (ldapDisplayName 'displayName').")] String DisplayName; + [Write, Description("Specifies the X.500 path of the Organizational Unit (OU) or container where the new object is created.")] String Path; + [Write, Description("Specifies the user's given name (ldapDisplayName 'givenName').")] String GivenName; + [Write, Description("Specifies the initials that represent part of a user's name (ldapDisplayName 'initials').")] String Initials; + [Write, Description("Specifies the user's last name or surname (ldapDisplayName 'sn').")] String Surname; + [Write, Description("Specifies a description of the object (ldapDisplayName 'description').")] String Description; + [Write, Description("Specifies the user's street address (ldapDisplayName 'streetAddress').")] String StreetAddress; + [Write, Description("Specifies the user's post office box number (ldapDisplayName 'postOfficeBox').")] String POBox; + [Write, Description("Specifies the user's town or city (ldapDisplayName 'l').")] String City; + [Write, Description("Specifies the user's or Organizational Unit's state or province (ldapDisplayName 'st').")] String State; + [Write, Description("Specifies the user's postal code or zip code (ldapDisplayName 'postalCode').")] String PostalCode; + [Write, Description("Specifies the country or region code for the user's language of choice (ldapDisplayName 'c').")] String Country; + [Write, Description("Specifies the user's department (ldapDisplayName 'department').")] String Department; + [Write, Description("Specifies the user's division (ldapDisplayName 'division').")] String Division; + [Write, Description("Specifies the user's company (ldapDisplayName 'company').")] String Company; + [Write, Description("Specifies the location of the user's office or place of business (ldapDisplayName 'physicalDeliveryOfficeName').")] String Office; + [Write, Description("Specifies the user's title (ldapDisplayName 'title').")] String JobTitle; + [Write, Description("Specifies the user's e-mail address (ldapDisplayName 'mail').")] String EmailAddress; + [Write, Description("Specifies the user's employee ID (ldapDisplayName 'employeeID').")] String EmployeeID; + [Write, Description("Specifies the user's employee number (ldapDisplayName 'employeeNumber').")] String EmployeeNumber; + [Write, Description("Specifies a user's home directory path (ldapDisplayName 'homeDirectory').")] String HomeDirectory; + [Write, Description("Specifies a drive that is associated with the UNC path defined by the HomeDirectory property (ldapDisplayName 'homeDrive').")] String HomeDrive; + [Write, Description("Specifies the URL of the home page of the object (ldapDisplayName 'wWWHomePage').")] String HomePage; + [Write, Description("Specifies a path to the user's profile (ldapDisplayName 'profilePath').")] String ProfilePath; + [Write, Description("Specifies a path to the user's log on script (ldapDisplayName 'scriptPath').")] String LogonScript; + [Write, Description("Specifies the notes attached to the user's accoutn (ldapDisplayName 'info').")] String Notes; + [Write, Description("Specifies the user's office telephone number (ldapDisplayName 'telephoneNumber').")] String OfficePhone; + [Write, Description("Specifies the user's mobile phone number (ldapDisplayName 'mobile').")] String MobilePhone; + [Write, Description("Specifies the user's fax phone number (ldapDisplayName 'facsimileTelephoneNumber').")] String Fax; + [Write, Description("Specifies the user's home telephone number (ldapDisplayName 'homePhone').")] String HomePhone; + [Write, Description("Specifies the user's pager number (ldapDisplayName 'pager').")] String Pager; + [Write, Description("Specifies the user's IP telephony phone number (ldapDisplayName 'ipPhone').")] String IPPhone; + [Write, Description("Specifies the user's manager specified as a Distinguished Name (ldapDisplayName 'manager').")] String Manager; + [Write, Description("Specifies the computers that the user can access. To specify more than one computer, create a single comma-separated list. You can identify a computer by using the Security Account Manager (SAM) account name (sAMAccountName) or the DNS host name of the computer. The SAM account name is the same as the NetBIOS name of the computer. The LDAP display name (ldapDisplayName) for this property is userWorkStations.")] String LogonWorkstations; + [Write, Description("Specifies the user's organization. This parameter sets the Organization property of a user object. The LDAP display name (ldapDisplayName) of this property is 'o'.")] String Organization; + [Write, Description("Specifies a name in addition to a user's given name and surname, such as the user's middle name. This parameter sets the OtherName property of a user object. The LDAP display name (ldapDisplayName) of this property is 'middleName'.")] String OtherName; + [Write, Description("Specifies if the account is enabled. Default value is $true.")] Boolean Enabled; + [Write, Description("Specifies whether the account password can be changed.")] Boolean CannotChangePassword; + [Write, Description("Specifies whether the account password must be changed during the next logon attempt. This will only be enabled when the user is initially created. This parameter cannot be set to $true if the parameter PasswordNeverExpires is also set to $true.")] Boolean ChangePasswordAtLogon; + [Write, Description("Specifies whether the password of an account can expire.")] Boolean PasswordNeverExpires; + [Write, Description("Specifies the Active Directory Domain Services instance to use to perform the task.")] String DomainController; + [Write, Description("Specifies the user account credentials to use to perform this task."), EmbeddedInstance("MSFT_Credential")] String Credential; + [Write, Description("Specifies the authentication context type used when testing passwords. Default value is 'Default'."), ValueMap{"Default","Negotiate"},Values{"Default","Negotiate"}] String PasswordAuthentication; + [Write, Description("Specifies whether existing user's password should be reset. Default value is $false.")] Boolean PasswordNeverResets; + [Write, Description("Specifies whether an account is trusted for Kerberos delegation. Default value is $false.")] Boolean TrustedForDelegation; + [Write, Description("Try to restore the user object from the recycle bin before creating a new one.")] Boolean RestoreFromRecycleBin; + [Write, Description("Specifies the service principal names for the user account.")] String ServicePrincipalNames[]; + [Write, Description("Specifies the proxy addresses for the user account.")] String ProxyAddresses[]; + [Write, Description("Indicates whether the security context of the user is delegated to a service. When this parameter is set to true, the security context of the account is not delegated to a service even when the service account is set as trusted for Kerberos delegation. This parameter sets the AccountNotDelegated property for an Active Directory account. This parameter also sets the ADS_UF_NOT_DELEGATED flag of the Active Directory User Account Control (UAC) attribute.")] Boolean AccountNotDelegated; + [Write, Description("Indicates whether reversible password encryption is allowed for the account. This parameter sets the AllowReversiblePasswordEncryption property of the account. This parameter also sets the ADS_UF_ENCRYPTED_TEXT_PASSWORD_ALLOWED flag of the Active Directory User Account Control (UAC) attribute.")] Boolean AllowReversiblePasswordEncryption; + [Write, Description("Specifies whether an account supports Kerberos service tickets which includes the authorization data for the user's device. This value sets the compound identity supported flag of the Active Directory msDS-SupportedEncryptionTypes attribute.")] Boolean CompoundIdentitySupported; + [Write, Description("Specifies whether the account requires a password. A password is not required for a new account. This parameter sets the PasswordNotRequired property of an account object.")] Boolean PasswordNotRequired; + [Write, Description("Specifies whether a smart card is required to logon. This parameter sets the SmartCardLoginRequired property for a user object. This parameter also sets the ADS_UF_SMARTCARD_REQUIRED flag of the Active Directory User Account Control attribute.")] Boolean SmartcardLogonRequired; + [Write, Description("Specifies the thumbnail photo to be used for the user object. Can be set either to a path pointing to a .jpg-file, or to a Base64-encoded jpeg image. If set to an empty string ('') the current thumbnail photo will be removed. The property ThumbnailPhoto will always return the image as a Base64-encoded string even if the configuration specified a file path.")] String ThumbnailPhoto; + [Read, Description("Returns the X.500 path of the object.")] String DistinguishedName; + [Read, Description("Return the MD5 hash of the current thumbnail photo, or $null if no thumbnail photo exist.")] String ThumbnailPhotoHash; +}; diff --git a/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_ADUser/README.md b/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_ADUser/README.md new file mode 100644 index 0000000..f4d0f42 --- /dev/null +++ b/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_ADUser/README.md @@ -0,0 +1,11 @@ +# Description + +The ADUser DSC resource will manage Users within Active Directory. + +## Requirements + +* Target machine must be running Windows Server 2008 R2 or later. +* The parameter `RestoreFromRecycleBin` requires that the feature Recycle + Bin has been enabled prior to an object is deleted. If the feature + Recycle Bin is disabled then the property `msDS-LastKnownRDN` is not + added the deleted object. diff --git a/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_ADUser/en-US/MSFT_ADUser.strings.psd1 b/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_ADUser/en-US/MSFT_ADUser.strings.psd1 new file mode 100644 index 0000000..5f16b24 --- /dev/null +++ b/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_ADUser/en-US/MSFT_ADUser.strings.psd1 @@ -0,0 +1,25 @@ +# culture="en-US" +ConvertFrom-StringData @' + RetrievingADUserError = Error retrieving '{0}' from domain '{1}'. (ADU0001) + PasswordParameterConflictError = Parameter '{0}' cannot be set to '{1}' when the '{2}' parameter is specified. (ADU0002) + ChangePasswordParameterConflictError = Parameter 'ChangePasswordAtLogon' cannot be set to 'true' when Parameter 'PasswordNeverExpires' is also set to 'true'. (ADU0003) + RetrievingADUser = Retrieving '{0}' from domain '{1}'. (ADU0004) + ADUserIsPresent = '{0}' is present in domain '{1}'. (ADU0007) + ADUserNotPresent = '{0}' is not present in domain '{1}'. (ADU0008) + ADUserInDesiredState = '{0}' is in the desired state. (ADU0009) + ADUserNotInDesiredState = '{0}' is not in the desired state. (ADU0010) + ADUserIsPresentButShouldBeAbsent = '{0}' is present but should be absent. (ADU0011) + ADUserIsAbsentButShouldBePresent = '{0}' is absent but should be present. (ADU0012) + ADUserNotDesiredPropertyState = '{0}' property is NOT in the desired state. Expected '{1}', actual '{2}'. (ADU0013) + AddingADUser = Adding '{0} to domain '{1}'. (ADU0014) + RemovingADUser = Removing '{0}' from domain '{1}'. (ADU0015) + UpdatingADUser = Updating '{0}' in domain '{1}'. (ADU0016) + SettingADUserPassword = Setting password for '{0}'. (ADU0017) + UpdatingADUserProperty = Updating property '{0}' with '{1}'. (ADU0018) + ClearingADUserProperty = Clearing property '{0}'. (ADU0019) + MovingADUser = Moving user from '{0}' to '{1}'. (ADU0020) + RestoringUser = Attempting to restore the user object {0} from the recycle bin. (ADU0022) + LoadingThumbnailFromFile = Importing thumbnail photo from the file '{0}'. (ADU0024) + ThumbnailPhotoNotAFile = Expected the thumbnail photo to be a file because the string contained the character '.' or '\', but the file could not be found. (ADU0025) + UpdatingThumbnailPhotoProperty = Updating property '{0}' with a new thumbnail photo with MD5 hash '{1}'. (ADU0026) +'@ diff --git a/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_ADUser/en-US/about_ADUser.help.txt b/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_ADUser/en-US/about_ADUser.help.txt new file mode 100644 index 0000000..38a741e --- /dev/null +++ b/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_ADUser/en-US/about_ADUser.help.txt @@ -0,0 +1,386 @@ +.NAME + ADUser + +.DESCRIPTION + The ADUser DSC resource will manage Users within Active Directory. + + ## Requirements + + * Target machine must be running Windows Server 2008 R2 or later. + * The parameter `RestoreFromRecycleBin` requires that the feature Recycle + Bin has been enabled prior to an object is deleted. If the feature + Recycle Bin is disabled then the property `msDS-LastKnownRDN` is not + added the deleted object. + +.PARAMETER DomainName + Key - String + Name of the domain where the user account is located (only used if password is managed). + +.PARAMETER UserName + Key - String + Specifies the Security Account Manager (SAM) account name of the user (ldapDisplayName 'sAMAccountName'). + +.PARAMETER Password + Write - PSCredential + Specifies a new password value for the account. + +.PARAMETER Ensure + Write - String + Allowed values: Present, Absent + Specifies whether the user account should be present or absent. Default value is 'Present'. + +.PARAMETER CommonName + Write - String + Specifies the common name assigned to the user account (ldapDisplayName 'cn'). If not specified the default value will be the same value provided in parameter UserName. + +.PARAMETER UserPrincipalName + Write - String + Specifies the User Principal Name (UPN) assigned to the user account (ldapDisplayName 'userPrincipalName'). + +.PARAMETER DisplayName + Write - String + Specifies the display name of the object (ldapDisplayName 'displayName'). + +.PARAMETER Path + Write - String + Specifies the X.500 path of the Organizational Unit (OU) or container where the new object is created. + +.PARAMETER GivenName + Write - String + Specifies the user's given name (ldapDisplayName 'givenName'). + +.PARAMETER Initials + Write - String + Specifies the initials that represent part of a user's name (ldapDisplayName 'initials'). + +.PARAMETER Surname + Write - String + Specifies the user's last name or surname (ldapDisplayName 'sn'). + +.PARAMETER Description + Write - String + Specifies a description of the object (ldapDisplayName 'description'). + +.PARAMETER StreetAddress + Write - String + Specifies the user's street address (ldapDisplayName 'streetAddress'). + +.PARAMETER POBox + Write - String + Specifies the user's post office box number (ldapDisplayName 'postOfficeBox'). + +.PARAMETER City + Write - String + Specifies the user's town or city (ldapDisplayName 'l'). + +.PARAMETER State + Write - String + Specifies the user's or Organizational Unit's state or province (ldapDisplayName 'st'). + +.PARAMETER PostalCode + Write - String + Specifies the user's postal code or zip code (ldapDisplayName 'postalCode'). + +.PARAMETER Country + Write - String + Specifies the country or region code for the user's language of choice (ldapDisplayName 'c'). + +.PARAMETER Department + Write - String + Specifies the user's department (ldapDisplayName 'department'). + +.PARAMETER Division + Write - String + Specifies the user's division (ldapDisplayName 'division'). + +.PARAMETER Company + Write - String + Specifies the user's company (ldapDisplayName 'company'). + +.PARAMETER Office + Write - String + Specifies the location of the user's office or place of business (ldapDisplayName 'physicalDeliveryOfficeName'). + +.PARAMETER JobTitle + Write - String + Specifies the user's title (ldapDisplayName 'title'). + +.PARAMETER EmailAddress + Write - String + Specifies the user's e-mail address (ldapDisplayName 'mail'). + +.PARAMETER EmployeeID + Write - String + Specifies the user's employee ID (ldapDisplayName 'employeeID'). + +.PARAMETER EmployeeNumber + Write - String + Specifies the user's employee number (ldapDisplayName 'employeeNumber'). + +.PARAMETER HomeDirectory + Write - String + Specifies a user's home directory path (ldapDisplayName 'homeDirectory'). + +.PARAMETER HomeDrive + Write - String + Specifies a drive that is associated with the UNC path defined by the HomeDirectory property (ldapDisplayName 'homeDrive'). + +.PARAMETER HomePage + Write - String + Specifies the URL of the home page of the object (ldapDisplayName 'wWWHomePage'). + +.PARAMETER ProfilePath + Write - String + Specifies a path to the user's profile (ldapDisplayName 'profilePath'). + +.PARAMETER LogonScript + Write - String + Specifies a path to the user's log on script (ldapDisplayName 'scriptPath'). + +.PARAMETER Notes + Write - String + Specifies the notes attached to the user's accoutn (ldapDisplayName 'info'). + +.PARAMETER OfficePhone + Write - String + Specifies the user's office telephone number (ldapDisplayName 'telephoneNumber'). + +.PARAMETER MobilePhone + Write - String + Specifies the user's mobile phone number (ldapDisplayName 'mobile'). + +.PARAMETER Fax + Write - String + Specifies the user's fax phone number (ldapDisplayName 'facsimileTelephoneNumber'). + +.PARAMETER HomePhone + Write - String + Specifies the user's home telephone number (ldapDisplayName 'homePhone'). + +.PARAMETER Pager + Write - String + Specifies the user's pager number (ldapDisplayName 'pager'). + +.PARAMETER IPPhone + Write - String + Specifies the user's IP telephony phone number (ldapDisplayName 'ipPhone'). + +.PARAMETER Manager + Write - String + Specifies the user's manager specified as a Distinguished Name (ldapDisplayName 'manager'). + +.PARAMETER LogonWorkstations + Write - String + Specifies the computers that the user can access. To specify more than one computer, create a single comma-separated list. You can identify a computer by using the Security Account Manager (SAM) account name (sAMAccountName) or the DNS host name of the computer. The SAM account name is the same as the NetBIOS name of the computer. The LDAP display name (ldapDisplayName) for this property is userWorkStations. + +.PARAMETER Organization + Write - String + Specifies the user's organization. This parameter sets the Organization property of a user object. The LDAP display name (ldapDisplayName) of this property is 'o'. + +.PARAMETER OtherName + Write - String + Specifies a name in addition to a user's given name and surname, such as the user's middle name. This parameter sets the OtherName property of a user object. The LDAP display name (ldapDisplayName) of this property is 'middleName'. + +.PARAMETER Enabled + Write - Boolean + Specifies if the account is enabled. Default value is $true. + +.PARAMETER CannotChangePassword + Write - Boolean + Specifies whether the account password can be changed. + +.PARAMETER ChangePasswordAtLogon + Write - Boolean + Specifies whether the account password must be changed during the next logon attempt. This will only be enabled when the user is initially created. This parameter cannot be set to $true if the parameter PasswordNeverExpires is also set to $true. + +.PARAMETER PasswordNeverExpires + Write - Boolean + Specifies whether the password of an account can expire. + +.PARAMETER DomainController + Write - String + Specifies the Active Directory Domain Services instance to use to perform the task. + +.PARAMETER Credential + Write - PSCredential + Specifies the user account credentials to use to perform this task. + +.PARAMETER PasswordAuthentication + Write - String + Allowed values: Default, Negotiate + Specifies the authentication context type used when testing passwords. Default value is 'Default'. + +.PARAMETER PasswordNeverResets + Write - Boolean + Specifies whether existing user's password should be reset. Default value is $false. + +.PARAMETER TrustedForDelegation + Write - Boolean + Specifies whether an account is trusted for Kerberos delegation. Default value is $false. + +.PARAMETER RestoreFromRecycleBin + Write - Boolean + Try to restore the user object from the recycle bin before creating a new one. + +.PARAMETER ServicePrincipalNames + Write - StringArray + Specifies the service principal names for the user account. + +.PARAMETER ProxyAddresses + Write - StringArray + Specifies the proxy addresses for the user account. + +.PARAMETER AccountNotDelegated + Write - Boolean + Indicates whether the security context of the user is delegated to a service. When this parameter is set to true, the security context of the account is not delegated to a service even when the service account is set as trusted for Kerberos delegation. This parameter sets the AccountNotDelegated property for an Active Directory account. This parameter also sets the ADS_UF_NOT_DELEGATED flag of the Active Directory User Account Control (UAC) attribute. + +.PARAMETER AllowReversiblePasswordEncryption + Write - Boolean + Indicates whether reversible password encryption is allowed for the account. This parameter sets the AllowReversiblePasswordEncryption property of the account. This parameter also sets the ADS_UF_ENCRYPTED_TEXT_PASSWORD_ALLOWED flag of the Active Directory User Account Control (UAC) attribute. + +.PARAMETER CompoundIdentitySupported + Write - Boolean + Specifies whether an account supports Kerberos service tickets which includes the authorization data for the user's device. This value sets the compound identity supported flag of the Active Directory msDS-SupportedEncryptionTypes attribute. + +.PARAMETER PasswordNotRequired + Write - Boolean + Specifies whether the account requires a password. A password is not required for a new account. This parameter sets the PasswordNotRequired property of an account object. + +.PARAMETER SmartcardLogonRequired + Write - Boolean + Specifies whether a smart card is required to logon. This parameter sets the SmartCardLoginRequired property for a user object. This parameter also sets the ADS_UF_SMARTCARD_REQUIRED flag of the Active Directory User Account Control attribute. + +.PARAMETER ThumbnailPhoto + Write - String + Specifies the thumbnail photo to be used for the user object. Can be set either to a path pointing to a .jpg-file, or to a Base64-encoded jpeg image. If set to an empty string ('') the current thumbnail photo will be removed. The property ThumbnailPhoto will always return the image as a Base64-encoded string even if the configuration specified a file path. + +.PARAMETER DistinguishedName + Read - String + Returns the X.500 path of the object. + +.PARAMETER ThumbnailPhotoHash + Read - String + Return the MD5 hash of the current thumbnail photo, or $null if no thumbnail photo exist. + +.EXAMPLE 1 + +This configuration will create a user with a managed password. +This might be used to manage the lifecycle of a service account. + +Configuration ADUser_CreateUserAndManagePassword_Config +{ + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.Management.Automation.PSCredential] + $Password + ) + + Import-DscResource -Module ActiveDirectoryDsc + + Node localhost + { + ADUser 'Contoso\ExampleUser' + { + Ensure = 'Present' + UserName = 'ExampleUser' + Password = $Password + DomainName = 'contoso.com' + Path = 'CN=Users,DC=contoso,DC=com' + } + } +} + +.EXAMPLE 2 + +This configuration will create a user with a password and then ignore +when the password has changed. This might be used with a traditional +user account where a managed password is not desired. + +Configuration ADUser_CreateUserAndIgnorePasswordChanges_Config +{ + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.Management.Automation.PSCredential] + $Password + ) + + Import-DscResource -Module ActiveDirectoryDsc + + Node localhost + { + ADUser 'Contoso\ExampleUser' + { + Ensure = 'Present' + UserName = 'ExampleUser' + Password = $Password + PasswordNeverResets = $true + DomainName = 'contoso.com' + Path = 'CN=Users,DC=contoso,DC=com' + } + } +} + +.EXAMPLE 3 + +This configuration will update a user with a thumbnail photo using +a jpeg image encoded as a Base64 string. + +Configuration ADUser_UpdateThumbnailPhotoAsBase64_Config +{ + Import-DscResource -Module ActiveDirectoryDsc + + Node localhost + { + ADUser 'Contoso\ExampleUser' + { + UserName = 'ExampleUser' + DomainName = 'contoso.com' + ThumbnailPhoto = '/9j/4AAQSkZJRgABAQEAYABgAAD/4QB .... STRING TRUNCATED FOR LENGTH' + } + } +} + +.EXAMPLE 4 + +This configuration will update a user with a thumbnail photo using +a jpeg file. + +Configuration ADUser_UpdateThumbnailPhotoFromFile_Config +{ + Import-DscResource -Module ActiveDirectoryDsc + + Node localhost + { + ADUser 'Contoso\ExampleUser' + { + UserName = 'ExampleUser' + DomainName = 'contoso.com' + ThumbnailPhoto = 'C:\ThumbnailPhotos\ExampleUser.jpg' + } + } +} + +.EXAMPLE 5 + +This configuration will remove the thumbnail photo from the user. + +Configuration ADUser_RemoveThumbnailPhoto_Config +{ + Import-DscResource -Module ActiveDirectoryDsc + + Node localhost + { + ADUser 'Contoso\ExampleUser' + { + UserName = 'ExampleUser' + DomainName = 'contoso.com' + ThumbnailPhoto = '' + } + } +} + + diff --git a/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_WaitForADDomain/MSFT_WaitForADDomain.psm1 b/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_WaitForADDomain/MSFT_WaitForADDomain.psm1 new file mode 100644 index 0000000..e0bd01e --- /dev/null +++ b/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_WaitForADDomain/MSFT_WaitForADDomain.psm1 @@ -0,0 +1,629 @@ +$script:resourceModulePath = Split-Path -Path (Split-Path -Path $PSScriptRoot -Parent) -Parent +$script:modulesFolderPath = Join-Path -Path $script:resourceModulePath -ChildPath 'Modules' + +$script:localizationModulePath = Join-Path -Path $script:modulesFolderPath -ChildPath 'ActiveDirectoryDsc.Common' +Import-Module -Name (Join-Path -Path $script:localizationModulePath -ChildPath 'ActiveDirectoryDsc.Common.psm1') + +$script:localizedData = Get-LocalizedData -ResourceName 'MSFT_WaitForADDomain' + +# This file is used to remember the number of times the node has been rebooted. +$script:restartLogFile = Join-Path $env:temp -ChildPath 'WaitForADDomain_Reboot.tmp' + +# This scriptblock is ran inside the background job. +$script:waitForDomainControllerScriptBlock = { + param + ( + # Only used for unit tests, and debug purpose. + [Parameter()] + [System.Boolean] + $RunOnce, + + [Parameter(Mandatory = $true)] + [System.String] + $DomainName, + + [Parameter()] + [System.String] + $SiteName, + + [Parameter()] + [System.Management.Automation.PSCredential] + $Credential, + + [Parameter()] + [System.Boolean] + $WaitForValidCredentials + ) + + $domainFound = $false + + do + { + Import-Module ActiveDirectoryDsc + + $findDomainControllerParameters = @{ + DomainName = $DomainName + } + + if ($SiteName) + { + $findDomainControllerParameters['SiteName'] = $SiteName + + } + + if ($null -ne $Credential) + { + $findDomainControllerParameters['Credential'] = $Credential + } + + if ($PSBoundParameters.ContainsKey('WaitForValidCredentials')) + { + $findDomainControllerParameters['WaitForValidCredentials'] = $WaitForValidCredentials + } + + $currentDomainController = $null + + # Using verbose so that Receive-Job can output whats happened. + $currentDomainController = Find-DomainController @findDomainControllerParameters -Verbose + + if ($currentDomainController) + { + $domainFound = $true + } + else + { + $domainFound = $false + + # Using verbose so that Receive-Job can output whats happened. + Clear-DnsClientCache -Verbose + + Start-Sleep -Seconds 10 + } + } until ($domainFound -or $RunOnce) +} + +<# + .SYNOPSIS + Returns the current state of the specified Active Directory domain. + + .PARAMETER DomainName + Specifies the fully qualified domain name to wait for. + + .PARAMETER SiteName + Specifies the site in the domain where to look for a domain controller. + + .PARAMETER Credential + Specifies the credentials that are used when accessing the domain, + unless the built-in PsDscRunAsCredential is used. + + .PARAMETER WaitTimeout + Specifies the timeout in seconds that the resource will wait for the + domain to be accessible. Default value is 300 seconds. + + .PARAMETER RestartCount + Specifies the number of times the node will be reboot in an effort to + connect to the domain. + + .PARAMETER WaitForValidCredentials + Specifies that the resource will not throw an error if authentication + fails using the provided credentials and continue wait for the timeout. + This can be used if the credentials are known to eventually exist but + there are a potential timing issue before they are accessible. +#> +function Get-TargetResource +{ + [OutputType([System.Collections.Hashtable])] + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $DomainName, + + [Parameter()] + [System.String] + $SiteName, + + [Parameter()] + [System.Management.Automation.PSCredential] + $Credential, + + [Parameter()] + [System.UInt64] + $WaitTimeout = 300, + + [Parameter()] + [System.UInt32] + $RestartCount, + + [Parameter()] + [System.Boolean] + $WaitForValidCredentials + ) + + $findDomainControllerParameters = @{ + DomainName = $DomainName + } + + Write-Verbose -Message ( + $script:localizedData.SearchDomainController -f $DomainName + ) + + if ($PSBoundParameters.ContainsKey('SiteName')) + { + $findDomainControllerParameters['SiteName'] = $SiteName + + Write-Verbose -Message ( + $script:localizedData.SearchInSiteOnly -f $SiteName + ) + } + + if ($PSBoundParameters.ContainsKey('Credential')) + { + $cimCredentialInstance = New-CimCredentialInstance -Credential $Credential + + $findDomainControllerParameters['Credential'] = $Credential + + Write-Verbose -Message ( + $script:localizedData.ImpersonatingCredentials -f $Credential.UserName + ) + } + else + { + if ($null -ne $global:PsDscContext.RunAsUser) + { + # Running using PsDscRunAsCredential + Write-Verbose -Message ( + $script:localizedData.ImpersonatingCredentials -f $global:PsDscContext.RunAsUser + ) + } + else + { + # Running as SYSTEM or current user. + Write-Verbose -Message ( + $script:localizedData.ImpersonatingCredentials -f (Get-CurrentUser).Name + ) + } + + $cimCredentialInstance = $null + } + + $currentDomainController = $null + + if ($PSBoundParameters.ContainsKey('WaitForValidCredentials')) + { + $findDomainControllerParameters['WaitForValidCredentials'] = $WaitForValidCredentials + } + + $currentDomainController = Find-DomainController @findDomainControllerParameters + + if ($currentDomainController) + { + $domainFound = $true + $domainControllerSiteName = $currentDomainController.SiteName + + Write-Verbose -Message $script:localizedData.FoundDomainController + + } + else + { + $domainFound = $false + $domainControllerSiteName = $null + + Write-Verbose -Message $script:localizedData.NoDomainController + } + + return @{ + DomainName = $DomainName + SiteName = $domainControllerSiteName + Credential = $cimCredentialInstance + WaitTimeout = $WaitTimeout + RestartCount = $RestartCount + IsAvailable = $domainFound + WaitForValidCredentials = $WaitForValidCredentials + } +} + +<# + .SYNOPSIS + Waits for the specified Active Directory domain to have a domain + controller that can serve connections. + + .PARAMETER DomainName + Specifies the fully qualified domain name to wait for. + + .PARAMETER SiteName + Specifies the site in the domain where to look for a domain controller. + + .PARAMETER Credential + Specifies the credentials that are used when accessing the domain, + unless the built-in PsDscRunAsCredential is used. + + .PARAMETER WaitTimeout + Specifies the timeout in seconds that the resource will wait for the + domain to be accessible. Default value is 300 seconds. + + .PARAMETER RestartCount + Specifies the number of times the node will be reboot in an effort to + connect to the domain. + + .PARAMETER WaitForValidCredentials + Specifies that the resource will not throw an error if authentication + fails using the provided credentials and continue wait for the timeout. + This can be used if the credentials are known to eventually exist but + there are a potential timing issue before they are accessible. +#> +function Set-TargetResource +{ + <# + Suppressing this rule because $global:DSCMachineStatus is used to trigger + a reboot if the domain name cannot be found withing the timeout period. + #> + [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidGlobalVars', '')] + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $DomainName, + + [Parameter()] + [System.String] + $SiteName, + + [Parameter()] + [System.Management.Automation.PSCredential] + $Credential, + + [Parameter()] + [System.UInt64] + $WaitTimeout = 300, + + [Parameter()] + [System.UInt32] + $RestartCount, + + [Parameter()] + [System.Boolean] + $WaitForValidCredentials + ) + + Write-Verbose -Message ( + $script:localizedData.WaitingForDomain -f $DomainName, $WaitTimeout + ) + + # Only pass properties that could be used when fetching the domain controller. + $compareTargetResourceStateParameters = @{ + DomainName = $DomainName + SiteName = $SiteName + Credential = $Credential + WaitForValidCredentials = $WaitForValidCredentials + } + + <# + Removes any keys not bound to $PSBoundParameters. + Need the @() around this to get a new array to enumerate. + #> + @($compareTargetResourceStateParameters.Keys) | ForEach-Object { + if (-not $PSBoundParameters.ContainsKey($_)) + { + $compareTargetResourceStateParameters.Remove($_) + } + } + + <# + This returns array of hashtables which contain the properties ParameterName, + Expected, Actual, and InDesiredState. In this case only the property + 'IsAvailable' will be returned. + #> + $compareTargetResourceStateResult = Compare-TargetResourceState @compareTargetResourceStateParameters + + $isInDesiredState = $compareTargetResourceStateResult.Where( { $_.ParameterName -eq 'IsAvailable' }).InDesiredState + + if (-not $isInDesiredState) + { + $startJobParameters = @{ + ScriptBlock = $script:waitForDomainControllerScriptBlock + ArgumentList = @( + $false + $DomainName + $SiteName + $Credential + $WaitForValidCredentials + ) + } + + Write-Verbose -Message $script:localizedData.StartBackgroundJob + + $jobSearchDomainController = Start-Job @startJobParameters + + Write-Verbose -Message $script:localizedData.WaitBackgroundJob + + $waitJobResult = Wait-Job -Job $jobSearchDomainController -Timeout $WaitTimeout + + # Wait-Job returns an object if the job completed or failed within the timeout. + if ($waitJobResult) + { + Write-Verbose -Message $script:localizedData.BackgroundJobFinished + switch ($waitJobResult.State) + { + 'Failed' + { + Write-Warning -Message $script:localizedData.BackgroundJobFailed + + $foundDomainController = $false + } + + 'Completed' + { + Write-Verbose -Message $script:localizedData.BackgroundJobSuccessful + + if ($PSBoundParameters.ContainsKey('RestartCount')) + { + Remove-RestartLogFile + } + + $foundDomainController = $true + } + } + } + else + { + Write-Warning -Message $script:localizedData.TimeoutReached + + if ($PSBoundParameters.ContainsKey('RestartCount')) + { + # if the file does not exist this will set $currentRestartCount to 0. + [System.UInt32] $currentRestartCount = Get-Content $restartLogFile -ErrorAction SilentlyContinue + + if ($currentRestartCount -lt $RestartCount) + { + $currentRestartCount += 1 + + Set-Content -Path $restartLogFile -Value $currentRestartCount + + Write-Verbose -Message ( + $script:localizedData.RestartWasRequested -f $currentRestartCount, $RestartCount + ) + + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', '', + Justification = 'Set LCM DSCMachineStatus to indicate reboot required')] + $global:DSCMachineStatus = 1 + } + } + + # The timeout was reached and no restarts was requested. + $foundDomainController = $false + } + + # Only output the result from the running job if Verbose was chosen. + if ($PSBoundParameters.ContainsKey('Verbose') -or $waitJobResult.State -eq 'Failed') + { + Write-Verbose -Message $script:localizedData.StartOutputBackgroundJob + + Receive-Job -Job $jobSearchDomainController + + Write-Verbose -Message $script:localizedData.EndOutputBackgroundJob + } + + Write-Verbose -Message $script:localizedData.RemoveBackgroundJob + + # Forcedly remove the job even if it was not completed. + Remove-Job -Job $jobSearchDomainController -Force + } + else + { + $foundDomainController = $true + } + + if ($foundDomainController) + { + Write-Verbose -Message ($script:localizedData.DomainInDesiredState -f $DomainName) + } + else + { + throw $script:localizedData.NoDomainController + } +} + +<# + .SYNOPSIS + Determines if the specified Active Directory domain have a domain controller + that can serve connections. + + .PARAMETER DomainName + Specifies the fully qualified domain name to wait for. + + .PARAMETER SiteName + Specifies the site in the domain where to look for a domain controller. + + .PARAMETER Credential + Specifies the credentials that are used when accessing the domain, + unless the built-in PsDscRunAsCredential is used. + + .PARAMETER WaitTimeout + Specifies the timeout in seconds that the resource will wait for the + domain to be accessible. Default value is 300 seconds. + + .PARAMETER RestartCount + Specifies the number of times the node will be reboot in an effort to + connect to the domain. + + .PARAMETER WaitForValidCredentials + Specifies that the resource will not throw an error if authentication + fails using the provided credentials and continue wait for the timeout. + This can be used if the credentials are known to eventually exist but + there are a potential timing issue before they are accessible. +#> +function Test-TargetResource +{ + [OutputType([System.Boolean])] + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $DomainName, + + [Parameter()] + [System.String] + $SiteName, + + [Parameter()] + [System.Management.Automation.PSCredential] + $Credential, + + [Parameter()] + [System.UInt64] + $WaitTimeout = 300, + + [Parameter()] + [System.UInt32] + $RestartCount, + + [Parameter()] + [System.Boolean] + $WaitForValidCredentials + ) + + Write-Verbose -Message ( + $script:localizedData.TestConfiguration -f $DomainName + ) + + # Only pass properties that could be used when fetching the domain controller. + $compareTargetResourceStateParameters = @{ + DomainName = $DomainName + SiteName = $SiteName + Credential = $Credential + WaitForValidCredentials = $WaitForValidCredentials + } + + <# + Removes any keys not bound to $PSBoundParameters. + Need the @() around this to get a new array to enumerate. + #> + @($compareTargetResourceStateParameters.Keys) | ForEach-Object { + if (-not $PSBoundParameters.ContainsKey($_)) + { + $compareTargetResourceStateParameters.Remove($_) + } + } + + <# + This returns array of hashtables which contain the properties ParameterName, + Expected, Actual, and InDesiredState. In this case only the property + 'IsAvailable' will be returned. + #> + $compareTargetResourceStateResult = Compare-TargetResourceState @compareTargetResourceStateParameters + + if ($false -in $compareTargetResourceStateResult.InDesiredState) + { + $testTargetResourceReturnValue = $false + + Write-Verbose -Message ( + $script:localizedData.DomainNotInDesiredState -f $DomainName + ) + } + else + { + $testTargetResourceReturnValue = $true + + if ($PSBoundParameters.ContainsKey('RestartCount') -and $RestartCount -gt 0 ) + { + Remove-RestartLogFile + } + + Write-Verbose -Message ( + $script:localizedData.DomainInDesiredState -f $DomainName + ) + } + + return $testTargetResourceReturnValue +} + +<# + .SYNOPSIS + Compares the properties in the current state with the properties of the + desired state and returns a hashtable with the comparison result. + + .PARAMETER DomainName + Specifies the fully qualified domain name to wait for. + + .PARAMETER SiteName + Specifies the site in the domain where to look for a domain controller. + + .PARAMETER Credential + Specifies the credentials that are used when accessing the domain, + unless the built-in PsDscRunAsCredential is used. + + .PARAMETER WaitForValidCredentials + Specifies that the resource will not throw an error if authentication + fails using the provided credentials and continue wait for the timeout. + This can be used if the credentials are known to eventually exist but + there are a potential timing issue before they are accessible. +#> +function Compare-TargetResourceState +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $DomainName, + + [Parameter()] + [System.String] + $SiteName, + + [Parameter()] + [System.Management.Automation.PSCredential] + $Credential, + + [Parameter()] + [System.Boolean] + $WaitForValidCredentials + ) + + $getTargetResourceParameters = @{ + DomainName = $DomainName + SiteName = $SiteName + Credential = $Credential + WaitForValidCredentials = $WaitForValidCredentials + } + + <# + Removes any keys not bound to $PSBoundParameters. + Need the @() around this to get a new array to enumerate. + #> + @($getTargetResourceParameters.Keys) | ForEach-Object { + if (-not $PSBoundParameters.ContainsKey($_)) + { + $getTargetResourceParameters.Remove($_) + } + } + + $getTargetResourceResult = Get-TargetResource @getTargetResourceParameters + + <# + Only interested in the read-only property IsAvailable, which + should always be compared to the value $true. + #> + $compareResourcePropertyStateParameters = @{ + CurrentValues = $getTargetResourceResult + DesiredValues = @{ + IsAvailable = $true + } + Properties = 'IsAvailable' + } + + return Compare-ResourcePropertyState @compareResourcePropertyStateParameters +} + +function Remove-RestartLogFile +{ + [CmdletBinding()] + param () + + if (Test-Path -Path $script:restartLogFile) + { + Remove-Item $script:restartLogFile -Force -ErrorAction SilentlyContinue + } +} diff --git a/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_WaitForADDomain/MSFT_WaitForADDomain.schema.mof b/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_WaitForADDomain/MSFT_WaitForADDomain.schema.mof new file mode 100644 index 0000000..238fc2e --- /dev/null +++ b/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_WaitForADDomain/MSFT_WaitForADDomain.schema.mof @@ -0,0 +1,11 @@ +[ClassVersion("1.0.1.0"), FriendlyName("WaitForADDomain")] +class MSFT_WaitForADDomain : OMI_BaseResource +{ + [Key, Description("Specifies the fully qualified domain name to wait for.")] String DomainName; + [Write, Description("Specifies the site in the domain where to look for a domain controller.")] String SiteName; + [Write, Description("Specifies the credentials that are used when accessing the domain, unless the built-in PsDscRunAsCredential is used."), EmbeddedInstance("MSFT_Credential")] String Credential; + [Write, Description("Specifies the timeout in seconds that the resource will wait for the domain to be accessible. Default value is 300 seconds.")] UInt64 WaitTimeout; + [Write, Description("Specifies the number of times the node will be reboot in an effort to connect to the domain.")] UInt32 RestartCount; + [Write, Description("Specifies that the resource will not throw an error if authentication fails using the provided credentials and continue wait for the timeout. This can be used if the credentials are known to eventually exist but there are a potential timing issue before they are accessible.")] Boolean WaitForValidCredentials; + [Read, Description("Returns a value indicating if a domain controller was found.")] Boolean IsAvailable; +}; diff --git a/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_WaitForADDomain/README.md b/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_WaitForADDomain/README.md new file mode 100644 index 0000000..5c82c5f --- /dev/null +++ b/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_WaitForADDomain/README.md @@ -0,0 +1,24 @@ +# Description + +The WaitForADDomain resource is used to wait for Active Directory domain +controller to become available in the domain, or available in +a specific site in the domain. + +>Running the resource as *NT AUTHORITY\SYSTEM*, only work when +>evaluating the domain on the current node, for example on a +>node that should be a domain controller (which might require a +>restart of the node once the node becomes a domain controller). +>In all other scenarios use either the built-in parameter +>`PsDscRunAsCredential`, or the parameter `Credential`. + +Using the parameter `WaitForValidCredentials` ignores authentication +errors a let the resource wait until time timeout is reached. If the +parameter `WaitForValidCredentials` is not specified and the resource +throws an authentication error, then the resource will fail. But the +Local Configuration Manger (LCM) will automatically run the configuration +again to try to get the node in desired state. If and when the LCM retries +depends on how the LCM is configured. + +## Requirements + +* Target machine must be running Windows Server 2008 R2 or later. diff --git a/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_WaitForADDomain/en-US/MSFT_WaitForADDomain.strings.psd1 b/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_WaitForADDomain/en-US/MSFT_WaitForADDomain.strings.psd1 new file mode 100644 index 0000000..1bfbae4 --- /dev/null +++ b/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_WaitForADDomain/en-US/MSFT_WaitForADDomain.strings.psd1 @@ -0,0 +1,22 @@ +# culture='en-US' +ConvertFrom-StringData @' + SearchDomainController = Searching for a domain controller in the domain '{0}'. (WFADD0001) + RestartWasRequested = A restart was requested when no domain controller was found. Restart number {0} of a total of {1}. (WFADD0003) + DomainInDesiredState = Domain '{0}' is in the desired state. (WFADD0006) + DomainNotInDesiredState = Domain '{0}' is not in the desired state. (WFADD0007) + FoundDomainController = Found domain controller. (WFADD0009) + NoDomainController = No domain controller was found. (WFADD0010) + ImpersonatingCredentials = Impersonating the credentials '{0}' when looking for a domain controller. (WFADD0011) + SearchInSiteOnly = Limiting the search scope for a domain controller to the site '{0}'. (WFADD0012) + TestConfiguration = Determining the current state of the Active Directory domain '{0}'. (WFADD0013) + BackgroundJobFinished = The background job finished running. (WFADD0014) + BackgroundJobFailed = The background job failed while searching for the domain controller. Returning the result of the background job. (WFADD0015) + TimeoutReached = The background job did not completed before the timeout period. (WFADD0016) + WaitingForDomain = Waiting for a domain '{0}' is available or until the timeout of {1} seconds has been reached. (WFADD0017) + StartBackgroundJob = Starting background job that will be searching for the domain controller. (WFADD0018) + WaitBackgroundJob = Waiting for the background job to finish, or timeout. (WFADD0019) + BackgroundJobSuccessful = The background job completed successfully. (WFADD0020) + StartOutputBackgroundJob = --- Start of result from background job. (WFADD0021) + EndOutputBackgroundJob = --- End of result from background job. (WFADD0022) + RemoveBackgroundJob = Removing the background job. (WFADD0023) +'@ diff --git a/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_WaitForADDomain/en-US/about_WaitForADDomain.help.txt b/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_WaitForADDomain/en-US/about_WaitForADDomain.help.txt new file mode 100644 index 0000000..eb474f4 --- /dev/null +++ b/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/DSCResources/MSFT_WaitForADDomain/en-US/about_WaitForADDomain.help.txt @@ -0,0 +1,273 @@ +.NAME + WaitForADDomain + +.DESCRIPTION + The WaitForADDomain resource is used to wait for Active Directory domain + controller to become available in the domain, or available in + a specific site in the domain. + + >Running the resource as *NT AUTHORITY\SYSTEM*, only work when + >evaluating the domain on the current node, for example on a + >node that should be a domain controller (which might require a + >restart of the node once the node becomes a domain controller). + >In all other scenarios use either the built-in parameter + >`PsDscRunAsCredential`, or the parameter `Credential`. + + Using the parameter `WaitForValidCredentials` ignores authentication + errors a let the resource wait until time timeout is reached. If the + parameter `WaitForValidCredentials` is not specified and the resource + throws an authentication error, then the resource will fail. But the + Local Configuration Manger (LCM) will automatically run the configuration + again to try to get the node in desired state. If and when the LCM retries + depends on how the LCM is configured. + + ## Requirements + + * Target machine must be running Windows Server 2008 R2 or later. + +.PARAMETER DomainName + Key - String + Specifies the fully qualified domain name to wait for. + +.PARAMETER SiteName + Write - String + Specifies the site in the domain where to look for a domain controller. + +.PARAMETER Credential + Write - PSCredential + Specifies the credentials that are used when accessing the domain, unless the built-in PsDscRunAsCredential is used. + +.PARAMETER WaitTimeout + Write - UInt64 + Specifies the timeout in seconds that the resource will wait for the domain to be accessible. Default value is 300 seconds. + +.PARAMETER RestartCount + Write - UInt32 + Specifies the number of times the node will be reboot in an effort to connect to the domain. + +.PARAMETER WaitForValidCredentials + Write - Boolean + Specifies that the resource will not throw an error if authentication fails using the provided credentials and continue wait for the timeout. This can be used if the credentials are known to eventually exist but there are a potential timing issue before they are accessible. + +.PARAMETER IsAvailable + Read - Boolean + Returns a value indicating if a domain controller was found. + +.EXAMPLE 1 + +This configuration will wait for an Active Directory domain controller +to respond within 300 seconds (default) in the domain 'contoso.com' +before returning and allowing the configuration to continue to run. +If the timeout is reached an error will be thrown. +This will use the current user when determining if the domain is available, +if run though LCM this will use SYSTEM (which might not have access). + +Configuration WaitForADDomain_WaitForDomainController_Config +{ + Import-DscResource -Module ActiveDirectoryDsc + + Node localhost + { + WaitForADDomain 'contoso.com' + { + DomainName = 'contoso.com' + } + } +} + +.EXAMPLE 2 + +This configuration will wait for an Active Directory domain controller +to respond within 300 seconds (default) in the domain 'contoso.com' +before returning and allowing the configuration to continue to run. +If the timeout is reached an error will be thrown. +This will use the user credential passed in the built-in PsDscRunAsCredential +parameter when determining if the domain is available. + +Configuration WaitForADDomain_WaitForDomainControllerUsingBuiltInCredential_Config +{ + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.Management.Automation.PSCredential] + $Credential + ) + + Import-DscResource -Module ActiveDirectoryDsc + + Node localhost + { + WaitForADDomain 'contoso.com' + { + DomainName = 'contoso.com' + + PsDscRunAsCredential = $Credential + } + } +} + +.EXAMPLE 3 + +This configuration will wait for an Active Directory domain controller +to respond within 300 seconds (default) in the domain 'contoso.com' +before returning and allowing the configuration to continue to run. +If the timeout is reached an error will be thrown. +This will use the user credential passed in the parameter Credential +when determining if the domain is available. + +Configuration WaitForADDomain_WaitForDomainControllerUsingCredential_Config +{ + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.Management.Automation.PSCredential] + $Credential + ) + + Import-DscResource -Module ActiveDirectoryDsc + + Node localhost + { + WaitForADDomain 'contoso.com' + { + DomainName = 'contoso.com' + Credential = $Credential + } + } +} + +.EXAMPLE 4 + +This configuration will wait for an Active Directory domain controller +in the site 'Europe' to respond within 300 seconds (default) in the +domain 'contoso.com' before returning and allowing the configuration to +continue to run. +If the timeout is reached an error will be thrown. +This will use the user credential passed in the built-in PsDscRunAsCredential +parameter when determining if the domain is available. + +Configuration WaitForADDomain_WaitForDomainControllerInSite_Config +{ + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.Management.Automation.PSCredential] + $Credential + ) + + Import-DscResource -Module ActiveDirectoryDsc + + Node localhost + { + WaitForADDomain 'contoso.com' + { + DomainName = 'contoso.com' + SiteName = 'Europe' + + PsDscRunAsCredential = $Credential + } + } +} + +.EXAMPLE 5 + +This configuration will wait for an Active Directory domain controller +to respond within 300 seconds (default) in the domain 'contoso.com' +before returning and allowing the configuration to continue to run. +If the timeout is reached the node will be restarted up to two times +and again wait after each restart. If no domain controller is found +after the second restart an error will be thrown. +This will use the user credential passed in the built-in PsDscRunAsCredential +parameter when determining if the domain is available. + +Configuration WaitForADDomain_WaitForDomainControllerWithReboot_Config +{ + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.Management.Automation.PSCredential] + $Credential + ) + + Import-DscResource -Module ActiveDirectoryDsc + + Node localhost + { + WaitForADDomain 'contoso.com' + { + DomainName = 'contoso.com' + RestartCount = 2 + + PsDscRunAsCredential = $Credential + } + } +} + +.EXAMPLE 6 + +This configuration will wait for an Active Directory domain controller +to respond within 600 seconds in the domain 'contoso.com' before +returning and allowing the configuration to continue to run. If the timeout +is reached an error will be thrown. +This will use the user credential passed in the built-in PsDscRunAsCredential +parameter when determining if the domain is available. + +Configuration WaitForADDomain_WaitForDomainControllerWithLongerDelay_Config +{ + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.Management.Automation.PSCredential] + $Credential + ) + + Import-DscResource -Module ActiveDirectoryDsc + + Node localhost + { + WaitForADDomain 'contoso.com' + { + DomainName = 'contoso.com' + WaitTimeout = 600 + + PsDscRunAsCredential = $Credential + } + } +} + +.EXAMPLE 7 + +This configuration will wait for an Active Directory domain controller +to respond within the default period, and ignore any authentication +errors. + +Configuration WaitForADDomain_WaitForDomainControllerIgnoringAuthenticationErrors_Config +{ + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.Management.Automation.PSCredential] + $Credential + ) + + Import-DscResource -Module ActiveDirectoryDsc + + Node localhost + { + WaitForADDomain 'contoso.com' + { + DomainName = 'contoso.com' + WaitForValidCredentials = $true + + PsDscRunAsCredential = $Credential + } + } +} + + diff --git a/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/Modules/ActiveDirectoryDsc.Common/ActiveDirectoryDsc.Common.psd1 b/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/Modules/ActiveDirectoryDsc.Common/ActiveDirectoryDsc.Common.psd1 new file mode 100644 index 0000000..a5bed4d --- /dev/null +++ b/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/Modules/ActiveDirectoryDsc.Common/ActiveDirectoryDsc.Common.psd1 @@ -0,0 +1,80 @@ +@{ + # Script module or binary module file associated with this manifest. + RootModule = 'ActiveDirectoryDsc.Common.psm1' + + # Version number of this module. + ModuleVersion = '1.0' + + # ID used to uniquely identify this module + GUID = 'a4af6d71-e828-4ec3-8a05-6083b8d5d4c2' + + # Author of this module + Author = 'DSC Community' + + # Company or vendor of this module + CompanyName = 'DSC Community' + + # Copyright statement for this module + Copyright = 'Copyright the DSC Community contributors. All rights reserved.' + + # Description of the functionality provided by this module + Description = 'Functions used by the DSC resources in ActiveDirectoryDsc.' + + # Functions to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no functions to export. + FunctionsToExport = @( + 'New-InvalidArgumentException' + 'New-InvalidOperationException' + 'New-ObjectNotFoundException' + 'New-InvalidResultException' + 'Get-LocalizedData' + 'Start-ProcessWithTimeout' + 'Assert-Module' + 'Test-DomainMember' + 'Get-DomainName' + 'Resolve-DomainFQDN' + 'Get-ADObjectParentDN' + 'Assert-MemberParameters' + 'Remove-DuplicateMembers' + 'Test-Members' + 'ConvertTo-TimeSpan' + 'ConvertFrom-TimeSpan' + 'Get-ADCommonParameters' + 'Test-ADReplicationSite' + 'ConvertTo-DeploymentForestMode' + 'ConvertTo-DeploymentDomainMode' + 'Restore-ADCommonObject' + 'Get-ADDomainNameFromDistinguishedName' + 'Add-ADCommonGroupMember' + 'Get-DomainControllerObject' + 'Test-IsDomainController' + 'Convert-PropertyMapToObjectProperties' + 'Compare-ResourcePropertyState' + 'Test-DscPropertyState' + 'Assert-ADPSDrive' + 'Set-DscADComputer' + 'New-CimCredentialInstance' + 'Add-TypeAssembly' + 'Get-ADDirectoryContext' + 'Find-DomainController' + 'Get-CurrentUser' + 'Test-Password' + 'Test-PrincipalContextCredentials' + ) + + # Cmdlets to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no cmdlets to export. + CmdletsToExport = @() + + # Variables to export from this module + VariablesToExport = @() + + # Aliases to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no aliases to export. + AliasesToExport = @() + + # Private data to pass to the module specified in RootModule/ModuleToProcess. This may also contain a PSData hashtable with additional module metadata used by PowerShell. + PrivateData = @{ + + PSData = @{ + } # End of PSData hashtable + + } # End of PrivateData hashtable +} diff --git a/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/Modules/ActiveDirectoryDsc.Common/ActiveDirectoryDsc.Common.psm1 b/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/Modules/ActiveDirectoryDsc.Common/ActiveDirectoryDsc.Common.psm1 new file mode 100644 index 0000000..5d2522b --- /dev/null +++ b/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/Modules/ActiveDirectoryDsc.Common/ActiveDirectoryDsc.Common.psm1 @@ -0,0 +1,2356 @@ +<# + .SYNOPSIS + Retrieves the localized string data based on the machine's culture. + Falls back to en-US strings if the machine's culture is not supported. + + .PARAMETER ResourceName + The name of the resource as it appears before '.strings.psd1' of the localized string file. + For example: + For WindowsOptionalFeature: MSFT_WindowsOptionalFeature + For Service: MSFT_ServiceResource + For Registry: MSFT_RegistryResource + For Helper: SqlServerDscHelper + + .PARAMETER ScriptRoot + Optional. The root path where to expect to find the culture folder. This is only needed + for localization in helper modules. This should not normally be used for resources. + + .NOTES + To be able to use localization in the helper function, this function must + be first in the file, before Get-LocalizedData is used by itself to load + localized data for this helper module (see directly after this function). +#> +function Get-LocalizedData +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $ResourceName, + + [Parameter()] + [ValidateNotNullOrEmpty()] + [System.String] + $ScriptRoot + ) + + if (-not $ScriptRoot) + { + $dscResourcesFolder = Join-Path -Path (Split-Path -Path (Split-Path -Path $PSScriptRoot -Parent) -Parent) -ChildPath 'DSCResources' + $resourceDirectory = Join-Path -Path $dscResourcesFolder -ChildPath $ResourceName + } + else + { + $resourceDirectory = $ScriptRoot + } + + $localizedStringFileLocation = Join-Path -Path $resourceDirectory -ChildPath $PSUICulture + + if (-not (Test-Path -Path $localizedStringFileLocation)) + { + # Fallback to en-US + $localizedStringFileLocation = Join-Path -Path $resourceDirectory -ChildPath 'en-US' + } + + Import-LocalizedData ` + -BindingVariable 'localizedData' ` + -FileName "$ResourceName.strings.psd1" ` + -BaseDirectory $localizedStringFileLocation + + return $localizedData +} + +<# + .SYNOPSIS + Creates and throws an invalid argument exception. + + .PARAMETER Message + The message explaining why this error is being thrown. + + .PARAMETER ArgumentName + The name of the invalid argument that is causing this error to be thrown. +#> +function New-InvalidArgumentException +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $Message, + + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $ArgumentName + ) + + $argumentException = New-Object -TypeName 'ArgumentException' ` + -ArgumentList @($Message, $ArgumentName) + + $newObjectParameters = @{ + TypeName = 'System.Management.Automation.ErrorRecord' + ArgumentList = @($argumentException, $ArgumentName, 'InvalidArgument', $null) + } + + $errorRecord = New-Object @newObjectParameters + + throw $errorRecord +} + +<# + .SYNOPSIS + Creates and throws an invalid operation exception. + + .PARAMETER Message + The message explaining why this error is being thrown. + + .PARAMETER ErrorRecord + The error record containing the exception that is causing this terminating error. +#> +function New-InvalidOperationException +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $Message, + + [Parameter()] + [ValidateNotNull()] + [System.Management.Automation.ErrorRecord] + $ErrorRecord + ) + + if ($null -eq $ErrorRecord) + { + $invalidOperationException = New-Object -TypeName 'InvalidOperationException' ` + -ArgumentList @($Message) + } + else + { + $invalidOperationException = New-Object -TypeName 'InvalidOperationException' ` + -ArgumentList @($Message, $ErrorRecord.Exception) + } + + $newObjectParameters = @{ + TypeName = 'System.Management.Automation.ErrorRecord' + ArgumentList = @( + $invalidOperationException.ToString(), + 'MachineStateIncorrect', + 'InvalidOperation', + $null + ) + } + + $errorRecordToThrow = New-Object @newObjectParameters + + throw $errorRecordToThrow +} + +<# + .SYNOPSIS + Creates and throws an object not found exception. + + .PARAMETER Message + The message explaining why this error is being thrown. + + .PARAMETER ErrorRecord + The error record containing the exception that is causing this terminating error. +#> +function New-ObjectNotFoundException +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $Message, + + [Parameter()] + [ValidateNotNull()] + [System.Management.Automation.ErrorRecord] + $ErrorRecord + ) + + if ($null -eq $ErrorRecord) + { + $exception = New-Object -TypeName 'System.Exception' ` + -ArgumentList @($Message) + } + else + { + $exception = New-Object -TypeName 'System.Exception' ` + -ArgumentList @($Message, $ErrorRecord.Exception) + } + + $newObjectParameters = @{ + TypeName = 'System.Management.Automation.ErrorRecord' + ArgumentList = @( + $exception.ToString(), + 'MachineStateIncorrect', + 'ObjectNotFound', + $null + ) + } + + $errorRecordToThrow = New-Object @newObjectParameters + + throw $errorRecordToThrow +} + +<# + .SYNOPSIS + Creates and throws an invalid result exception. + + .PARAMETER Message + The message explaining why this error is being thrown. + + .PARAMETER ErrorRecord + The error record containing the exception that is causing this terminating error. +#> +function New-InvalidResultException +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $Message, + + [Parameter()] + [ValidateNotNull()] + [System.Management.Automation.ErrorRecord] + $ErrorRecord + ) + + if ($null -eq $ErrorRecord) + { + $exception = New-Object -TypeName 'System.Exception' ` + -ArgumentList @($Message) + } + else + { + $exception = New-Object -TypeName 'System.Exception' ` + -ArgumentList @($Message, $ErrorRecord.Exception) + } + + $newObjectParameters = @{ + TypeName = 'System.Management.Automation.ErrorRecord' + ArgumentList = @( + $exception.ToString(), + 'MachineStateIncorrect', + 'InvalidResult', + $null + ) + } + + $errorRecordToThrow = New-Object @newObjectParameters + + throw $errorRecordToThrow +} + +<# + .SYNOPSIS + Starts a process with a timeout. + + .PARAMETER FilePath + String containing the path to the executable to start. + + .PARAMETER ArgumentList + The arguments that should be passed to the executable. + + .PARAMETER Timeout + The timeout in seconds to wait for the process to finish. + +#> +function Start-ProcessWithTimeout +{ + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $FilePath, + + [Parameter()] + [System.String[]] + $ArgumentList, + + [Parameter(Mandatory = $true)] + [System.UInt32] + $Timeout + ) + + $startProcessParameters = @{ + FilePath = $FilePath + ArgumentList = $ArgumentList + PassThru = $true + NoNewWindow = $true + ErrorAction = 'Stop' + } + + $sqlSetupProcess = Start-Process @startProcessParameters + + Write-Verbose -Message ($script:localizedData.StartProcess -f $sqlSetupProcess.Id, $startProcessParameters.FilePath, $Timeout) -Verbose + + Wait-Process -InputObject $sqlSetupProcess -Timeout $Timeout -ErrorAction 'Stop' + + return $sqlSetupProcess.ExitCode +} + +<# + .SYNOPSIS + Assert if the role specific module is installed or not and optionally + import it. + + .PARAMETER ModuleName + The name of the module to assert is installed. + + .PARAMETER ImportModule + This switch causes the module to be imported if it is installed. +#> +function Assert-Module +{ + [CmdletBinding()] + param + ( + [Parameter()] + [ValidateNotNullOrEmpty()] + [System.String] + $ModuleName = 'ActiveDirectory', + + [Parameter()] + [System.Management.Automation.SwitchParameter] + $ImportModule + ) + + if (-not (Get-Module -Name $ModuleName -ListAvailable)) + { + $errorMessage = $script:localizedData.ModuleNotFoundError -f $moduleName + New-ObjectNotFoundException -Message $errorMessage + } + + if ($ImportModule) + { + Import-Module -Name $ModuleName + } +} #end function Assert-Module + +<# + .SYNOPSIS + Tests whether this computer is a member of a domain. +#> +function Test-DomainMember +{ + [CmdletBinding()] + [OutputType([System.Boolean])] + param + ( + ) + + $isDomainMember = [System.Boolean] (Get-CimInstance -ClassName Win32_ComputerSystem -Verbose:$false).PartOfDomain + + return $isDomainMember +} + + +<# + .SYNOPSIS + Get the domain name of this computer. +#> +function Get-DomainName +{ + [CmdletBinding()] + [OutputType([System.String])] + param + ( + ) + + $domainName = [System.String] (Get-CimInstance -ClassName Win32_ComputerSystem -Verbose:$false).Domain + + return $domainName +} # function Get-DomainName + +<# + .SYNOPSIS + Assemble a fully qualifies domain name by appending the domain name + to the parent domain name. + + .PARAMETER DomainName + The domain name to append to the ParentDomainName. + + .PARAMETER ParentDomainName + The parent domain name. +#> +function Resolve-DomainFQDN +{ + [CmdletBinding()] + [OutputType([System.String])] + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $DomainName, + + [Parameter()] + [AllowNull()] + [System.String] + $ParentDomainName + ) + + $domainFQDN = $DomainName + + if ($ParentDomainName) + { + $domainFQDN = '{0}.{1}' -f $DomainName, $ParentDomainName + } + + return $domainFQDN +} + +<# + .SYNOPSIS + Get an Active Directory object's parent distinguished name. + + .PARAMETER DN + The distinguished name of the object to return the parent from. + + .NOTES + Copyright (c) 2016 The University Of Vermont + All rights reserved. + + Redistribution and use in source and binary forms, with or without modification, are permitted provided that + the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the + following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the + following disclaimer in the documentation and/or other materials provided with the distribution. + 3. Neither the name of the University nor the names of its contributors may be used to endorse or promote + products derived from this software without specific prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE AUTHOR "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. + IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS + OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF + THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + http://www.uvm.edu/~gcd/code-license/ +#> +function Get-ADObjectParentDN +{ + [CmdletBinding()] + [OutputType([System.String])] + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $DN + ) + + # https://www.uvm.edu/~gcd/2012/07/listing-parent-of-ad-object-in-powershell/ + $distinguishedNameParts = $DN -split '(? +function Assert-MemberParameters +{ + [CmdletBinding()] + param + ( + [Parameter()] + [System.String[]] + $Members, + + [Parameter()] + [ValidateNotNull()] + [System.String[]] + $MembersToInclude, + + [Parameter()] + [ValidateNotNull()] + [System.String[]] + $MembersToExclude + ) + + if ($PSBoundParameters.ContainsKey('Members')) + { + if ($PSBoundParameters.ContainsKey('MembersToInclude') -or $PSBoundParameters.ContainsKey('MembersToExclude')) + { + # If Members are provided, Include and Exclude are not allowed. + $errorMessage = $script:localizedData.MembersAndIncludeExcludeError -f 'Members', 'MembersToInclude', 'MembersToExclude' + New-InvalidArgumentException -ArgumentName 'Members' -Message $errorMessage + } + } + + if ($PSBoundParameters.ContainsKey('MembersToInclude')) + { + $MembersToInclude = Remove-DuplicateMembers -Members $MembersToInclude + } + + if ($PSBoundParameters.ContainsKey('MembersToExclude')) + { + $MembersToExclude = Remove-DuplicateMembers -Members $MembersToExclude + } + + if (($PSBoundParameters.ContainsKey('MembersToInclude')) -and ($PSBoundParameters.ContainsKey('MembersToExclude'))) + { + if (($MembersToInclude.Length -eq 0) -and ($MembersToExclude.Length -eq 0)) + { + $errorMessage = $script:localizedData.IncludeAndExcludeAreEmptyError -f 'MembersToInclude', 'MembersToExclude' + New-InvalidArgumentException -ArgumentName 'MembersToInclude, MembersToExclude' -Message $errorMessage + } + + # Both MembersToInclude and MembersToExclude were provided. Check if they have common principals. + foreach ($member in $MembersToInclude) + { + if ($member -in $MembersToExclude) + { + $errorMessage = $script:localizedData.IncludeAndExcludeConflictError -f $member, 'MembersToInclude', 'MembersToExclude' + New-InvalidArgumentException -ArgumentName 'MembersToInclude, MembersToExclude' -Message $errorMessage + } + } + } + +} #end function Assert-MemberParameters + +<# + .SYNOPSIS + Remove duplicate members from a string array. The comparison is + case insensitive. + + .PARAMETER Members + The array of members to remove duplicates from. + + .OUTPUTS + A string array with the unique members- +#> +function Remove-DuplicateMembers +{ + [CmdletBinding()] + [OutputType([System.String[]])] + param + ( + [Parameter()] + [System.String[]] + $Members + ) + + if ($null -eq $Members -or $Members.Count -eq 0) + { + $uniqueMembers = [System.String[]] @() + } + else + { + $uniqueMembers = [System.String[]] ($members | Sort-Object -Unique) + } + + <# + Comma make sure we return the string array as the correct type, + and also make sure one entry is returned as a string array. + #> + return , $uniqueMembers +} #end function RemoveDuplicateMembers + +<# + .SYNOPSIS + Test whether the existing array members match the defined explicit array + and include/exclude the specified members. + + .PARAMETER ExistingMembers + Existing array members. + + .PARAMETER Members + Explicit array members. + + .PARAMETER MembersToInclude + Compulsory array members. + + .PARAMETER MembersToExclude + Excluded array members. +#> +function Test-Members +{ + [CmdletBinding()] + [OutputType([System.Boolean])] + param + ( + [Parameter()] + [AllowNull()] + [System.String[]] + $ExistingMembers, + + [Parameter()] + [AllowNull()] + [System.String[]] + $Members, + + [Parameter()] + [AllowNull()] + [System.String[]] + $MembersToInclude, + + [Parameter()] + [AllowNull()] + [System.String[]] + $MembersToExclude + ) + + if ($PSBoundParameters.ContainsKey('Members')) + { + if ($null -eq $Members -or (($Members.Count -eq 1) -and ($Members[0].Length -eq 0))) + { + $Members = @() + } + + Write-Verbose ($script:localizedData.CheckingMembers -f 'Explicit') + + $Members = Remove-DuplicateMembers -Members $Members + + if ($ExistingMembers.Count -ne $Members.Count) + { + Write-Verbose -Message ($script:localizedData.MembershipCountMismatch -f $Members.Count, $ExistingMembers.Count) + return $false + } + + $isInDesiredState = $true + + foreach ($member in $Members) + { + if ($member -notin $ExistingMembers) + { + Write-Verbose -Message ($script:localizedData.MemberNotInDesiredState -f $member) + $isInDesiredState = $false + } + } + + if (-not $isInDesiredState) + { + Write-Verbose -Message ($script:localizedData.MembershipNotDesiredState -f $member) + return $false + } + } #end if $Members + + if ($PSBoundParameters.ContainsKey('MembersToInclude')) + { + if ($null -eq $MembersToInclude -or (($MembersToInclude.Count -eq 1) -and ($MembersToInclude[0].Length -eq 0))) + { + $MembersToInclude = @() + } + + Write-Verbose -Message ($script:localizedData.CheckingMembers -f 'Included') + + $MembersToInclude = Remove-DuplicateMembers -Members $MembersToInclude + + $isInDesiredState = $true + + foreach ($member in $MembersToInclude) + { + if ($member -notin $ExistingMembers) + { + Write-Verbose -Message ($script:localizedData.MemberNotInDesiredState -f $member) + $isInDesiredState = $false + } + } + + if (-not $isInDesiredState) + { + Write-Verbose -Message ($script:localizedData.MembershipNotDesiredState -f $member) + return $false + } + } #end if $MembersToInclude + + if ($PSBoundParameters.ContainsKey('MembersToExclude')) + { + if ($null -eq $MembersToExclude -or (($MembersToExclude.Count -eq 1) -and ($MembersToExclude[0].Length -eq 0))) + { + $MembersToExclude = @() + } + + Write-Verbose -Message ($script:localizedData.CheckingMembers -f 'Excluded') + + $MembersToExclude = Remove-DuplicateMembers -Members $MembersToExclude + + $isInDesiredState = $true + + foreach ($member in $MembersToExclude) + { + if ($member -in $ExistingMembers) + { + Write-Verbose -Message ($script:localizedData.MemberNotInDesiredState -f $member) + $isInDesiredState = $false + } + } + + if (-not $isInDesiredState) + { + Write-Verbose -Message ($script:localizedData.MembershipNotDesiredState -f $member) + return $false + } + } #end if $MembersToExclude + + Write-Verbose -Message $script:localizedData.MembershipInDesiredState + return $true +} #end function Test-Membership + +<# + .SYNOPSIS + Convert a specified time period in seconds, minutes, hours or days into + a time span object. + + .PARAMETER TimeSpan + The length of time to use for the time span. + + .PARAMETER TimeSpanType + The units of measure in the TimeSpan parameter. +#> +function ConvertTo-TimeSpan +{ + [CmdletBinding()] + [OutputType([System.TimeSpan])] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.UInt32] + $TimeSpan, + + [Parameter(Mandatory = $true)] + [ValidateSet('Seconds', 'Minutes', 'Hours', 'Days')] + [System.String] + $TimeSpanType + ) + + $newTimeSpanParams = @{} + + switch ($TimeSpanType) + { + 'Seconds' + { + $newTimeSpanParams['Seconds'] = $TimeSpan + } + + 'Minutes' + { + $newTimeSpanParams['Minutes'] = $TimeSpan + } + + 'Hours' + { + $newTimeSpanParams['Hours'] = $TimeSpan + } + + 'Days' + { + $newTimeSpanParams['Days'] = $TimeSpan + } + } + return (New-TimeSpan @newTimeSpanParams) +} #end function ConvertTo-TimeSpan + +<# + .SYNOPSIS + Converts a System.TimeSpan into the number of seconds, minutes, hours or days. + + .PARAMETER TimeSpan + TimeSpan to convert into an integer + + .PARAMETER TimeSpanType + Convert timespan into the total number of seconds, minutes, hours or days. + + .EXAMPLE + ConvertFrom-TimeSpan -TimeSpan (New-TimeSpan -Days 15) -TimeSpanType Seconds + + Returns the number of seconds in 15 days. +#> +function ConvertFrom-TimeSpan +{ + [CmdletBinding()] + [OutputType([System.Int32])] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.TimeSpan] + $TimeSpan, + + [Parameter(Mandatory = $true)] + [ValidateSet('Seconds', 'Minutes', 'Hours', 'Days')] + [System.String] + $TimeSpanType + ) + + switch ($TimeSpanType) + { + 'Seconds' + { + return $TimeSpan.TotalSeconds -as [System.UInt32] + } + 'Minutes' + { + return $TimeSpan.TotalMinutes -as [System.UInt32] + } + 'Hours' + { + return $TimeSpan.TotalHours -as [System.UInt32] + } + 'Days' + { + return $TimeSpan.TotalDays -as [System.UInt32] + } + } +} #end function ConvertFrom-TimeSpan + +<# + .SYNOPSIS + Returns common AD cmdlet connection parameter for splatting. + + .PARAMETER CommonName + When specified, a CommonName overrides theUsed by the ADUser + cmdletReturns the Identity as the Name key. For example, the + Get-ADUser, Set-ADUser and Remove-ADUser cmdlets take an Identity + parameter, but the New-ADUser cmdlet uses the Name parameter. + + .PARAMETER UseNameParameter + Returns the Identity as the Name key. For example, the Get-ADUser, + Set-ADUser and Remove-ADUser cmdlets take an Identity parameter, + but the New-ADUser cmdlet uses the Name parameter. + + .EXAMPLE + $getADUserParams = Get-CommonADParameters @PSBoundParameters + + Returns connection parameters suitable for Get-ADUser using the + splatted cmdlet parameters. + + .EXAMPLE + $newADUserParams = Get-CommonADParameters @PSBoundParameters -UseNameParameter + + Returns connection parameters suitable for New-ADUser using + the splatted cmdlet parameters. +#> +function Get-ADCommonParameters +{ + [CmdletBinding()] + [OutputType([System.Collections.Hashtable])] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [Alias('UserName', 'GroupName', 'ComputerName', 'ServiceAccountName')] + [System.String] + $Identity, + + [Parameter()] + [ValidateNotNullOrEmpty()] + [System.String] + $CommonName, + + [Parameter()] + [ValidateNotNull()] + [System.Management.Automation.PSCredential] + [System.Management.Automation.CredentialAttribute()] + $Credential, + + [Parameter()] + [ValidateNotNullOrEmpty()] + [Alias('DomainController')] + [System.String] + $Server, + + [Parameter()] + [System.Management.Automation.SwitchParameter] + $UseNameParameter, + + [Parameter()] + [System.Management.Automation.SwitchParameter] + $PreferCommonName, + + # Catch all to enable splatted $PSBoundParameters + [Parameter(ValueFromRemainingArguments)] + $RemainingArguments + ) + + if ($UseNameParameter) + { + if ($PreferCommonName -and ($PSBoundParameters.ContainsKey('CommonName'))) + { + $adConnectionParameters = @{ + Name = $CommonName + } + } + else + { + $adConnectionParameters = @{ + Name = $Identity + } + } + } + else + { + if ($PreferCommonName -and ($PSBoundParameters.ContainsKey('CommonName'))) + { + $adConnectionParameters = @{ + Identity = $CommonName + } + } + else + { + $adConnectionParameters = @{ + Identity = $Identity + } + } + } + + if ($Credential) + { + $adConnectionParameters['Credential'] = $Credential + } + + if ($Server) + { + $adConnectionParameters['Server'] = $Server + } + + return $adConnectionParameters +} #end function Get-ADCommonParameters + +<# + .SYNOPSIS + Test Active Directory replication site availablity. + + .PARAMETER SiteName + The replication site name to test the availability of. + + .PARAMETER DomainName + The domain name containing the replication site. + + .PARAMETER Credential + The credential to use to access the replication site. +#> +function Test-ADReplicationSite +{ + [CmdletBinding()] + [OutputType([System.Boolean])] + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $SiteName, + + [Parameter(Mandatory = $true)] + [System.String] + $DomainName, + + [Parameter(Mandatory = $true)] + [System.Management.Automation.PSCredential] + $Credential + ) + + Write-Verbose -Message ($script:localizedData.CheckingSite -f $SiteName) + + $existingDC = "$((Get-ADDomainController -Discover -DomainName $DomainName -ForceDiscover).HostName)" + + try + { + $site = Get-ADReplicationSite -Identity $SiteName -Server $existingDC -Credential $Credential + } + catch [Microsoft.ActiveDirectory.Management.ADIdentityNotFoundException] + { + return $false + } + + return ($null -ne $site) +} + +<# + .SYNOPSIS + Convert a ModeId or Microsoft.ActiveDirectory.Management.ADForestMode to + a Microsoft.DirectoryServices.Deployment.Types.ForestMode type. + + .PARAMETER ModeId + The ModeId value to convert to a + Microsoft.DirectoryServices.Deployment.Types.ForestMode type. + + .PARAMETER Mode + The Microsoft.ActiveDirectory.Management.ADForestMode value to convert + to a Microsoft.DirectoryServices.Deployment.Types.ForestMode type +#> +function ConvertTo-DeploymentForestMode +{ + [CmdletBinding()] + [OutputType([Microsoft.DirectoryServices.Deployment.Types.ForestMode])] + param + ( + [Parameter( + Mandatory = $true, + ParameterSetName = 'ById')] + [System.UInt16] + $ModeId, + + [Parameter( + Mandatory = $true, + ParameterSetName = 'ByName')] + [AllowNull()] + [System.Nullable``1[Microsoft.ActiveDirectory.Management.ADForestMode]] + $Mode + ) + + $convertedMode = $null + + if ($PSCmdlet.ParameterSetName -eq 'ByName' -and $Mode) + { + $convertedMode = $Mode -as [Microsoft.DirectoryServices.Deployment.Types.ForestMode] + } + + if ($PSCmdlet.ParameterSetName -eq 'ById') + { + $convertedMode = $ModeId -as [Microsoft.DirectoryServices.Deployment.Types.ForestMode] + } + + if ([enum]::GetValues([Microsoft.DirectoryServices.Deployment.Types.ForestMode]) -notcontains $convertedMode) + { + return $null + } + + return $convertedMode +} + +<# + .SYNOPSIS + Convert a ModeId or Microsoft.ActiveDirectory.Management.ADDomainMode to + a Microsoft.DirectoryServices.Deployment.Types.DomainMode type. + + .PARAMETER ModeId + The ModeId value to convert to a + Microsoft.DirectoryServices.Deployment.Types.DomainMode type. + + .PARAMETER Mode + The Microsoft.ActiveDirectory.Management.ADDomainMode value to convert + to a Microsoft.DirectoryServices.Deployment.Types.DomainMode type +#> +function ConvertTo-DeploymentDomainMode +{ + [CmdletBinding()] + [OutputType([Microsoft.DirectoryServices.Deployment.Types.DomainMode])] + param + ( + [Parameter( + Mandatory = $true, + ParameterSetName = 'ById')] + [System.UInt16] + $ModeId, + + [Parameter( + Mandatory = $true, + ParameterSetName = 'ByName')] + [AllowNull()] + [System.Nullable``1[Microsoft.ActiveDirectory.Management.ADDomainMode]] + $Mode + ) + + $convertedMode = $null + + if ($PSCmdlet.ParameterSetName -eq 'ByName' -and $Mode) + { + $convertedMode = $Mode -as [Microsoft.DirectoryServices.Deployment.Types.DomainMode] + } + + if ($PSCmdlet.ParameterSetName -eq 'ById') + { + $convertedMode = $ModeId -as [Microsoft.DirectoryServices.Deployment.Types.DomainMode] + } + + if ([enum]::GetValues([Microsoft.DirectoryServices.Deployment.Types.DomainMode]) -notcontains $convertedMode) + { + return $null + } + + return $convertedMode +} + +<# + .SYNOPSIS + Restore and AD object from the AD recyle bin. + + .PARAMETER Identity + The identity of the object to restore. + + .PARAMETER ObjectClass + The type of the AD object to restore. + + .PARAMETER Credential + The credential to use to restore the object in the Active + Directory. + + .PARAMETER Server + The name of the domain controller use to restore the object. +#> +function Restore-ADCommonObject +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [Alias('UserName', 'GroupName', 'ComputerName', 'ServiceAccountName')] + [System.String] + $Identity, + + [Parameter(Mandatory = $true)] + [ValidateSet('Computer', 'OrganizationalUnit', 'User', 'Group')] + [System.String] + $ObjectClass, + + [Parameter()] + [ValidateNotNull()] + [System.Management.Automation.PSCredential] + [System.Management.Automation.CredentialAttribute()] + $Credential, + + [Parameter()] + [ValidateNotNullOrEmpty()] + [Alias('DomainController')] + [System.String] + $Server + ) + + $restoreFilter = 'msDS-LastKnownRDN -eq "{0}" -and objectClass -eq "{1}" -and isDeleted -eq $true' -f $Identity, $ObjectClass + Write-Verbose -Message ($script:localizedData.FindInRecycleBin -f $restoreFilter) -Verbose + + <# + Using IsDeleted and IncludeDeletedObjects will mean that the cmdlet does not throw + any more, and simply returns $null instead + #> + $commonParams = Get-ADCommonParameters @PSBoundParameters + $getAdObjectParams = $commonParams.Clone() + $getAdObjectParams.Remove('Identity') + $getAdObjectParams['Filter'] = $restoreFilter + $getAdObjectParams['IncludeDeletedObjects'] = $true + $getAdObjectParams['Properties'] = @('whenChanged') + + # If more than one object is returned, we pick the one that was changed last. + $restorableObject = Get-ADObject @getAdObjectParams | + Sort-Object -Descending -Property 'whenChanged' | + Select-Object -First 1 + + $restoredObject = $null + + if ($restorableObject) + { + Write-Verbose -Message ($script:localizedData.FoundRestoreTargetInRecycleBin -f $Identity, $ObjectClass, $restorableObject.DistinguishedName) -Verbose + + try + { + $restoreParams = $commonParams.Clone() + $restoreParams['PassThru'] = $true + $restoreParams['ErrorAction'] = 'Stop' + $restoreParams['Identity'] = $restorableObject.DistinguishedName + $restoredObject = Restore-ADObject @restoreParams + + Write-Verbose -Message ($script:localizedData.RecycleBinRestoreSuccessful -f $Identity, $ObjectClass) -Verbose + } + catch [Microsoft.ActiveDirectory.Management.ADException] + { + # After Get-TargetResource is through, only one error can occur here: Object parent does not exist + $errorMessage = $script:localizedData.RecycleBinRestoreFailed -f $Identity, $ObjectClass + New-InvalidOperationException -Message $errorMessage -ErrorRecord $_ + } + } + else + { + Write-Verbose -Message ($script:localizedData.NoObjectFoundInRecycleBin) -Verbose + } + + return $restoredObject +} + +<# + .SYNOPSIS + Converts an Active Directory distinguished name into a fully + qualified domain name. + + .DESCRIPTION + Takes an Active Directory distinguished name as input, returns + the domain FQDN. + + .PARAMETER DistinguishedName + The distinguished name to convert into the FQDN. + + .EXAMPLE + Get-ADDomainNameFromDistinguishedName -DistinguishedName 'CN=ExampleObject,OU=ExampleOU,DC=example,DC=com' + + .NOTES + Author: Robert D. Biddle (https://github.com/RobBiddle) + Created: December.20.2017 +#> +function Get-ADDomainNameFromDistinguishedName +{ + [CmdletBinding()] + param + ( + [Parameter()] + [System.String] + $DistinguishedName + ) + + if ($DistinguishedName -notlike '*DC=*') + { + return + } + + $splitDistinguishedName = ($DistinguishedName -split 'DC=') + $splitDistinguishedNameParts = $splitDistinguishedName[1..$splitDistinguishedName.Length] + $domainFqdn = '' + + foreach ($part in $splitDistinguishedNameParts) + { + $domainFqdn += "DC=$part" + } + + $domainName = $domainFqdn -replace 'DC=', '' -replace ',', '.' + + return $domainName + +} #end function Get-ADDomainNameFromDistinguishedName + +<# + .SYNOPSIS + Add group member from current or different domain. + + .PARAMETER Members + The members to add to the group. These may be in the same + domain as the group or in alternate domains. + + .PARAMETER Parameters + The parameters to pass to the Add-ADGroupMember cmdlet when + adding the members to the group. This should include the group + identity. + + .PARAMETER MembersInMultipleDomains + Setting this switch indicates that there are members from + alternate domains. This triggers the identities of the members + to be looked up in the alternate domain. + + .NOTES + Author original code: Robert D. Biddle (https://github.com/RobBiddle) + Author refactored code: Jan-Hendrik Peters (https://github.com/nyanhp) +#> +function Add-ADCommonGroupMember +{ + [CmdletBinding()] + param + ( + [Parameter()] + [System.String[]] + $Members, + + [Parameter()] + [hashtable] + $Parameters, + + [Parameter()] + [System.Management.Automation.SwitchParameter] + $MembersInMultipleDomains + ) + + Assert-Module -ModuleName ActiveDirectory + + if ($Members) + { + if ($MembersInMultipleDomains.IsPresent) + { + foreach ($member in $Members) + { + $memberDomain = Get-ADDomainNameFromDistinguishedName -DistinguishedName $member + + if (-not $memberDomain) + { + $errorMessage = $script:localizedData.EmptyDomainError -f $member, $Parameters.Identity + New-InvalidOperationException -Message $errorMessage + } + + Write-Verbose -Message ($script:localizedData.AddingGroupMember -f $member, $memberDomain, $Parameters.Identity) + + $commonParameters = @{ + Identity = $member + Server = $memberDomain + ErrorAction = 'Stop' + } + + $activeDirectoryObject = Get-ADObject @commonParameters -Properties @('ObjectClass') + + $memberObjectClass = $activeDirectoryObject.ObjectClass + + if ($memberObjectClass -eq 'computer') + { + $memberObject = Get-ADComputer @commonParameters + } + elseif ($memberObjectClass -eq 'group') + { + $memberObject = Get-ADGroup @commonParameters + } + elseif ($memberObjectClass -eq 'user') + { + $memberObject = Get-ADUser @commonParameters + } + elseif ($memberObjectClass -eq 'msDS-ManagedServiceAccount') + { + $memberObject = Get-ADServiceAccount @commonParameters + } + elseif ($memberObjectClass -eq 'msDS-GroupManagedServiceAccount') + { + $memberObject = Get-ADServiceAccount @commonParameters + } + + Add-ADGroupMember @Parameters -Members $memberObject -ErrorAction 'Stop' + } + } + else + { + Add-ADGroupMember @Parameters -Members $Members -ErrorAction 'Stop' + } + } +} + +<# + .SYNOPSIS + Returns the domain controller object if the node is a domain controller, + otherwise it return $null. + + .PARAMETER DomainName + The name of the domain that should contain the domain controller. + + .PARAMETER ComputerName + The name of the node to return the domain controller object for. + Defaults to $env:COMPUTERNAME. + + .OUTPUTS + If the domain controller is not found, an empty object ($null) is returned. + + .NOTES + Throws an exception of Microsoft.ActiveDirectory.Management.ADServerDownException + if the domain cannot be contacted. +#> +function Get-DomainControllerObject +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $DomainName, + + [Parameter()] + [System.String] + $ComputerName = $env:COMPUTERNAME, + + [Parameter()] + [System.Management.Automation.PSCredential] + $Credential + ) + + <# + It is not possible to use `-ErrorAction 'SilentlyContinue` on the + cmdlet Get-ADDomainController, it will throw an error regardless. + #> + try + { + $getADDomainControllerParameters = @{ + Filter = 'Name -eq "{0}"' -f $ComputerName + Server = $DomainName + } + + if ($PSBoundParameters.ContainsKey('Credential')) + { + $getADDomainControllerParameters['Credential'] = $Credential + } + + $domainControllerObject = Get-ADDomainController @getADDomainControllerParameters + + if (-not $domainControllerObject -and (Test-IsDomainController) -eq $true) + { + $errorMessage = $script:localizedData.WasExpectingDomainController + New-InvalidResultException -Message $errorMessage + } + } + catch + { + $errorMessage = $script:localizedData.FailedEvaluatingDomainController + New-InvalidOperationException -Message $errorMessage -ErrorRecord $_ + } + + return $domainControllerObject +} + +<# + .SYNOPSIS + Returns $true if the node is a domain controller, otherwise it returns + $false +#> +function Test-IsDomainController +{ + [CmdletBinding()] + param + ( + ) + + $operatingSystemInformation = Get-CimInstance -ClassName 'Win32_OperatingSystem' + + return $operatingSystemInformation.ProductType -eq 2 +} + +<# + .SYNOPSIS + Converts a hashtable containing the parameter to property mappings to + an array of properties that can be used to call cmdlets that supports the + parameter Properties. + + .PARAMETER PropertyMap + The property map, as an array of hashtables, to convert to a properties array. + + .EXAMPLE + $computerObjectPropertyMap = @( + @{ + ParameterName = 'ComputerName' + PropertyName = 'cn' + }, + @{ + ParameterName = 'Location' + } + ) + + $computerObjectProperties = Convert-PropertyMapToObjectProperties $computerObjectPropertyMap + $getADComputerResult = Get-ADComputer -Identity 'APP01' -Properties $computerObjectProperties +#> +function Convert-PropertyMapToObjectProperties +{ + [CmdletBinding()] + [OutputType([System.Array])] + param + ( + [Parameter(Mandatory = $true)] + [System.Array] + $PropertyMap + ) + + $objectProperties = @() + + # Create an array of the AD property names to retrieve from the property map + foreach ($property in $PropertyMap) + { + if ($property -isnot [System.Collections.Hashtable]) + { + $errorMessage = $script:localizedData.PropertyMapArrayIsWrongType + New-InvalidOperationException -Message $errorMessage + } + + if ($property.ContainsKey('PropertyName')) + { + $objectProperties += @($property.PropertyName) + } + else + { + $objectProperties += $property.ParameterName + } + } + + return $objectProperties +} + +<# + .SYNOPSIS + This function is used to compare current and desired values for any DSC + resource, and return a hashtable with the result from the comparison. + + .PARAMETER CurrentValues + The current values that should be compared to to desired values. Normally + the values returned from Get-TargetResource. + + .PARAMETER DesiredValues + The values set in the configuration and is provided in the call to the + functions *-TargetResource, and that will be compared against current + values. Normally set to $PSBoundParameters. + + .PARAMETER Properties + An array of property names, from the keys provided in DesiredValues, that + will be compared. If this parameter is left out, all the keys in the + DesiredValues will be compared. +#> +function Compare-ResourcePropertyState +{ + [CmdletBinding()] + [OutputType([System.Collections.Hashtable[]])] + param + ( + [Parameter(Mandatory = $true)] + [System.Collections.Hashtable] + $CurrentValues, + + [Parameter(Mandatory = $true)] + [System.Collections.Hashtable] + $DesiredValues, + + [Parameter()] + [System.String[]] + $Properties, + + [Parameter()] + [System.String[]] + $IgnoreProperties + ) + + if ($PSBoundParameters.ContainsKey('Properties')) + { + # Filter out the parameters (keys) not specified in Properties + $desiredValuesToRemove = $DesiredValues.Keys | + Where-Object -FilterScript { + $_ -notin $Properties + } + + $desiredValuesToRemove | + ForEach-Object -Process { + $DesiredValues.Remove($_) + } + } + else + { + <# + Remove any common parameters that might be part of DesiredValues, + if it $PSBoundParameters was used to pass the desired values. + #> + $commonParametersToRemove = $DesiredValues.Keys | + Where-Object -FilterScript { + $_ -in [System.Management.Automation.PSCmdlet]::CommonParameters ` + -or $_ -in [System.Management.Automation.PSCmdlet]::OptionalCommonParameters + } + + $commonParametersToRemove | + ForEach-Object -Process { + $DesiredValues.Remove($_) + } + } + + # Remove any properties that should be ignored. + if ($PSBoundParameters.ContainsKey('IgnoreProperties')) + { + $IgnoreProperties | + ForEach-Object -Process { + if ($DesiredValues.ContainsKey($_)) + { + $DesiredValues.Remove($_) + } + } + } + + $compareTargetResourceStateReturnValue = @() + + foreach ($parameterName in $DesiredValues.Keys) + { + Write-Verbose -Message ($script:localizedData.EvaluatePropertyState -f $parameterName) -Verbose + + $parameterState = @{ + ParameterName = $parameterName + Expected = $DesiredValues.$parameterName + Actual = $CurrentValues.$parameterName + } + + # Check if the parameter is in compliance. + $isPropertyInDesiredState = Test-DscPropertyState -Values @{ + CurrentValue = $CurrentValues.$parameterName + DesiredValue = $DesiredValues.$parameterName + } + + if ($isPropertyInDesiredState) + { + Write-Verbose -Message ($script:localizedData.PropertyInDesiredState -f $parameterName) -Verbose + + $parameterState['InDesiredState'] = $true + } + else + { + Write-Verbose -Message ($script:localizedData.PropertyNotInDesiredState -f $parameterName) -Verbose + + $parameterState['InDesiredState'] = $false + } + + $compareTargetResourceStateReturnValue += $parameterState + } + + return $compareTargetResourceStateReturnValue +} + +<# + .SYNOPSIS + This function is used to compare the current and the desired value of a + property. + + .PARAMETER Values + This is set to a hash table with the current value (the CurrentValue key) + and desired value (the DesiredValue key). + + .EXAMPLE + Test-DscPropertyState -Values @{ + CurrentValue = 'John' + DesiredValue = 'Alice' + } + .EXAMPLE + Test-DscPropertyState -Values @{ + CurrentValue = 1 + DesiredValue = 2 + } +#> +function Test-DscPropertyState +{ + [CmdletBinding()] + [OutputType([System.Boolean])] + param + ( + [Parameter(Mandatory = $true)] + [System.Collections.Hashtable] + $Values + ) + + if ($null -eq $Values.CurrentValue -and $null -eq $Values.DesiredValue) + { + # Both values are $null so return $true + $returnValue = $true + } + elseif ($null -eq $Values.CurrentValue -or $null -eq $Values.DesiredValue) + { + # Either CurrentValue or DesiredValue are $null so return $false + $returnValue = $false + } + elseif ($Values.DesiredValue.GetType().IsArray -or $Values.CurrentValue.GetType().IsArray) + { + $compareObjectParameters = @{ + ReferenceObject = $Values.CurrentValue + DifferenceObject = $Values.DesiredValue + } + + $arrayCompare = Compare-Object @compareObjectParameters + + if ($null -ne $arrayCompare) + { + Write-Verbose -Message $script:localizedData.ArrayDoesNotMatch -Verbose + + $arrayCompare | + ForEach-Object -Process { + Write-Verbose -Message ($script:localizedData.ArrayValueThatDoesNotMatch -f ` + $_.InputObject, $_.SideIndicator) -Verbose + } + + $returnValue = $false + } + else + { + $returnValue = $true + } + } + elseif ($Values.CurrentValue -ne $Values.DesiredValue) + { + $desiredType = $Values.DesiredValue.GetType() + + $returnValue = $false + + $supportedTypes = @( + 'String' + 'Int32' + 'UInt32' + 'Int16' + 'UInt16' + 'Single' + 'Boolean' + ) + + if ($desiredType.Name -notin $supportedTypes) + { + Write-Warning -Message ($script:localizedData.UnableToCompareType -f $desiredType.Name) + } + else + { + Write-Verbose -Message ( + $script:localizedData.PropertyValueOfTypeDoesNotMatch ` + -f $desiredType.Name, $Values.CurrentValue, $Values.DesiredValue + ) -Verbose + } + } + else + { + $returnValue = $true + } + + return $returnValue +} + +<# + .SYNOPSIS + Asserts if the AD PS Provider has been installed. + + .NOTES + Attempts to force import the ActiveDirectory module if the AD PS Provider has not been installed + and throws an exception if the AD PS Provider cannot be installed. +#> + +function Assert-ADPSProvider +{ + [CmdletBinding()] + param () + + $activeDirectoryPSProvider = Get-PSProvider -PSProvider 'ActiveDirectory' -ErrorAction SilentlyContinue + + if ($null -eq $activeDirectoryPSProvider) + { + Write-Verbose -Message $script:localizedData.AdPsProviderNotFound -Verbose + Import-Module -Name 'ActiveDirectory' -Force + try + { + $activeDirectoryPSProvider = Get-PSProvider -PSProvider 'ActiveDirectory' + } + catch + { + $errorMessage = $script:localizedData.AdPsProviderInstallFailureError + New-InvalidOperationException -Message $errorMessage -ErrorRecord $_ + } + } +} + +<# + .SYNOPSIS + Asserts if the AD PS Drive has been created, and creates one if not. + + .PARAMETER Root + Specifies the AD path to which the drive is mapped. + + .NOTES + Throws an exception if the PS Drive cannot be created. +#> +function Assert-ADPSDrive +{ + [CmdletBinding()] + param + ( + [Parameter()] + [System.String] + $Root = '//RootDSE/' + ) + + Assert-Module -ModuleName 'ActiveDirectory' + + Assert-ADPSProvider + + $activeDirectoryPSDrive = Get-PSDrive -Name AD -ErrorAction SilentlyContinue + + if ($null -eq $activeDirectoryPSDrive) + { + Write-Verbose -Message $script:localizedData.CreatingNewADPSDrive -Verbose + + try + { + New-PSDrive -Name AD -PSProvider 'ActiveDirectory' -Root $Root -Scope Global -ErrorAction 'Stop' | + Out-Null + } + catch + { + $errorMessage = $script:localizedData.CreatingNewADPSDriveError + New-InvalidOperationException -Message $errorMessage -ErrorRecord $_ + } + } +} + +<# + .SYNOPSIS + This is a wrapper for Set-ADComputer. + + .PARAMETER Parameters + A hash table containing all parameters that will be passed trough to + Set-ADComputer. + + .NOTES + This is needed because of how Pester is unable to handle mocking the + cmdlet Set-ADComputer. Therefor there are no unit test for this function. +#> +function Set-DscADComputer +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [System.Collections.Hashtable] + $Parameters + ) + + Set-ADComputer @Parameters | Out-Null +} + +<# + .SYNOPSIS + This returns a new MSFT_Credential CIM instance credential object to be + used when returning credential objects from Get-TargetResource. + This returns a credential object without the password. + + .PARAMETER Credential + The PSCredential object to return as a MSFT_Credential CIM instance + credential object. + + .NOTES + When returning a PSCredential object from Get-TargetResource, the + credential object does not contain the username. The object is empty. + + Password UserName PSComputerName + -------- -------- -------------- + localhost + + When the MSFT_Credential CIM instance credential object is returned by + the Get-TargetResource then the credential object contains the values + provided in the object. + + Password UserName PSComputerName + -------- -------- -------------- + COMPANY\TestAccount localhost +#> +function New-CimCredentialInstance +{ + [CmdletBinding()] + [OutputType([Microsoft.Management.Infrastructure.CimInstance])] + param + ( + [Parameter(Mandatory = $true)] + [System.Management.Automation.PSCredential] + $Credential + ) + + $newCimInstanceParameters = @{ + ClassName = 'MSFT_Credential' + ClientOnly = $true + Namespace = 'root/microsoft/windows/desiredstateconfiguration' + Property = @{ + UserName = [System.String] $Credential.UserName + Password = [System.String] $null + } + } + + return New-CimInstance @newCimInstanceParameters +} + +<# + .SYNOPSIS + This loads the assembly type, optionally after a check + if the type is missing in the PowerShell session. + + .PARAMETER AssemblyName + The assembly to load into the PowerShell session. + + .PARAMETER TypeName + An optional parameter to check if the type exist, if it exist then the + assembly is not loaded again. +#> +function Add-TypeAssembly +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $AssemblyName, + + [Parameter()] + [System.String] + $TypeName + ) + + if ($PSBoundParameters.ContainsKey('TypeName')) + { + if ($TypeName -as [Type]) + { + Write-Verbose -Message ($script:localizedData.TypeAlreadyExistInSession -f $TypeName) -Verbose + + # The type already exists so no need to load the type again. + return + } + else + { + Write-Verbose -Message ($script:localizedData.TypeDoesNotExistInSession -f $TypeName) -Verbose + } + } + + try + { + Write-Verbose -Message ($script:localizedData.AddingAssemblyToSession -f $AssemblyName) -Verbose + + Add-Type -AssemblyName $AssemblyName + } + catch + { + $missingRoleMessage = $script:localizedData.CouldNotLoadAssembly -f $AssemblyName + New-ObjectNotFoundException -Message $missingRoleMessage -ErrorRecord $_ + } +} + +<# + .SYNOPSIS + This returns a new object of the type System.DirectoryServices.ActiveDirectory.DirectoryContext. + + .PARAMETER DirectoryContextType + The context type of the object to return. Valid values are 'Domain', 'Forest', + 'ApplicationPartition', 'ConfigurationSet' or 'DirectoryServer'. + + .PARAMETER Name + An optional parameter for the target of the directory context. + For the correct format for this parameter depending on context type, see + the article https://docs.microsoft.com/en-us/dotnet/api/system.directoryservices.activedirectory.directorycontext?view=netframework-4.8 +#> +function Get-ADDirectoryContext +{ + [CmdletBinding()] + [OutputType([System.DirectoryServices.ActiveDirectory.DirectoryContext])] + param + ( + [Parameter(Mandatory = $true)] + [ValidateSet('Domain', 'Forest', 'ApplicationPartition', 'ConfigurationSet', 'DirectoryServer')] + [System.String] + $DirectoryContextType, + + [Parameter()] + [System.String] + $Name, + + [Parameter()] + [System.Management.Automation.PSCredential] + $Credential + ) + + $typeName = 'System.DirectoryServices.ActiveDirectory.DirectoryContext' + + Add-TypeAssembly -AssemblyName 'System.DirectoryServices' -TypeName $typeName + + Write-Verbose -Message ($script:localizedData.NewDirectoryContext -f $DirectoryContextType) -Verbose + + $newObjectArgumentList = @( + $DirectoryContextType + ) + + if ($PSBoundParameters.ContainsKey('Name')) + { + Write-Verbose -Message ($script:localizedData.NewDirectoryContextTarget -f $Name) -Verbose + + $newObjectArgumentList += @( + $Name + ) + } + + if ($PSBoundParameters.ContainsKey('Credential')) + { + Write-Verbose -Message ($script:localizedData.NewDirectoryContextCredential -f $Credential.UserName) -Verbose + + $newObjectArgumentList += @( + $Credential.UserName + $Credential.GetNetworkCredential().Password + ) + } + else + { + Write-Verbose -Message ($script:localizedData.NewDirectoryContextCredential -f (Get-CurrentUser).Name) -Verbose + } + + $newObjectParameters = @{ + TypeName = $typeName + ArgumentList = $newObjectArgumentList + } + + return New-Object @newObjectParameters +} + +<# + .SYNOPSIS + Gets the specified Active Directory domain + + .PARAMETER DomainName + Specifies the fully qualified domain name to wait for.. + + .PARAMETER DomainName + Specifies the site in the domain where to look for a domain controller. + + .PARAMETER Credential + Specifies the credentials that are used when accessing the domain, + or uses the current user if not specified. + + .PARAMETER WaitForValidCredentials + Specifies if authentication exceptions should be ignored. + + .NOTES + This function is designed so that it can run on any computer without + having the ActiveDirectory module installed. +#> +function Find-DomainController +{ + [OutputType([System.DirectoryServices.ActiveDirectory.DomainController])] + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $DomainName, + + [Parameter()] + [System.String] + $SiteName, + + [Parameter()] + [System.Management.Automation.PSCredential] + $Credential, + + [Parameter()] + [System.Management.Automation.SwitchParameter] + $WaitForValidCredentials + ) + + if ($PSBoundParameters.ContainsKey('SiteName')) + { + Write-Verbose -Message ($script:localizedData.SearchingForDomainControllerInSite -f $SiteName, $DomainName) -Verbose + } + else + { + Write-Verbose -Message ($script:localizedData.SearchingForDomainController -f $DomainName) -Verbose + } + + if ($PSBoundParameters.ContainsKey('Credential')) + { + $adDirectoryContext = Get-ADDirectoryContext -DirectoryContextType 'Domain' -Name $DomainName -Credential $Credential + } + else + { + $adDirectoryContext = Get-ADDirectoryContext -DirectoryContextType 'Domain' -Name $DomainName + } + + $domainControllerObject = $null + + try + { + if ($PSBoundParameters.ContainsKey('SiteName')) + { + $domainControllerObject = Find-DomainControllerFindOneInSiteWrapper -DirectoryContext $adDirectoryContext -SiteName $SiteName + + Write-Verbose -Message ($script:localizedData.FoundDomainControllerInSite -f $SiteName, $DomainName) -Verbose + } + else + { + $domainControllerObject = Find-DomainControllerFindOneWrapper -DirectoryContext $adDirectoryContext + + Write-Verbose -Message ($script:localizedData.FoundDomainController -f $DomainName) -Verbose + } + } + catch [System.DirectoryServices.ActiveDirectory.ActiveDirectoryObjectNotFoundException] + { + Write-Verbose -Message ($script:localizedData.FailedToFindDomainController -f $DomainName) -Verbose + } + catch [System.Management.Automation.MethodInvocationException] + { + $isTypeNameToSuppress = $_.Exception.InnerException -is [System.Security.Authentication.AuthenticationException] + + if ($WaitForValidCredentials.IsPresent -and $isTypeNameToSuppress) + { + Write-Warning -Message ( + $script:localizedData.IgnoreCredentialError -f $_.FullyQualifiedErrorId, $_.Exception.Message + ) + } + elseif ($_.Exception.InnerException -is [System.DirectoryServices.ActiveDirectory.ActiveDirectoryObjectNotFoundException]) + { + Write-Verbose -Message ($script:localizedData.FailedToFindDomainController -f $DomainName) -Verbose + } + else + { + throw $_ + } + } + catch + { + throw $_ + } + + return $domainControllerObject +} + +<# + .SYNOPSIS + This returns a new object of the type System.DirectoryServices.ActiveDirectory.Forest + which is a class that represents an Active Directory Domain Services forest. + + .PARAMETER DirectoryContext + The Active Directory context from which the forest object is returned. + Calling the Get-ADDirectoryContext gets a value that can be provided in + this parameter. + + .NOTES + This is a wrapper to enable unit testing of the function Find-DomainController. + It is not possible to make a stub class to mock these, since these classes + are loaded into the PowerShell session when it starts. + + This function is not exported. +#> +function Find-DomainControllerFindOneWrapper +{ + [CmdletBinding()] + [OutputType([System.DirectoryServices.ActiveDirectory.DomainController])] + param + ( + [Parameter(Mandatory = $true)] + [System.DirectoryServices.ActiveDirectory.DirectoryContext] + $DirectoryContext + ) + + return [System.DirectoryServices.ActiveDirectory.DomainController]::FindOne($DirectoryContext) +} + +<# + .SYNOPSIS + This returns a new object of the type System.DirectoryServices.ActiveDirectory.Forest + which is a class that represents an Active Directory Domain Services forest. + + .PARAMETER DirectoryContext + The Active Directory context from which the forest object is returned. + Calling the Get-ADDirectoryContext gets a value that can be provided in + this parameter. + + .PARAMETER SiteName + Specifies the site in the domain where to look for a domain controller. + + .NOTES + This is a wrapper to enable unit testing of the function Find-DomainController. + It is not possible to make a stub class to mock these, since these classes + are loaded into the PowerShell session when it starts. + + This function is not exported. +#> +function Find-DomainControllerFindOneInSiteWrapper +{ + [CmdletBinding()] + [OutputType([System.DirectoryServices.ActiveDirectory.DomainController])] + param + ( + [Parameter(Mandatory = $true)] + [System.DirectoryServices.ActiveDirectory.DirectoryContext] + $DirectoryContext, + + [Parameter(Mandatory = $true)] + [System.String] + $SiteName + ) + + return [System.DirectoryServices.ActiveDirectory.DomainController]::FindOne($DirectoryContext, $SiteName) +} + +<# + .SYNOPSIS + This is used to get the current user context when the resource + script runs. + + .NOTES + We are putting this in a function so we can mock it with pester +#> +function Get-CurrentUser +{ + [CmdletBinding()] + [OutputType([System.String])] + param () + + return [System.Security.Principal.WindowsIdentity]::GetCurrent() +} + +<# + .SYNOPSIS + Test the validity of a user's password. + + .PARAMETER DomainName + Name of the domain where the user account is located (only used if + password is managed). + + .PARAMETER UserName + Specifies the Security Account Manager (SAM) account name of the user + (ldapDisplayName 'sAMAccountName'). + + .PARAMETER Password + Specifies a new password value for the account. + + .PARAMETER Credential + Specifies the user account credentials to use to perform this task. + + .PARAMETER PasswordAuthentication + Specifies the authentication context type used when testing passwords. + Default value is 'Default'. +#> +function Test-Password +{ + [CmdletBinding()] + [OutputType([System.Boolean])] + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $DomainName, + + [Parameter(Mandatory = $true)] + [System.String] + $UserName, + + [Parameter(Mandatory = $true)] + [System.Management.Automation.PSCredential] + [System.Management.Automation.CredentialAttribute()] + $Password, + + [Parameter()] + [ValidateNotNull()] + [System.Management.Automation.PSCredential] + [System.Management.Automation.CredentialAttribute()] + $Credential, + + # Specifies the authentication context type when testing user passwords #61 + [Parameter(Mandatory = $true)] + [ValidateSet('Default', 'Negotiate')] + [System.String] + $PasswordAuthentication + ) + + Write-Verbose -Message ($script:localizedData.CreatingADDomainConnection -f $DomainName) + + $principalContextTypeName = 'System.DirectoryServices.AccountManagement.PrincipalContext' + + Add-TypeAssembly -AssemblyName 'System.DirectoryServices.AccountManagement' -TypeName $principalContextTypeName + + <# + If the domain name contains a distinguished name, set it to the fully + qualified domain name (FQDN) instead. + If the $DomainName does not contain a distinguished name the function + Get-ADDomainNameFromDistinguishedName returns $null. + #> + $ADDomainName = Get-ADDomainNameFromDistinguishedName -DistinguishedName $DomainName + if ($ADDomainName) + { + $DomainName = $ADDomainName + } + + if ($Credential) + { + Write-Verbose -Message ( + $script:localizedData.TestPasswordUsingImpersonation -f $Credential.UserName, $UserName + ) + + $principalContext = New-Object -TypeName $principalContextTypeName -ArgumentList @( + [System.DirectoryServices.AccountManagement.ContextType]::Domain, + $DomainName, + $Credential.UserName, + $Credential.GetNetworkCredential().Password + ) + } + else + { + $principalContext = New-Object -TypeName $principalContextTypeName -ArgumentList @( + [System.DirectoryServices.AccountManagement.ContextType]::Domain, + $DomainName, + $null, + $null + ) + } + + Write-Verbose -Message ($script:localizedData.CheckingADUserPassword -f $UserName) + + $getPrincipalContextCredentials = @{ + UserName = $UserName + Password = $Password + PrincipalContext = $principalContext + PasswordAuthentication = $PasswordAuthentication + } + return Test-PrincipalContextCredentials @getPrincipalContextCredentials +} + +<# + .SYNOPSIS + Test the validity of credentials using a PrincipalContext + + .PARAMETER UserName + Specifies the Security Account Manager (SAM) account name of the user + (ldapDisplayName 'sAMAccountName'). + + .PARAMETER Password + Specifies a new password value for the account. + + .PARAMETER PrincipalContext + Specifies the PrincipalContext object that the credential test will be + performed using. + + .PARAMETER PasswordAuthentication + Specifies the authentication context type used when testing passwords. + Default value is 'Default'. + + .NOTES + We are putting this in a function so we can mock it with pester. +#> +function Test-PrincipalContextCredentials +{ + [CmdletBinding()] + [OutputType([System.Boolean])] + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $UserName, + + [Parameter(Mandatory = $true)] + [System.Management.Automation.PSCredential] + [System.Management.Automation.CredentialAttribute()] + $Password, + + [Parameter(Mandatory = $true)] + [System.DirectoryServices.AccountManagement.PrincipalContext] + $PrincipalContext, + + [Parameter(Mandatory = $true)] + [ValidateSet('Default', 'Negotiate')] + [System.String] + $PasswordAuthentication + ) + + if ($PasswordAuthentication -eq 'Negotiate') + { + $result = $principalContext.ValidateCredentials( + $UserName, + $Password.GetNetworkCredential().Password, + [System.DirectoryServices.AccountManagement.ContextOptions]::Negotiate -bor + [System.DirectoryServices.AccountManagement.ContextOptions]::Signing -bor + [System.DirectoryServices.AccountManagement.ContextOptions]::Sealing + ) + } + else + { + # Use default authentication context + $result = $principalContext.ValidateCredentials( + $UserName, + $Password.GetNetworkCredential().Password + ) + } + + return $result +} + +$script:localizedData = Get-LocalizedData -ResourceName 'ActiveDirectoryDsc.Common' -ScriptRoot $PSScriptRoot diff --git a/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/Modules/ActiveDirectoryDsc.Common/en-US/ActiveDirectoryDsc.Common.strings.psd1 b/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/Modules/ActiveDirectoryDsc.Common/en-US/ActiveDirectoryDsc.Common.strings.psd1 new file mode 100644 index 0000000..2ce9aae --- /dev/null +++ b/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/Modules/ActiveDirectoryDsc.Common/en-US/ActiveDirectoryDsc.Common.strings.psd1 @@ -0,0 +1,52 @@ +# Localized resources for helper module ActiveDirectoryDsc.Common. + +ConvertFrom-StringData @' + WasExpectingDomainController = The operating system product type code returned 2, which indicates that this is domain controller, but was unable to retrieve the domain controller object. (ADCOMMON0001) + FailedEvaluatingDomainController = Could not evaluate if the node is a domain controller. (ADCOMMON0002) + EvaluatePropertyState = Evaluating the state of the property '{0}'. (ADCOMMON0003) + PropertyInDesiredState = The parameter '{0}' is in desired state. (ADCOMMON0004) + PropertyNotInDesiredState = The parameter '{0}' is not in desired state. (ADCOMMON0005) + ArrayDoesNotMatch = One or more values in an array does not match the desired state. Details of the changes are below. (ADCOMMON0006) + ArrayValueThatDoesNotMatch = {0} - {1} (ADCOMMON0007) + PropertyValueOfTypeDoesNotMatch = {0} value does not match. Current value is '{1}', but expected the value '{2}'. (ADCOMMON0008) + UnableToCompareType = Unable to compare the type {0} as it is not handled by the Test-DscPropertyState cmdlet. (ADCOMMON0009) + ModuleNotFoundError = Please ensure that the PowerShell module for role '{0}' is installed. (ADCOMMON0010) + MembersAndIncludeExcludeError = The '{0}' and '{1}' and/or '{2}' parameters conflict. The '{0}' parameter should not be used in any combination with the '{1}' and '{2}' parameters. (ADCOMMON0011) + IncludeAndExcludeConflictError = The member '{0}' is included in both '{1}' and '{2}' parameter values. The same member must not be included in both '{1}' and '{2}' parameter values. (ADCOMMON0014) + IncludeAndExcludeAreEmptyError = The '{0}' and '{1}' parameters are either both null or empty. At least one member must be specified in one of these parameters. (ADCOMMON0015) + RecycleBinRestoreFailed = Failed restoring {0} ({1}) from the recycle bin. (ADCOMMON0017) + EmptyDomainError = No domain name retrieved for group member {0} in group {1}. (ADCOMMON0018) + CheckingMembers = Checking for '{0}' members. (ADCOMMON0019) + MembershipCountMismatch = Membership count is not correct. Expected '{0}' members, actual '{1}' members. (ADCOMMON0020) + MemberNotInDesiredState = Member '{0}' is not in the desired state. (ADCOMMON0021) + MembershipInDesiredState = Membership is in the desired state. (ADCOMMON0023) + MembershipNotDesiredState = Membership is NOT in the desired state. (ADCOMMON0024) + CheckingSite = Checking for site '{0}'. (ADCOMMON0026) + FindInRecycleBin = Finding objects in the recycle bin matching the filter {0}. (ADCOMMON0027) + FoundRestoreTargetInRecycleBin = Found object {0} ({1}) in the recycle bin as {2}. Attempting to restore the object. (ADCOMMON0028) + RecycleBinRestoreSuccessful = Successfully restored object {0} ({1}) from the recycle bin. (ADCOMMON0029) + AddingGroupMember = Adding member '{0}' from domain '{1}' to AD group '{2}'. (ADCOMMON0030) + PropertyMapArrayIsWrongType = An object in the property map array is not of the type [System.Collections.Hashtable]. (ADCOMMON0031) + CreatingNewADPSDrive = Creating new AD: PSDrive. (ADCOMMON0032) + CreatingNewADPSDriveError = Error creating AD: PS Drive. (ADCOMMON0033) + StartProcess = Started the process with id {0} using the path '{1}', and with a timeout value of {2} seconds. (ADCOMMON0041) + CouldNotLoadAssembly = The assembly '{0}' could not be loaded into the PowerShell session. (ADCOMMON0042) + TypeAlreadyExistInSession = The type '{0}' is already loaded into the PowerShell session. (ADCOMMON0043) + TypeDoesNotExistInSession = Missing the type '{0}' from the PowerShell session. (ADCOMMON0044) + AddingAssemblyToSession = Adding the assembly '{0}' into the PowerShell session. (ADCOMMON0045) + NewDirectoryContext = Get a new Active Directory context of the type '{0}'. (ADCOMMON0046) + NewDirectoryContextTarget = The Active Directory context will target '{0}'. (ADCOMMON0047) + NewDirectoryContextCredential = The Active Directory context will be accessed using the '{0}' credentials. (ADCOMMON0048) + FoundDomainController = Found a domain controller in the domain '{0}'. (ADCOMMON0049) + FoundDomainControllerInSite = Found a domain controller in the site '{0}' in the domain '{1}'. (ADCOMMON0050) + FailedToFindDomainController = No domain controller was found in the domain '{0}'. (ADCOMMON0051) + SearchingForDomainController = Searching for a domain controller in the domain '{0}'. (ADCOMMON0052) + SearchingForDomainControllerInSite = Searching for a domain controller in the site '{0}' in the domain '{1}'. (ADCOMMON0053) + IgnoreCredentialError = Suppressing the credential error '{0}' with the message '{1}'. (ADCOMMON0054) + NoObjectFoundInRecycleBin = Did not find a restorable object in the recycle bin. (ADCOMMON0055) + AdPsProviderNotFound = The Active Directory PS Provider was not found, Forcing import of the ActiveDirectory module. (ADCOMMON0056) + AdPsProviderInstallFailureError = Error installing the Active Directory PS Provider. (ADCOMMON0057) + CreatingADDomainConnection = Creating connection to Active Directory domain '{0}'. (ADCOMMON0058) + CheckingADUserPassword = Checking Active Directory user '{0}' password. (ADCOMMON0059) + TestPasswordUsingImpersonation = Impersonating the credentials ''{0}'' to test password for user ''{1}''. (ADCOMMON0060) +'@ diff --git a/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/PSGetModuleInfo.xml b/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/PSGetModuleInfo.xml new file mode 100644 index 0000000..4af3f7b Binary files /dev/null and b/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/PSGetModuleInfo.xml differ diff --git a/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/en-US/about_ActiveDirectoryDsc.help.txt b/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/en-US/about_ActiveDirectoryDsc.help.txt new file mode 100644 index 0000000..61a6ee5 --- /dev/null +++ b/deployment/dsc/azshcihost/ActiveDirectoryDsc/6.0.1/en-US/about_ActiveDirectoryDsc.help.txt @@ -0,0 +1,23 @@ +TOPIC + about_ActiveDirectoryDsc + +SHORT DESCRIPTION + DSC Resources for deployment and configuration of Active Directory Domain Services. + +LONG DESCRIPTION + This module contains DSC Resources for deployment and configuration of Active Directory Domain Services. + +EXAMPLES + PS C:\> Get-DscResource -Module ActiveDirectoryDsc + +NOTE: + Thank you to all those who contributed to this module, by writing code, sharing opinions, and provided feedback. + +TROUBLESHOOTING NOTE: + Look out on the Github repository for issues and new releases. + +SEE ALSO + - https://github.com/dsccommunity/ActiveDirectoryDsc + +KEYWORDS + DSC, DscResource, ActiveDirectory diff --git a/deployment/dsc/azshcihost/AzSHCIHost.ps1 b/deployment/dsc/azshcihost/AzSHCIHost.ps1 new file mode 100644 index 0000000..386b7bb --- /dev/null +++ b/deployment/dsc/azshcihost/AzSHCIHost.ps1 @@ -0,0 +1,1122 @@ +configuration AzSHCIHost +{ + param + ( + [Parameter(Mandatory)] + [string]$DomainName, + [Parameter(Mandatory)] + [string]$environment, + [Parameter(Mandatory)] + [System.Management.Automation.PSCredential]$Admincreds, + [Parameter(Mandatory)] + [string]$enableDHCP, + [Parameter(Mandatory)] + [string]$customRdpPort, + [string]$vSwitchNameHost = "InternalNAT", + [String]$targetDrive = "V", + [String]$sourcePath = "$targetDrive" + ":\Source", + [String]$updatePath = "$sourcePath\Updates", + [String]$ssuPath = "$updatePath\SSU", + [String]$cuPath = "$updatePath\CU", + [String]$targetVMPath = "$targetDrive" + ":\VMs", + [String]$witnessPath = "$targetDrive" + ":\Witness", + [String]$targetADPath = "$targetDrive" + ":\ADDS", + [String]$baseVHDFolderPath = "$targetVMPath\Base", + [String]$azsHCIIsoUri = "https://aka.ms/2CNBagfhSZ8BM7jyEV8I", + [String]$azsHciVhdPath = "$baseVHDFolderPath\AzSHCI.vhdx", + [String]$azsHCIISOLocalPath = "$sourcePath\AzSHCI.iso", + [Int]$azsHostCount = 2, + [Int]$azsHostDataDiskCount = 4, + [Int64]$dataDiskSize = 250GB + ) + + Import-DscResource -ModuleName 'PSDesiredStateConfiguration' + Import-DscResource -ModuleName 'xPSDesiredStateConfiguration' + Import-DscResource -ModuleName 'ComputerManagementDsc' + Import-DscResource -ModuleName 'xHyper-v' + Import-DscResource -ModuleName 'cHyper-v' + Import-DscResource -ModuleName 'StorageDSC' + Import-DscResource -ModuleName 'NetworkingDSC' + Import-DscResource -ModuleName 'xDHCpServer' + Import-DscResource -ModuleName 'DnsServerDsc' + Import-DscResource -ModuleName 'cChoco' + Import-DscResource -ModuleName 'DSCR_Shortcut' + Import-DscResource -ModuleName 'xCredSSP' + Import-DscResource -ModuleName 'ActiveDirectoryDsc' + + $aszhciHostsMofUri = "https://raw.githubusercontent.com/Azure/AzureStackHCI-EvalGuide/main/deployment/helpers/Install-AzsRolesandFeatures.ps1" + $updateAdUri = "https://raw.githubusercontent.com/Azure/AzureStackHCI-EvalGuide/main/deployment/helpers/Update-AD.ps1" + $regHciUri = "https://raw.githubusercontent.com/Azure/AzureStackHCI-EvalGuide/main/deployment/helpers/Register-AzSHCI.ps1" + + if ($enableDHCP -eq "Enabled") { + $dhcpStatus = "Active" + } + else { $dhcpStatus = "Inactive" } + + [System.Management.Automation.PSCredential]$DomainCreds = New-Object System.Management.Automation.PSCredential ("${DomainName}\$($Admincreds.UserName)", $Admincreds.Password) + + $ipConfig = (Get-NetAdapter -Physical | Where-Object { $_.InterfaceDescription -like "*Hyper-V*" } | Get-NetIPConfiguration | Where-Object IPv4DefaultGateway) + $netAdapters = Get-NetAdapter -Name ($ipConfig.InterfaceAlias) | Select-Object -First 1 + $InterfaceAlias = $($netAdapters.Name) + + Node localhost + { + LocalConfigurationManager { + RebootNodeIfNeeded = $true + ActionAfterReboot = 'ContinueConfiguration' + ConfigurationMode = 'ApplyOnly' + } + + #### CREATE STORAGE SPACES V: & VM FOLDER #### + + Script StoragePool { + SetScript = { + New-StoragePool -FriendlyName AzSHCIPool -StorageSubSystemFriendlyName '*storage*' -PhysicalDisks (Get-PhysicalDisk -CanPool $true) + } + TestScript = { + (Get-StoragePool -ErrorAction SilentlyContinue -FriendlyName AzSHCIPool).OperationalStatus -eq 'OK' + } + GetScript = { + @{Ensure = if ((Get-StoragePool -FriendlyName AzSHCIPool).OperationalStatus -eq 'OK') { 'Present' } Else { 'Absent' } } + } + } + Script VirtualDisk { + SetScript = { + $disks = Get-StoragePool -FriendlyName AzSHCIPool -IsPrimordial $False | Get-PhysicalDisk + $diskNum = $disks.Count + New-VirtualDisk -StoragePoolFriendlyName AzSHCIPool -FriendlyName AzSHCIDisk -ResiliencySettingName Simple -NumberOfColumns $diskNum -UseMaximumSize + } + TestScript = { + (Get-VirtualDisk -ErrorAction SilentlyContinue -FriendlyName AzSHCIDisk).OperationalStatus -eq 'OK' + } + GetScript = { + @{Ensure = if ((Get-VirtualDisk -FriendlyName AzSHCIDisk).OperationalStatus -eq 'OK') { 'Present' } Else { 'Absent' } } + } + DependsOn = "[Script]StoragePool" + } + Script FormatDisk { + SetScript = { + $vDisk = Get-VirtualDisk -FriendlyName AzSHCIDisk + if ($vDisk | Get-Disk | Where-Object PartitionStyle -eq 'raw') { + $vDisk | Get-Disk | Initialize-Disk -Passthru | New-Partition -DriveLetter $Using:targetDrive -UseMaximumSize | Format-Volume -NewFileSystemLabel AzSHCIData -AllocationUnitSize 64KB -FileSystem NTFS + } + elseif ($vDisk | Get-Disk | Where-Object PartitionStyle -eq 'GPT') { + $vDisk | Get-Disk | New-Partition -DriveLetter $Using:targetDrive -UseMaximumSize | Format-Volume -NewFileSystemLabel AzSHCIData -AllocationUnitSize 64KB -FileSystem NTFS + } + } + TestScript = { + (Get-Volume -ErrorAction SilentlyContinue -FileSystemLabel AzSHCIData).FileSystem -eq 'NTFS' + } + GetScript = { + @{Ensure = if ((Get-Volume -FileSystemLabel AzSHCIData).FileSystem -eq 'NTFS') { 'Present' } Else { 'Absent' } } + } + DependsOn = "[Script]VirtualDisk" + } + + File "VMfolder" { + Type = 'Directory' + DestinationPath = $targetVMPath + DependsOn = "[Script]FormatDisk" + } + + File "Witnessfolder" { + Type = 'Directory' + DestinationPath = $witnessPath + DependsOn = "[Script]FormatDisk" + } + + if ($environment -eq "AD Domain") { + File "ADfolder" { + Type = 'Directory' + DestinationPath = $targetADPath + DependsOn = "[Script]FormatDisk" + } + } + + File "Source" { + DestinationPath = $sourcePath + Type = 'Directory' + Force = $true + DependsOn = "[Script]FormatDisk" + } + + File "Updates" { + DestinationPath = $updatePath + Type = 'Directory' + Force = $true + DependsOn = "[File]Source" + } + + File "CU" { + DestinationPath = $cuPath + Type = 'Directory' + Force = $true + DependsOn = "[File]Updates" + } + + File "SSU" { + DestinationPath = $ssuPath + Type = 'Directory' + Force = $true + DependsOn = "[File]Updates" + } + + File "VM-base" { + Type = 'Directory' + DestinationPath = $baseVHDFolderPath + DependsOn = "[File]VMfolder" + } + + script "Download DSC Config for AzsHci Hosts" { + GetScript = { + $result = Test-Path -Path "$using:sourcePath\Install-AzsRolesandFeatures.ps1" + return @{ 'Result' = $result } + } + + SetScript = { + Start-BitsTransfer -Source "$using:aszhciHostsMofUri" -Destination "$using:sourcePath\Install-AzsRolesandFeatures.ps1" + } + + TestScript = { + # Create and invoke a scriptblock using the $GetScript automatic variable, which contains a string representation of the GetScript. + $state = [scriptblock]::Create($GetScript).Invoke() + return $state.Result + } + DependsOn = "[File]Source" + } + + script "Download Update-AD" { + GetScript = { + $result = Test-Path -Path "$using:sourcePath\Update-AD.ps1" + return @{ 'Result' = $result } + } + + SetScript = { + Start-BitsTransfer -Source "$using:updateAdUri" -Destination "$using:sourcePath\Update-AD.ps1" + } + + TestScript = { + # Create and invoke a scriptblock using the $GetScript automatic variable, which contains a string representation of the GetScript. + $state = [scriptblock]::Create($GetScript).Invoke() + return $state.Result + } + DependsOn = "[File]Source" + } + + script "Download Register-AzSHCI" { + GetScript = { + $result = Test-Path -Path "$using:sourcePath\Register-AzSHCI.ps1" + return @{ 'Result' = $result } + } + + SetScript = { + Start-BitsTransfer -Source "$using:regHciUri" -Destination "$using:sourcePath\Register-AzSHCI.ps1" + } + + TestScript = { + # Create and invoke a scriptblock using the $GetScript automatic variable, which contains a string representation of the GetScript. + $state = [scriptblock]::Create($GetScript).Invoke() + return $state.Result + } + DependsOn = "[File]Source" + } + + script "Download AzureStack HCI bits" { + GetScript = { + $result = Test-Path -Path $using:azsHCIISOLocalPath + return @{ 'Result' = $result } + } + + SetScript = { + Start-BitsTransfer -Source $using:azsHCIIsoUri -Destination $using:azsHCIISOLocalPath + } + + TestScript = { + # Create and invoke a scriptblock using the $GetScript automatic variable, which contains a string representation of the GetScript. + $state = [scriptblock]::Create($GetScript).Invoke() + return $state.Result + } + DependsOn = "[File]Source" + } + + script "Download AzSHCI SSU" { + GetScript = { + $result = Test-Path -Path "$using:ssuPath\*" -Include "*.msu" + return @{ 'Result' = $result } + } + + SetScript = { + $ssuSearchString = "Servicing Stack Update for Azure Stack HCI, version 20H2 for x64-based Systems" + $ssuID = "Azure Stack HCI" + $ssuUpdate = Get-MSCatalogUpdate -Search $ssuSearchString | Where-Object Products -eq $ssuID | Select-Object -First 1 + $ssuUpdate | Save-MSCatalogUpdate -Destination $using:ssuPath + } + + TestScript = { + # Create and invoke a scriptblock using the $GetScript automatic variable, which contains a string representation of the GetScript. + $state = [scriptblock]::Create($GetScript).Invoke() + return $state.Result + } + DependsOn = "[File]SSU" + } + + script "Download AzSHCI CU" { + GetScript = { + $result = Test-Path -Path "$using:cuPath\*" -Include "*.msu" + return @{ 'Result' = $result } + } + + SetScript = { + $cuSearchString = "Cumulative Update for Azure Stack HCI, version 20H2" + $cuID = "Azure Stack HCI" + $cuUpdate = Get-MSCatalogUpdate -Search $cuSearchString | Where-Object Products -eq $cuID | Where-Object Title -like "*$($cuSearchString)*" | Select-Object -First 1 + $cuUpdate | Save-MSCatalogUpdate -Destination $using:cuPath + } + + TestScript = { + # Create and invoke a scriptblock using the $GetScript automatic variable, which contains a string representation of the GetScript. + $state = [scriptblock]::Create($GetScript).Invoke() + return $state.Result + } + DependsOn = "[File]CU" + } + + #### SET WINDOWS DEFENDER EXCLUSION FOR VM STORAGE #### + + Script defenderExclusions { + SetScript = { + $exclusionPath = "$Using:targetDrive" + ":\" + Add-MpPreference -ExclusionPath "$exclusionPath" + } + TestScript = { + $exclusionPath = "$Using:targetDrive" + ":\" + (Get-MpPreference).ExclusionPath -contains "$exclusionPath" + } + GetScript = { + $exclusionPath = "$Using:targetDrive" + ":\" + @{Ensure = if ((Get-MpPreference).ExclusionPath -contains "$exclusionPath") { 'Present' } Else { 'Absent' } } + } + DependsOn = "[File]VMfolder" + } + + #### REGISTRY & SCHEDULED TASK TWEAKS #### + + Registry "Disable Internet Explorer ESC for Admin" { + Key = "HKLM:\SOFTWARE\Microsoft\Active Setup\Installed Components\{A509B1A7-37EF-4b3f-8CFC-4F3A74704073}" + Ensure = 'Present' + ValueName = "IsInstalled" + ValueData = "0" + ValueType = "Dword" + } + + Registry "Disable Internet Explorer ESC for User" { + Key = "HKLM:\SOFTWARE\Microsoft\Active Setup\Installed Components\{A509B1A8-37EF-4b3f-8CFC-4F3A74704073}" + Ensure = 'Present' + ValueName = "IsInstalled" + ValueData = "0" + ValueType = "Dword" + } + + Registry "Disable Server Manager WAC Prompt" { + Key = "HKLM:\SOFTWARE\Microsoft\ServerManager" + Ensure = 'Present' + ValueName = "DoNotPopWACConsoleAtSMLaunch" + ValueData = "1" + ValueType = "Dword" + } + + Registry "Disable Network Profile Prompt" { + Key = 'HKLM:\System\CurrentControlSet\Control\Network\NewNetworkWindowOff' + Ensure = 'Present' + ValueName = '' + } + + if ($environment -eq "Workgroup") { + Registry "Set Network Private Profile Default" { + Key = 'HKLM:\SOFTWARE\Policies\Microsoft\Windows NT\CurrentVersion\NetworkList\Signatures\010103000F0000F0010000000F0000F0C967A3643C3AD745950DA7859209176EF5B87C875FA20DF21951640E807D7C24' + Ensure = 'Present' + ValueName = "Category" + ValueData = "1" + ValueType = "Dword" + } + + Registry "SetWorkgroupDomain" { + Key = "HKLM:\SYSTEM\CurrentControlSet\Services\Tcpip\Parameters" + Ensure = 'Present' + ValueName = "Domain" + ValueData = "$DomainName" + ValueType = "String" + } + + Registry "SetWorkgroupNVDomain" { + Key = "HKLM:\SYSTEM\CurrentControlSet\Services\Tcpip\Parameters" + Ensure = 'Present' + ValueName = "NV Domain" + ValueData = "$DomainName" + ValueType = "String" + } + + Registry "NewCredSSPKey" { + Key = 'HKLM:\SOFTWARE\Policies\Microsoft\Windows\CredentialsDelegation\AllowFreshCredentialsWhenNTLMOnly' + Ensure = 'Present' + ValueName = '' + } + + Registry "NewCredSSPKey2" { + Key = 'HKLM:\SOFTWARE\Policies\Microsoft\Windows\CredentialsDelegation' + ValueName = 'AllowFreshCredentialsWhenNTLMOnly' + ValueData = '1' + ValueType = "Dword" + DependsOn = "[Registry]NewCredSSPKey" + } + + Registry "NewCredSSPKey3" { + Key = 'HKLM:\SOFTWARE\Policies\Microsoft\Windows\CredentialsDelegation\AllowFreshCredentialsWhenNTLMOnly' + ValueName = '1' + ValueData = "*.$DomainName" + ValueType = "String" + DependsOn = "[Registry]NewCredSSPKey2" + } + } + + ScheduledTask "Disable Server Manager at Startup" { + TaskName = 'ServerManager' + Enable = $false + TaskPath = '\Microsoft\Windows\Server Manager' + } + + #### CUSTOM FIREWALL BASED ON ARM TEMPLATE #### + + if ($customRdpPort -ne "3389") { + + Registry "Set Custom RDP Port" { + Key = 'HKLM:\SYSTEM\CurrentControlSet\Control\Terminal Server\WinStations\RDP-Tcp' + ValueName = "PortNumber" + ValueData = "$customRdpPort" + ValueType = 'Dword' + } + + Firewall AddFirewallRule { + Name = 'CustomRdpRule' + DisplayName = 'Custom Rule for RDP' + Ensure = 'Present' + Enabled = 'True' + Profile = 'Any' + Direction = 'Inbound' + LocalPort = "$customRdpPort" + Protocol = 'TCP' + Description = 'Firewall Rule for Custom RDP Port' + } + } + + #### ENABLE ROLES & FEATURES #### + + WindowsFeature DNS { + Ensure = "Present" + Name = "DNS" + } + + WindowsFeature "Enable Deduplication" { + Ensure = "Present" + Name = "FS-Data-Deduplication" + } + + Script EnableDNSDiags { + SetScript = { + Set-DnsServerDiagnostics -All $true + Write-Verbose -Verbose "Enabling DNS client diagnostics" + } + GetScript = { @{} } + TestScript = { $false } + DependsOn = "[WindowsFeature]DNS" + } + + WindowsFeature DnsTools { + Ensure = "Present" + Name = "RSAT-DNS-Server" + DependsOn = "[WindowsFeature]DNS" + } + + DnsServerAddress "DnsServerAddress for $InterfaceAlias" + { + Address = '127.0.0.1' + InterfaceAlias = $InterfaceAlias + AddressFamily = 'IPv4' + DependsOn = "[WindowsFeature]DNS" + } + + if ($environment -eq "AD Domain") { + + WindowsFeature ADDSInstall { + Ensure = "Present" + Name = "AD-Domain-Services" + DependsOn = "[WindowsFeature]DNS" + } + + WindowsFeature ADDSTools { + Ensure = "Present" + Name = "RSAT-ADDS-Tools" + DependsOn = "[WindowsFeature]ADDSInstall" + } + + WindowsFeature ADAdminCenter { + Ensure = "Present" + Name = "RSAT-AD-AdminCenter" + DependsOn = "[WindowsFeature]ADDSInstall" + } + + ADDomain FirstDS { + DomainName = $DomainName + Credential = $DomainCreds + SafemodeAdministratorPassword = $DomainCreds + DatabasePath = "$targetADPath" + "\NTDS" + LogPath = "$targetADPath" + "\NTDS" + SysvolPath = "$targetADPath" + "\SYSVOL" + DependsOn = @("[File]ADfolder", "[WindowsFeature]ADDSInstall") + } + } + + WindowsFeature "RSAT-Clustering" { + Name = "RSAT-Clustering" + Ensure = "Present" + } + + WindowsFeature "Install DHCPServer" { + Name = 'DHCP' + Ensure = 'Present' + } + + WindowsFeature DHCPTools { + Ensure = "Present" + Name = "RSAT-DHCP" + DependsOn = "[WindowsFeature]Install DHCPServer" + } + + Registry "DHCpConfigComplete" { + Key = 'HKLM:\SOFTWARE\Microsoft\ServerManager\Roles\12' + ValueName = "ConfigurationState" + ValueData = "2" + ValueType = 'Dword' + DependsOn = "[WindowsFeature]DHCPTools" + } + + if ($environment -eq "AD Domain") { + WindowsFeature "Hyper-V" { + Name = "Hyper-V" + Ensure = "Present" + } + } + else { + WindowsFeature "Hyper-V" { + Name = "Hyper-V" + Ensure = "Present" + DependsOn = "[Registry]NewCredSSPKey3" + } + } + + WindowsFeature "RSAT-Hyper-V-Tools" { + Name = "RSAT-Hyper-V-Tools" + Ensure = "Present" + DependsOn = "[WindowsFeature]Hyper-V" + } + + #### HYPER-V vSWITCH CONFIG #### + + xVMHost "hpvHost" + { + IsSingleInstance = 'yes' + EnableEnhancedSessionMode = $true + VirtualHardDiskPath = $targetVMPath + VirtualMachinePath = $targetVMPath + DependsOn = "[WindowsFeature]Hyper-V" + } + + xVMSwitch "$vSwitchNameHost" + { + Name = $vSwitchNameHost + Type = "Internal" + DependsOn = "[WindowsFeature]Hyper-V" + } + + IPAddress "New IP for vEthernet $vSwitchNameHost" + { + InterfaceAlias = "vEthernet `($vSwitchNameHost`)" + AddressFamily = 'IPv4' + IPAddress = '192.168.0.1/16' + DependsOn = "[xVMSwitch]$vSwitchNameHost" + } + + NetIPInterface "Enable IP forwarding on vEthernet $vSwitchNameHost" + { + AddressFamily = 'IPv4' + InterfaceAlias = "vEthernet `($vSwitchNameHost`)" + Forwarding = 'Enabled' + DependsOn = "[IPAddress]New IP for vEthernet $vSwitchNameHost" + } + + NetAdapterRdma "EnableRDMAonvEthernet" + { + Name = "vEthernet `($vSwitchNameHost`)" + Enabled = $true + DependsOn = "[NetIPInterface]Enable IP forwarding on vEthernet $vSwitchNameHost" + } + + DnsServerAddress "DnsServerAddress for vEthernet $vSwitchNameHost" + { + Address = '127.0.0.1' + InterfaceAlias = "vEthernet `($vSwitchNameHost`)" + AddressFamily = 'IPv4' + DependsOn = "[IPAddress]New IP for vEthernet $vSwitchNameHost" + } + + if ($environment -eq "AD Domain") { + + xDhcpServerAuthorization "Authorize DHCP" { + Ensure = 'Present' + DependsOn = @('[WindowsFeature]Install DHCPServer') + DnsName = [System.Net.Dns]::GetHostByName($env:computerName).hostname + IPAddress = '192.168.0.1' + } + } + + if ($environment -eq "Workgroup") { + NetConnectionProfile SetProfile + { + InterfaceAlias = "$InterfaceAlias" + NetworkCategory = 'Private' + } + } + + #### PRIMARY NIC CONFIG #### + + NetAdapterBinding DisableIPv6Host + { + InterfaceAlias = "$InterfaceAlias" + ComponentId = 'ms_tcpip6' + State = 'Disabled' + } + + #### CONFIGURE InternaNAT NIC + + script NAT { + GetScript = { + $nat = "AzSHCINAT" + $result = if (Get-NetNat -Name $nat -ErrorAction SilentlyContinue) { $true } else { $false } + return @{ 'Result' = $result } + } + + SetScript = { + $nat = "AzSHCINAT" + New-NetNat -Name $nat -InternalIPInterfaceAddressPrefix "192.168.0.0/16" + } + + TestScript = { + # Create and invoke a scriptblock using the $GetScript automatic variable, which contains a string representation of the GetScript. + $state = [scriptblock]::Create($GetScript).Invoke() + return $state.Result + } + DependsOn = "[IPAddress]New IP for vEthernet $vSwitchNameHost" + } + + NetAdapterBinding DisableIPv6NAT + { + InterfaceAlias = "vEthernet `($vSwitchNameHost`)" + ComponentId = 'ms_tcpip6' + State = 'Disabled' + DependsOn = "[Script]NAT" + } + + #### CONFIGURE DHCP SERVER + + xDhcpServerScope "AzSHCIDhcpScope" { + Ensure = 'Present' + IPStartRange = '192.168.0.10' + IPEndRange = '192.168.0.149' + ScopeId = '192.168.0.0' + Name = 'AzSHCI Lab Range' + SubnetMask = '255.255.0.0' + LeaseDuration = '01.00:00:00' + State = "$dhcpStatus" + AddressFamily = 'IPv4' + DependsOn = @("[WindowsFeature]Install DHCPServer", "[IPAddress]New IP for vEthernet $vSwitchNameHost") + } + + xDhcpServerOption "AzSHCIDhcpServerOption" { + Ensure = 'Present' + ScopeID = '192.168.0.0' + DnsDomain = "$DomainName" + DnsServerIPAddress = '192.168.0.1' + AddressFamily = 'IPv4' + Router = '192.168.0.1' + DependsOn = "[xDhcpServerScope]AzSHCIDhcpScope" + } + + if ($environment -eq "Workgroup") { + + DnsServerPrimaryZone SetPrimaryDNSZone { + Name = "$DomainName" + Ensure = 'Present' + DependsOn = "[script]NAT" + ZoneFile = "$DomainName" + ".dns" + DynamicUpdate = 'NonSecureAndSecure' + } + + DnsServerPrimaryZone SetReverseLookupZone { + Name = '0.168.192.in-addr.arpa' + Ensure = 'Present' + DependsOn = "[DnsServerPrimaryZone]SetPrimaryDNSZone" + ZoneFile = '0.168.192.in-addr.arpa.dns' + DynamicUpdate = 'NonSecureAndSecure' + } + } + elseif ($environment -eq "AD Domain") { + + DnsServerPrimaryZone SetReverseLookupZone { + Name = '0.168.192.in-addr.arpa' + Ensure = 'Present' + DependsOn = "[ADDomain]FirstDS" + ZoneFile = '0.168.192.in-addr.arpa.dns' + } + } + + #### FINALIZE DHCP + + Script SetDHCPDNSSetting { + SetScript = { + Set-DhcpServerv4DnsSetting -DynamicUpdates "Always" -DeleteDnsRRonLeaseExpiry $True -UpdateDnsRRForOlderClients $True -DisableDnsPtrRRUpdate $false + Write-Verbose -Verbose "Setting server level DNS dynamic update configuration settings" + } + GetScript = { @{} + } + TestScript = { $false } + DependsOn = "[xDhcpServerOption]AzSHCIDhcpServerOption" + } + + if ($environment -eq "Workgroup") { + + DnsConnectionSuffix AddSpecificSuffixHostNic + { + InterfaceAlias = "$InterfaceAlias" + ConnectionSpecificSuffix = "$DomainName" + DependsOn = "[DnsServerPrimaryZone]SetPrimaryDNSZone" + } + + DnsConnectionSuffix AddSpecificSuffixNATNic + { + InterfaceAlias = "vEthernet `($vSwitchNameHost`)" + ConnectionSpecificSuffix = "$DomainName" + DependsOn = "[DnsServerPrimaryZone]SetPrimaryDNSZone" + } + + #### CONFIGURE CREDSSP & WinRM + + xCredSSP Server { + Ensure = "Present" + Role = "Server" + DependsOn = "[DnsConnectionSuffix]AddSpecificSuffixNATNic" + SuppressReboot = $true + } + xCredSSP Client { + Ensure = "Present" + Role = "Client" + DelegateComputers = "$env:COMPUTERNAME" + ".$DomainName" + DependsOn = "[xCredSSP]Server" + SuppressReboot = $true + } + + #### CONFIGURE WinRM + + Script ConfigureWinRM { + SetScript = { + Set-Item WSMan:\localhost\Client\TrustedHosts "*.$Using:DomainName" -Force + } + TestScript = { + (Get-Item WSMan:\localhost\Client\TrustedHosts).Value -contains "*.$Using:DomainName" + } + GetScript = { + @{Ensure = if ((Get-Item WSMan:\localhost\Client\TrustedHosts).Value -contains "*.$Using:DomainName") { 'Present' } Else { 'Absent' } } + } + DependsOn = "[xCredSSP]Client" + } + } + + #### Start AzSHCI Node Creation #### + + script "prepareVHDX" { + GetScript = { + $result = Test-Path -Path $using:azsHciVhdPath + return @{ 'Result' = $result } + } + + SetScript = { + # Create Azure Stack HCI Host Image from ISO + + $scratchPath = "$using:targetVMPath\Scratch" + New-Item -ItemType Directory -Path "$scratchPath" -Force | Out-Null + + Convert-WindowsImage -SourcePath $using:azsHCIISOLocalPath -SizeBytes 100GB -VHDPath $using:azsHciVhdPath ` + -VHDFormat VHDX -VHDType Dynamic -VHDPartitionStyle GPT -Package $using:ssuPath -TempDirectory $using:targetVMPath -Verbose + + <# + Convert-Wim2Vhd -DiskLayout UEFI -SourcePath $using:azsHCIISOLocalPath -Path $using:azsHciVhdPath ` + -Package $using:ssuPath -Size 100GB -Dynamic -Index 1 -ErrorAction SilentlyContinue + #> + + # Need to wait for disk to fully unmount + While ((Get-Disk).Count -gt 2) { + Start-Sleep -Seconds 5 + } + + Start-Sleep -Seconds 5 + + Mount-VHD -Path $using:azsHciVhdPath -Passthru -ErrorAction Stop -Verbose + Start-Sleep -Seconds 2 + + $disks = Get-CimInstance -ClassName Win32_DiskDrive | Where-Object Caption -eq "Microsoft Virtual Disk" + foreach ($disk in $disks) { + $vols = Get-CimAssociatedInstance -CimInstance $disk -ResultClassName Win32_DiskPartition + foreach ($vol in $vols) { + $updatedrive = Get-CimAssociatedInstance -CimInstance $vol -ResultClassName Win32_LogicalDisk | + Where-Object VolumeName -ne 'System Reserved' + } + } + $updatepath = $updatedrive.DeviceID + "\" + + $updates = get-childitem -path $using:cuPath -Recurse | Where-Object { ($_.extension -eq ".msu") -or ($_.extension -eq ".cab") } | Select-Object fullname + foreach ($update in $updates) { + write-debug $update.fullname + $command = "dism /image:" + $updatepath + " /add-package /packagepath:'" + $update.fullname + "'" + write-debug $command + Invoke-Expression $command + } + + $command = "dism /image:" + $updatepath + " /Cleanup-Image /spsuperseded" + Invoke-Expression $command + + Dismount-VHD -path $using:azsHciVhdPath -confirm:$false + + Start-Sleep -Seconds 5 + + # Enable Hyper-V role on the Azure Stack HCI Host Image + Install-WindowsFeature -Vhd $using:azsHciVhdPath -Name Hyper-V + } + + TestScript = { + # Create and invoke a scriptblock using the $GetScript automatic variable, which contains a string representation of the GetScript. + $state = [scriptblock]::Create($GetScript).Invoke() + return $state.Result + } + DependsOn = "[file]VM-Base", "[script]Download AzureStack HCI bits", "[script]Download AzSHCI SSU", "[script]Download AzSHCI CU" + } + + for ($i = 1; $i -lt $azsHostCount + 1; $i++) { + $suffix = '{0:D2}' -f $i + $vmname = $("AZSHCINODE" + $suffix) + $memory = 24gb + + file "VM-Folder-$vmname" { + Ensure = 'Present' + DestinationPath = "$targetVMPath\$vmname" + Type = 'Directory' + DependsOn = "[File]VMfolder" + } + + xVhd "NewOSDisk-$vmname" + { + Ensure = 'Present' + Name = "$vmname-OSDisk.vhdx" + Path = "$targetVMPath\$vmname" + Generation = 'vhdx' + ParentPath = $azsHciVhdPath + Type = 'Differencing' + DependsOn = "[xVMSwitch]$vSwitchNameHost", "[script]prepareVHDX", "[file]VM-Folder-$vmname" + } + + xVMHyperV "VM-$vmname" + { + Ensure = 'Present' + Name = $vmname + VhdPath = "$targetVMPath\$vmname\$vmname-OSDisk.vhdx" + Path = $targetVMPath + Generation = 2 + StartupMemory = $memory + ProcessorCount = 8 + DependsOn = "[xVhd]NewOSDisk-$vmname" + } + + xVMProcessor "Enable NestedVirtualization-$vmname" + { + VMName = $vmname + ExposeVirtualizationExtensions = $true + DependsOn = "[xVMHyperV]VM-$vmname" + } + + script "remove default Network Adapter on VM-$vmname" { + GetScript = { + $VMNetworkAdapter = Get-VMNetworkAdapter -VMName $using:vmname -Name 'Network Adapter' -ErrorAction SilentlyContinue + $result = if ($VMNetworkAdapter) { $false } else { $true } + return @{ + VMName = $VMNetworkAdapter.VMName + Name = $VMNetworkAdapter.Name + Result = $result + } + } + + SetScript = { + $state = [scriptblock]::Create($GetScript).Invoke() + Remove-VMNetworkAdapter -VMName $state.VMName -Name $state.Name + } + + TestScript = { + # Create and invoke a scriptblock using the $GetScript automatic variable, which contains a string representation of the GetScript. + $state = [scriptblock]::Create($GetScript).Invoke() + return $state.Result + } + DependsOn = "[xVMHyperV]VM-$vmname" + } + + for ($k = 1; $k -le 1; $k++) { + $ipAddress = $('192.168.0.' + ($i + 1)) + $mgmtNicName = "$vmname-Management$k" + xVMNetworkAdapter "New Network Adapter $mgmtNicName $vmname DHCP" + { + Id = $mgmtNicName + Name = $mgmtNicName + SwitchName = $vSwitchNameHost + VMName = $vmname + NetworkSetting = xNetworkSettings { + IpAddress = $ipAddress + Subnet = "255.255.0.0" + DefaultGateway = "192.168.0.1" + DnsServer = "192.168.0.1" + } + Ensure = 'Present' + DependsOn = "[xVMHyperV]VM-$vmname" + } + + cVMNetworkAdapterSettings "Enable $vmname $mgmtNicName Mac address spoofing and Teaming" + { + Id = $mgmtNicName + Name = $mgmtNicName + SwitchName = $vSwitchNameHost + VMName = $vmname + AllowTeaming = 'on' + MacAddressSpoofing = 'on' + DependsOn = "[xVMNetworkAdapter]New Network Adapter $mgmtNicName $vmname DHCP" + } + } + + for ($l = 1; $l -le 3; $l++) { + $ipAddress = $('10.10.1' + $l + '.' + $i) + $nicName = "$vmname-ConvergedNic$l" + + xVMNetworkAdapter "New Network Adapter Converged $vmname $nicName $ipAddress" + { + Id = $nicName + Name = $nicName + SwitchName = $vSwitchNameHost + VMName = $vmname + NetworkSetting = xNetworkSettings { + IpAddress = $ipAddress + Subnet = "255.255.255.0" + } + Ensure = 'Present' + DependsOn = "[xVMHyperV]VM-$vmname" + } + + cVMNetworkAdapterSettings "Enable $vmname $nicName Mac address spoofing and Teaming" + { + Id = $nicName + Name = $nicName + SwitchName = $vSwitchNameHost + VMName = $vmname + AllowTeaming = 'on' + MacAddressSpoofing = 'on' + DependsOn = "[xVMNetworkAdapter]New Network Adapter Converged $vmname $nicName $ipAddress" + } + } + + for ($j = 1; $j -lt $azsHostDataDiskCount + 1 ; $j++) { + xvhd "$vmname-DataDisk$j" + { + Ensure = 'Present' + Name = "$vmname-DataDisk$j.vhdx" + Path = "$targetVMPath\$vmname" + Generation = 'vhdx' + Type = 'Dynamic' + MaximumSizeBytes = $dataDiskSize + DependsOn = "[xVMHyperV]VM-$vmname" + } + + xVMHardDiskDrive "$vmname-DataDisk$j" + { + VMName = $vmname + ControllerType = 'SCSI' + ControllerLocation = $j + Path = "$targetVMPath\$vmname\$vmname-DataDisk$j.vhdx" + Ensure = 'Present' + DependsOn = "[xVMHyperV]VM-$vmname" + } + } + + script "UnattendXML for $vmname" { + GetScript = { + $name = $using:vmname + $result = Test-Path -Path "$using:targetVMPath\$name\Unattend.xml" + return @{ 'Result' = $result } + } + + SetScript = { + try { + $name = $using:vmname + $mount = Mount-VHD -Path "$using:targetVMPath\$name\$name-OSDisk.vhdx" -Passthru -ErrorAction Stop -Verbose + Start-Sleep -Seconds 2 + $driveLetter = $mount | Get-Disk | Get-Partition | Get-Volume | Where-Object DriveLetter | Select-Object -ExpandProperty DriveLetter + + New-Item -Path $("$driveLetter" + ":" + "\Temp") -ItemType Directory -Force -ErrorAction Stop + Copy-Item -Path "$using:sourcePath\Install-AzsRolesandFeatures.ps1" -Destination $("$driveLetter" + ":" + "\Temp") -Force -ErrorAction Stop + + New-BasicUnattendXML -ComputerName $name -LocalAdministratorPassword $($using:Admincreds).Password -Domain $using:DomainName -Username $using:Admincreds.Username ` + -Password $($using:Admincreds).Password -JoinDomain $using:DomainName -AutoLogonCount 1 -OutputPath "$using:targetVMPath\$name" -Force ` + -PowerShellScriptFullPath 'c:\temp\Install-AzsRolesandFeatures.ps1' -ErrorAction Stop + + Copy-Item -Path "$using:targetVMPath\$name\Unattend.xml" -Destination $("$driveLetter" + ":" + "\Windows\system32\SysPrep") -Force -ErrorAction Stop + + Start-Sleep -Seconds 2 + } + finally { + Dismount-VHD -Path "$using:targetVMPath\$name\$name-OSDisk.vhdx" + } + Start-VM -Name $name + } + + TestScript = { + # Create and invoke a scriptblock using the $GetScript automatic variable, which contains a string representation of the GetScript. + $state = [scriptblock]::Create($GetScript).Invoke() + return $state.Result + } + DependsOn = "[xVhd]NewOSDisk-$vmname", "[script]Download DSC Config for AzsHci Hosts" + } + } + + #### Update AD with Cluster Info #### + + script "UpdateAD" { + GetScript = { + $result = Test-Path -Path "$using:sourcePath\UpdateAD.txt" + return @{ 'Result' = $result } + } + + SetScript = { + Set-Location "$using:sourcePath\" + .\Update-AD.ps1 + New-item -Path "$using:sourcePath\" -Name "UpdateAD.txt" -ItemType File -Force + } + + TestScript = { + # Create and invoke a scriptblock using the $GetScript automatic variable, which contains a string representation of the GetScript. + $state = [scriptblock]::Create($GetScript).Invoke() + return $state.Result + } + DependsOn = "[script]UnattendXML for $vmname" + } + + #### Update WAC Extensions #### + + script "WACupdater" { + GetScript = { + # Specify the WAC gateway + $wac = "https://$env:COMPUTERNAME" + + # Add the module to the current session + $module = "$env:ProgramFiles\Windows Admin Center\PowerShell\Modules\ExtensionTools\ExtensionTools.psm1" + + Import-Module -Name $module -Verbose -Force + + # List the WAC extensions + $extensions = Get-Extension $wac | Where-Object { $_.isLatestVersion -like 'False' } + + $result = if ($extensions.count -gt 0) { $false } else { $true } + + return @{ + Wac = $WAC + extensions = $extensions + result = $result + } + } + TestScript = { + # Create and invoke a scriptblock using the $GetScript automatic variable, which contains a string representation of the GetScript. + $state = [scriptblock]::Create($GetScript).Invoke() + return $state.Result + } + SetScript = { + $state = [scriptblock]::Create($GetScript).Invoke() + $date = get-date -f yyyy-MM-dd + $logFile = Join-Path -Path "C:\Users\Public" -ChildPath $('WACUpdateLog-' + $date + '.log') + New-Item -Path $logFile -ItemType File -Force + ForEach ($extension in $state.extensions) { + Update-Extension $state.wac -ExtensionId $extension.Id -Verbose | Out-File -Append -FilePath $logFile -Force + } + } + } + + #### INSTALL CHOCO, DEPLOY EDGE and Shortcuts + + cChocoInstaller InstallChoco { + InstallDir = "c:\choco" + } + + cChocoFeature allowGlobalConfirmation { + FeatureName = "allowGlobalConfirmation" + Ensure = 'Present' + DependsOn = '[cChocoInstaller]installChoco' + } + + cChocoFeature useRememberedArgumentsForUpgrades { + FeatureName = "useRememberedArgumentsForUpgrades" + Ensure = 'Present' + DependsOn = '[cChocoInstaller]installChoco' + } + + cChocoPackageInstaller "Install Chromium Edge" { + Name = 'microsoft-edge' + Ensure = 'Present' + AutoUpgrade = $true + DependsOn = '[cChocoInstaller]installChoco' + } + + cShortcut "Wac Shortcut" + { + Path = 'C:\Users\Public\Desktop\Windows Admin Center.lnk' + Target = 'C:\Program Files (x86)\Microsoft\Edge\Application\msedge.exe' + Arguments = "https://$env:computerName" + Icon = 'shell32.dll,34' + } + + #### STAGE 3c - Update Firewall + + Firewall WACInboundRule { + Name = 'WACInboundRule' + DisplayName = 'Allow Windows Admin Center' + Ensure = 'Present' + Enabled = 'True' + Profile = 'Any' + Direction = 'Inbound' + LocalPort = "443" + Protocol = 'TCP' + Description = 'Allow Windows Admin Center' + } + + Firewall WACOutboundRule { + Name = 'WACOutboundRule' + DisplayName = 'Allow Windows Admin Center' + Ensure = 'Present' + Enabled = 'True' + Profile = 'Any' + Direction = 'Outbound' + LocalPort = "443" + Protocol = 'TCP' + Description = 'Allow Windows Admin Center' + } + } +} \ No newline at end of file diff --git a/deployment/dsc/azshcihost/ComputerManagementDsc/8.4.0/ComputerManagementDsc.psd1 b/deployment/dsc/azshcihost/ComputerManagementDsc/8.4.0/ComputerManagementDsc.psd1 new file mode 100644 index 0000000..28da93f --- /dev/null +++ b/deployment/dsc/azshcihost/ComputerManagementDsc/8.4.0/ComputerManagementDsc.psd1 @@ -0,0 +1,91 @@ +@{ + # Version number of this module. + moduleVersion = '8.4.0' + + # ID used to uniquely identify this module + GUID = 'B5004952-489E-43EA-999C-F16A25355B89' + + # Author of this module + Author = 'DSC Community' + + # Company or vendor of this module + CompanyName = 'DSC Community' + + # Copyright statement for this module + Copyright = 'Copyright the DSC Community contributors. All rights reserved.' + + # Description of the functionality provided by this module + Description = 'DSC resources for configuration of a Windows computer. These DSC resources allow you to perform computer management tasks, such as renaming the computer, joining a domain and scheduling tasks as well as configuring items such as virtual memory, event logs, time zones and power settings.' + + # Minimum version of the Windows PowerShell engine required by this module + PowerShellVersion = '4.0' + + # Minimum version of the common language runtime (CLR) required by this module + CLRVersion = '4.0' + + # Functions to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no functions to export. + FunctionsToExport = @() + + # Cmdlets to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no cmdlets to export. + CmdletsToExport = @() + + # Variables to export from this module + VariablesToExport = @() + + # Aliases to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no aliases to export. + AliasesToExport = @() + + # DSC resources to export from this module + DscResourcesToExport = @( + 'Computer' + 'OfflineDomainJoin' + 'PendingReboot' + 'PowerPlan' + 'PowerShellExecutionPolicy' + 'RemoteDesktopAdmin' + 'ScheduledTask' + 'SmbServerConfiguration' + 'SmbShare' + 'SystemLocale' + 'TimeZone' + 'VirtualMemory' + 'WindowsEventLog' + 'WindowsCapability' + 'IEEnhancedSecurityConfiguration' + 'UserAccountControl' + ) + + # Private data to pass to the module specified in RootModule/ModuleToProcess. This may also contain a PSData hashtable with additional module metadata used by PowerShell. + PrivateData = @{ + + PSData = @{ + # Set to a prerelease string value if the release should be a prerelease. + Prerelease = '' + + # Tags applied to this module. These help with module discovery in online galleries. + Tags = @('DesiredStateConfiguration', 'DSC', 'DSCResource') + + # A URL to the license for this module. + LicenseUri = 'https://github.com/dsccommunity/ComputerManagementDsc/blob/master/LICENSE' + + # A URL to the main website for this project. + ProjectUri = 'https://github.com/dsccommunity/ComputerManagementDsc' + + # A URL to an icon representing this module. + IconUri = 'https://dsccommunity.org/images/DSC_Logo_300p.png' + + # ReleaseNotes of this module + ReleaseNotes = '## [8.4.0] - 2020-08-03 + +### Changed + +- ComputerManagementDsc + - Automatically publish documentation to GitHub Wiki - Fixes [Issue #342](https://github.com/dsccommunity/ComputerManagementDsc/issues/342). + +' + } # End of PSData hashtable + } # End of PrivateData hashtable +} + + + diff --git a/deployment/dsc/azshcihost/ComputerManagementDsc/8.4.0/DSCResources/DSC_Computer/DSC_Computer.psm1 b/deployment/dsc/azshcihost/ComputerManagementDsc/8.4.0/DSCResources/DSC_Computer/DSC_Computer.psm1 new file mode 100644 index 0000000..94b246f --- /dev/null +++ b/deployment/dsc/azshcihost/ComputerManagementDsc/8.4.0/DSCResources/DSC_Computer/DSC_Computer.psm1 @@ -0,0 +1,650 @@ +[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidGlobalVars', '', Scope = 'Function')] +param +( +) + +$modulePath = Join-Path -Path (Split-Path -Path (Split-Path -Path $PSScriptRoot -Parent) -Parent) -ChildPath 'Modules' + +# Import the ComputerManagementDsc Common Modules +Import-Module -Name (Join-Path -Path $modulePath ` + -ChildPath (Join-Path -Path 'ComputerManagementDsc.Common' ` + -ChildPath 'ComputerManagementDsc.Common.psm1')) + +Import-Module -Name (Join-Path -Path $modulePath -ChildPath 'DscResource.Common') + +# Import Localization Strings +$script:localizedData = Get-LocalizedData -DefaultUICulture 'en-US' + +$FailToRenameAfterJoinDomainErrorId = 'FailToRenameAfterJoinDomain,Microsoft.PowerShell.Commands.AddComputerCommand' + +<# + .SYNOPSIS + Gets the current state of the computer. + + .PARAMETER Name + The desired computer name. + + .PARAMETER DomainName + The name of the domain to join. + + .PARAMETER JoinOU + The distinguished name of the organizational unit that the computer + account will be created in. + + .PARAMETER Credential + Credential to be used to join a domain. + + .PARAMETER UnjoinCredential + Credential to be used to leave a domain. + + .PARAMETER WorkGroupName + The name of the workgroup. + + .PARAMETER Description + The value assigned here will be set as the local computer description. + + .PARAMETER Server + The Active Directory Domain Controller to use to join the domain. +#> +function Get-TargetResource +{ + [CmdletBinding()] + [OutputType([System.Collections.Hashtable])] + param + ( + [Parameter(Mandatory = $true)] + [ValidateLength(1, 15)] + [ValidateScript( { $_ -inotmatch '[\/\\:*?"<>|]' })] + [System.String] + $Name, + + [Parameter()] + [System.String] + $DomainName, + + [Parameter()] + [System.String] + $JoinOU, + + [Parameter()] + [System.Management.Automation.PSCredential] + $Credential, + + [Parameter()] + [System.Management.Automation.PSCredential] + $UnjoinCredential, + + [Parameter()] + [System.String] + $WorkGroupName, + + [Parameter()] + [System.String] + $Description, + + [Parameter()] + [System.String] + $Server + ) + + Write-Verbose -Message ($script:localizedData.GettingComputerStateMessage -f $Name) + + $convertToCimCredential = New-CimInstance ` + -ClassName DSC_Credential ` + -Property @{ + Username = [System.String] $Credential.UserName + Password = [System.String] $null + } ` + -Namespace root/microsoft/windows/desiredstateconfiguration ` + -ClientOnly + + $convertToCimUnjoinCredential = New-CimInstance ` + -ClassName DSC_Credential ` + -Property @{ + Username = [System.String] $UnjoinCredential.UserName + Password = [System.String] $null + } ` + -Namespace root/microsoft/windows/desiredstateconfiguration ` + -ClientOnly + + $returnValue = @{ + Name = $env:COMPUTERNAME + DomainName = Get-ComputerDomain + JoinOU = $JoinOU + CurrentOU = Get-ComputerOU + Credential = [ciminstance] $convertToCimCredential + UnjoinCredential = [ciminstance] $convertToCimUnjoinCredential + WorkGroupName = (Get-CimInstance -Class 'Win32_ComputerSystem').Workgroup + Description = (Get-CimInstance -Class 'Win32_OperatingSystem').Description + Server = Get-LogonServer + } + + return $returnValue +} + +<# + .SYNOPSIS + Sets the current state of the computer. + + .PARAMETER Name + The desired computer name. + + .PARAMETER DomainName + The name of the domain to join. + + .PARAMETER JoinOU + The distinguished name of the organizational unit that the computer + account will be created in. + + .PARAMETER Credential + Credential to be used to join a domain. + + .PARAMETER UnjoinCredential + Credential to be used to leave a domain. + + .PARAMETER WorkGroupName + The name of the workgroup. + + .PARAMETER Description + The value assigned here will be set as the local computer description. + + .PARAMETER Server + The Active Directory Domain Controller to use to join the domain. +#> +function Set-TargetResource +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [ValidateLength(1, 15)] + [ValidateScript( { $_ -inotmatch '[\/\\:*?"<>|]' })] + [System.String] + $Name, + + [Parameter()] + [System.String] + $DomainName, + + [Parameter()] + [System.String] + $JoinOU, + + [Parameter()] + [System.Management.Automation.PSCredential] + $Credential, + + [Parameter()] + [System.Management.Automation.PSCredential] + $UnjoinCredential, + + [Parameter()] + [System.String] + $WorkGroupName, + + [Parameter()] + [System.String] + $Description, + + [Parameter()] + [System.String] + $Server + ) + + Write-Verbose -Message ($script:localizedData.SettingComputerStateMessage -f $Name) + + Assert-DomainOrWorkGroup -DomainName $DomainName -WorkGroupName $WorkGroupName + + if ($Name -eq 'localhost') + { + $Name = $env:COMPUTERNAME + } + + if ($PSBoundParameters.ContainsKey('Description')) + { + Write-Verbose -Message ($script:localizedData.SettingComputerDescriptionMessage -f $Description) + $win32OperatingSystemCimInstance = Get-CimInstance -ClassName Win32_OperatingSystem + $win32OperatingSystemCimInstance.Description = $Description + Set-CimInstance -InputObject $win32OperatingSystemCimInstance + } + + if ($Credential) + { + if ($DomainName) + { + if ($DomainName -eq (Get-ComputerDomain)) + { + # Rename the computer, but stay joined to the domain. + Rename-Computer -NewName $Name -DomainCredential $Credential -Force + Write-Verbose -Message ($script:localizedData.RenamedComputerMessage -f $Name) + } + else + { + $addComputerParameters = @{ + DomainName = $DomainName + Credential = $Credential + Force = $true + } + $rename = $false + if ($Name -ne $env:COMPUTERNAME) + { + $addComputerParameters.Add("NewName", $Name) + $rename = $true + } + + if ($UnjoinCredential) + { + $addComputerParameters.Add("UnjoinDomainCredential", $UnjoinCredential) + } + + if ($JoinOU) + { + $addComputerParameters.Add("OUPath", $JoinOU) + } + + if ($Server) + { + $addComputerParameters.Add("Server", $Server) + } + + # Rename the computer, and join it to the domain. + try + { + Add-Computer @addComputerParameters + } + catch [System.InvalidOperationException] + { + <# + If the rename failed during the domain join, re-try the rename. + References to this issue: + https://social.technet.microsoft.com/Forums/windowsserver/en-US/81105b18-b1ff-4fcc-ae5c-2c1a7cf7bf3d/addcomputer-to-domain-with-new-name-returns-error + https://powershell.org/forums/topic/the-directory-service-is-busy/ + #> + if ($_.FullyQualifiedErrorId -eq $failToRenameAfterJoinDomainErrorId) + { + Write-Verbose -Message $script:localizedData.FailToRenameAfterJoinDomainMessage + Rename-Computer -NewName $Name -DomainCredential $Credential + } + else + { + New-InvalidOperationException -Message $_.Exception.Message -ErrorRecord $_ + } + } + catch + { + throw $_ + } + + if ($rename) + { + Write-Verbose -Message ($script:localizedData.RenamedComputerAndJoinedDomainMessage -f $Name, $DomainName) + } + else + { + Write-Verbose -Message ($script:localizedData.JoinedDomainMessage -f $DomainName) + } + } + } + elseif ($WorkGroupName) + { + if ($WorkGroupName -eq (Get-CimInstance -Class 'Win32_ComputerSystem').Workgroup) + { + # Rename the computer, but stay in the same workgroup. + Rename-Computer ` + -NewName $Name + + Write-Verbose -Message ($script:localizedData.RenamedComputerMessage -f $Name) + } + else + { + if ($Name -ne $env:COMPUTERNAME) + { + # Rename the computer, and join it to the workgroup. + Add-Computer ` + -NewName $Name ` + -Credential $Credential ` + -WorkgroupName $WorkGroupName ` + -Force + + Write-Verbose -Message ($script:localizedData.RenamedComputerAndJoinedWorkgroupMessage -f $Name, $WorkGroupName) + } + else + { + # Same computer name, and join it to the workgroup. + Add-Computer ` + -WorkGroupName $WorkGroupName ` + -Credential $Credential ` + -Force + + Write-Verbose -Message ($script:localizedData.JoinedWorkgroupMessage -f $WorkGroupName) + } + } + } + elseif ($Name -ne $env:COMPUTERNAME) + { + if (Get-ComputerDomain) + { + Rename-Computer ` + -NewName $Name ` + -DomainCredential $Credential ` + -Force + + Write-Verbose -Message ($script:localizedData.RenamedComputerMessage -f $Name) + } + else + { + Rename-Computer ` + -NewName $Name ` + -Force + + Write-Verbose -Message ($script:localizedData.RenamedComputerMessage -f $Name) + } + } + } + else + { + if ($DomainName) + { + New-InvalidArgumentException ` + -Message ($script:localizedData.CredentialsNotSpecifiedError) ` + -ArgumentName 'Credentials' + } + + if ($WorkGroupName) + { + if ($WorkGroupName -eq (Get-CimInstance -Class 'Win32_ComputerSystem').Workgroup) + { + # Same workgroup, new computer name + Rename-Computer ` + -NewName $Name ` + -Force + + Write-Verbose -Message ($script:localizedData.RenamedComputerMessage -f $Name) + } + else + { + if ($name -ne $env:COMPUTERNAME) + { + # New workgroup, new computer name + Add-Computer ` + -WorkgroupName $WorkGroupName ` + -NewName $Name + + Write-Verbose -Message ($script:localizedData.RenamedComputerAndJoinedWorkgroupMessage -f $Name, $WorkGroupName) + } + else + { + # New workgroup, same computer name + Add-Computer ` + -WorkgroupName $WorkGroupName + + Write-Verbose -Message ($script:localizedData.JoinedWorkgroupMessage -f $WorkGroupName) + } + } + } + else + { + if ($Name -ne $env:COMPUTERNAME) + { + Rename-Computer ` + -NewName $Name + + Write-Verbose -Message ($script:localizedData.RenamedComputerMessage -f $Name) + } + } + } + + $global:DSCMachineStatus = 1 +} + +<# + .SYNOPSIS + Tests the current state of the computer. + + .PARAMETER Name + The desired computer name. + + .PARAMETER DomainName + The name of the domain to join. + + .PARAMETER JoinOU + The distinguished name of the organizational unit that the computer + account will be created in. + + .PARAMETER Credential + Credential to be used to join a domain. + + .PARAMETER UnjoinCredential + Credential to be used to leave a domain. + + .PARAMETER WorkGroupName + The name of the workgroup. + + .PARAMETER Description + The value assigned here will be set as the local computer description. +#> +function Test-TargetResource +{ + [CmdletBinding()] + [OutputType([System.Boolean])] + param + ( + [Parameter(Mandatory = $true)] + [ValidateLength(1, 15)] + [ValidateScript( { $_ -inotmatch '[\/\\:*?"<>|]' })] + [System.String] + $Name, + + [Parameter()] + [System.String] + $JoinOU, + + [Parameter()] + [System.Management.Automation.PSCredential] + $Credential, + + [Parameter()] + [System.Management.Automation.PSCredential] + $UnjoinCredential, + + [Parameter()] + [System.String] + $DomainName, + + [Parameter()] + [System.String] + $WorkGroupName, + + [Parameter()] + [System.String] + $Description, + + [Parameter()] + [System.String] + $Server + ) + + Write-Verbose -Message ($script:localizedData.TestingComputerStateMessage -f $Name) + + if (($Name -ne 'localhost') -and ($Name -ne $env:COMPUTERNAME)) + { + return $false + } + + if ($PSBoundParameters.ContainsKey('Description')) + { + Write-Verbose -Message ($script:localizedData.CheckingComputerDescriptionMessage -f $Description) + + if ($Description -ne (Get-CimInstance -Class 'Win32_OperatingSystem').Description) + { + return $false + } + } + + Assert-DomainOrWorkGroup -DomainName $DomainName -WorkGroupName $WorkGroupName + + if ($DomainName) + { + if (-not ($Credential)) + { + New-InvalidArgumentException ` + -Message ($script:localizedData.CredentialsNotSpecifiedError) ` + -ArgumentName 'Credentials' + } + + try + { + Write-Verbose -Message ($script:localizedData.CheckingDomainMemberMessage -f $DomainName) + + if ($DomainName.Contains('.')) + { + $getComputerDomainParameters = @{ + netbios = $false + } + } + else + { + $getComputerDomainParameters = @{ + netbios = $true + } + } + + return ($DomainName -eq (Get-ComputerDomain @getComputerDomainParameters)) + } + catch + { + Write-Verbose -Message ($script:localizedData.CheckingNotDomainMemberMessage) + + return $false + } + } + elseif ($WorkGroupName) + { + Write-Verbose -Message ($script:localizedData.CheckingWorkgroupMemberMessage -f $WorkGroupName) + + return ($WorkGroupName -eq (Get-CimInstance -Class 'Win32_ComputerSystem').Workgroup) + } + else + { + # No Domain or Workgroup specified and computer name is correct + return $true + } +} + +<# + .SYNOPSIS + Throws an exception if both the domain name and workgroup + name is set. + + .PARAMETER DomainName + The name of the domain to join. + + .PARAMETER WorkGroupName + The name of the workgroup. +#> +function Assert-DomainOrWorkGroup +{ + [CmdletBinding()] + param + ( + [Parameter()] + [System.String] + $DomainName, + + [Parameter()] + [System.String] + $WorkGroupName + ) + + if ($DomainName -and $WorkGroupName) + { + New-InvalidOperationException ` + -Message ($script:localizedData.DomainNameAndWorkgroupNameError) + } +} + +<# + .SYNOPSIS + Returns the domain the computer is joined to. + + .PARAMETER NetBios + Specifies if the NetBIOS name is returned instead of + the fully qualified domain name. +#> +function Get-ComputerDomain +{ + [CmdletBinding()] + [OutputType([System.String])] + param + ( + [Parameter()] + [Switch] + $NetBios + ) + + try + { + $domainInfo = Get-CimInstance -ClassName Win32_ComputerSystem + if ($domainInfo.PartOfDomain -eq $true) + { + if ($NetBios) + { + $domainName = (Get-Item -Path Env:\USERDOMAIN).Value + } + else + { + $domainName = $domainInfo.Domain + } + } + else + { + $domainName = '' + } + + return $domainName + } + catch [System.Management.Automation.MethodInvocationException] + { + Write-Verbose -Message ($script:localizedData.ComputerNotInDomainMessage) + } +} + +<# + .SYNOPSIS + Gets the organisation unit in the domain that the + computer account exists in. +#> +function Get-ComputerOU +{ + [CmdletBinding()] + param + ( + ) + + $ou = $null + + if (Get-ComputerDomain) + { + $dn = $null + $dn = ([adsisearcher]"(&(objectCategory=computer)(objectClass=computer)(cn=$env:COMPUTERNAME))").FindOne().Properties.distinguishedname + $ou = $dn -replace '^(CN=.*?(?<=,))', '' + } + + return $ou +} + +<# + .SYNOPSIS + Returns the logon server. +#> +function Get-LogonServer +{ + [CmdletBinding()] + [OutputType([System.String])] + param () + + $logonserver = $env:LOGONSERVER -replace "\\", "" + return $logonserver +} + +Export-ModuleMember -Function *-TargetResource diff --git a/deployment/dsc/azshcihost/ComputerManagementDsc/8.4.0/DSCResources/DSC_Computer/DSC_Computer.schema.mof b/deployment/dsc/azshcihost/ComputerManagementDsc/8.4.0/DSCResources/DSC_Computer/DSC_Computer.schema.mof new file mode 100644 index 0000000..96eae76 --- /dev/null +++ b/deployment/dsc/azshcihost/ComputerManagementDsc/8.4.0/DSCResources/DSC_Computer/DSC_Computer.schema.mof @@ -0,0 +1,13 @@ +[ClassVersion("1.0.1.0"), FriendlyName("Computer")] +class DSC_Computer : OMI_BaseResource +{ + [Key, Description("The desired computer name.")] String Name; + [Write, Description("The name of the domain to join.")] String DomainName; + [Write, Description("The distinguished name of the organizational unit that the computer account will be created in.")] String JoinOU; + [Write, Description("Credential to be used to join a domain."), EmbeddedInstance("MSFT_Credential")] String Credential; + [Write, Description("Credential to be used to leave a domain."), EmbeddedInstance("MSFT_Credential")] String UnjoinCredential; + [Write, Description("The name of the workgroup.")] String WorkGroupName; + [Write, Description("The value assigned here will be set as the local computer description.")] String Description; + [Write, Description("The Active Directory Domain Controller to use to join the domain")] String Server; + [Read, Description("A read-only property that specifies the organizational unit that the computer account is currently in.")] String CurrentOU; +}; diff --git a/deployment/dsc/azshcihost/ComputerManagementDsc/8.4.0/DSCResources/DSC_Computer/README.md b/deployment/dsc/azshcihost/ComputerManagementDsc/8.4.0/DSCResources/DSC_Computer/README.md new file mode 100644 index 0000000..d23b215 --- /dev/null +++ b/deployment/dsc/azshcihost/ComputerManagementDsc/8.4.0/DSCResources/DSC_Computer/README.md @@ -0,0 +1,4 @@ +# Description + +The resource allows you to configure a computer by changing its name and +description and modifying its Active Directory domain or workgroup membership. diff --git a/deployment/dsc/azshcihost/ComputerManagementDsc/8.4.0/DSCResources/DSC_Computer/en-US/DSC_Computer.strings.psd1 b/deployment/dsc/azshcihost/ComputerManagementDsc/8.4.0/DSCResources/DSC_Computer/en-US/DSC_Computer.strings.psd1 new file mode 100644 index 0000000..011bdec --- /dev/null +++ b/deployment/dsc/azshcihost/ComputerManagementDsc/8.4.0/DSCResources/DSC_Computer/en-US/DSC_Computer.strings.psd1 @@ -0,0 +1,19 @@ +ConvertFrom-StringData @' + GettingComputerStateMessage = Getting computer state for '{0}'. + SettingComputerStateMessage = Setting computer state for '{0}'. + SettingComputerDescriptionMessage = Setting computer description to '{0}'. + RenamedComputerMessage = Renamed computer to '{0}'. + RenamedComputerAndJoinedDomainMessage = Renamed computer to '{0}' and added to the domain '{1}'. + JoinedDomainMessage = Added computer to domain '{0}'. + FailToRenameAfterJoinDomainMessage = Failed to rename the computer during the domain join. Re-trying the rename. + RenamedComputerAndJoinedWorkgroupMessage = Renamed computer to '{0}' and addded to workgroup '{1}'. + JoinedWorkgroupMessage = Added computer to workgroup '{0}'. + CredentialsNotSpecifiedError = Must to specify credentials with domain. + TestingComputerStateMessage = Testing computer state for '{0}'. + CheckingComputerDescriptionMessage = Checking if computer description is '{0}'. + CheckingDomainMemberMessage = Checking if the machine is a member of domain '{0}'. + CheckingNotDomainMemberMessage = Checking if the machine is a not a member of a domain. + CheckingWorkgroupMemberMessage = Checking if the machine is a member of workgroup '{0}'. + DomainNameAndWorkgroupNameError = Only DomainName or WorkGroupName can be specified at once. + ComputerNotInDomainMessage = This machine is not a domain member. +'@ diff --git a/deployment/dsc/azshcihost/ComputerManagementDsc/8.4.0/DSCResources/DSC_IEEnhancedSecurityConfiguration/DSC_IEEnhancedSecurityConfiguration.psm1 b/deployment/dsc/azshcihost/ComputerManagementDsc/8.4.0/DSCResources/DSC_IEEnhancedSecurityConfiguration/DSC_IEEnhancedSecurityConfiguration.psm1 new file mode 100644 index 0000000..0a2506d --- /dev/null +++ b/deployment/dsc/azshcihost/ComputerManagementDsc/8.4.0/DSCResources/DSC_IEEnhancedSecurityConfiguration/DSC_IEEnhancedSecurityConfiguration.psm1 @@ -0,0 +1,251 @@ +$modulePath = Join-Path -Path (Split-Path -Path (Split-Path -Path $PSScriptRoot -Parent) -Parent) -ChildPath 'Modules' + +# Import the ComputerManagementDsc Common Modules +Import-Module -Name (Join-Path -Path $modulePath ` + -ChildPath (Join-Path -Path 'ComputerManagementDsc.Common' ` + -ChildPath 'ComputerManagementDsc.Common.psm1')) + +Import-Module -Name (Join-Path -Path $modulePath -ChildPath 'DscResource.Common') + +# Import Localization Strings +$script:localizedData = Get-LocalizedData -DefaultUICulture 'en-US' + +$script:registryKey_Administrators = 'HKLM:\SOFTWARE\Microsoft\Active Setup\Installed Components\{A509B1A7-37EF-4b3f-8CFC-4F3A74704073}' +$script:registryKey_Users = 'HKLM:\SOFTWARE\Microsoft\Active Setup\Installed Components\{A509B1A8-37EF-4b3f-8CFC-4F3A74704073}' +$script:registryKey_Property = 'IsInstalled' + +<# + .SYNOPSIS + Gets the current state of the IE Enhanced Security Configuration. + + .PARAMETER Role + Specifies the role for which the IE Enhanced Security Configuration + should be changed. + + .PARAMETER Enabled + Specifies if IE Enhanced Security Configuration should be enabled or + disabled. + + .PARAMETER SuppressRestart + Specifies if a restart of the node should be suppressed. By default the + node will be restarted if the value is changed. +#> +function Get-TargetResource +{ + [CmdletBinding()] + [OutputType([System.Collections.Hashtable])] + param + ( + [Parameter(Mandatory = $true)] + [ValidateSet('Administrators', 'Users')] + [System.String] + $Role, + + [Parameter(Mandatory = $true)] + [System.Boolean] + $Enabled, + + [Parameter()] + [System.Boolean] + $SuppressRestart + ) + + Write-Verbose -Message ($script:localizedData.GettingStateMessage -f $Role) + + $registryKey = Get-Variable -Name ('registryKey_{0}' -f $Role) -Scope 'Script' -ValueOnly + + try + { + $currentlyEnabled = [System.Boolean] (Get-ItemProperty -Path $registryKey -ErrorAction 'Stop').$script:registryKey_Property + } + catch + { + $currentlyEnabled = $false + + Write-Warning -Message ($script:localizedData.UnableToDetermineState -f $registryKey) + } + + $returnValue = @{ + Role = $Role + Enabled = $currentlyEnabled + SuppressRestart = $SuppressRestart + } + + return $returnValue +} + +<# + .SYNOPSIS + Sets the current state of the IE Enhanced Security Configuration. + + .PARAMETER Role + Specifies the role for which the IE Enhanced Security Configuration + should be changed. + + .PARAMETER Enabled + Specifies if IE Enhanced Security Configuration should be enabled or + disabled. + + .PARAMETER SuppressRestart + Specifies if a restart of the node should be suppressed. By default the + node will be restarted if the value is changed. + + .NOTES + The change could come in affect if the process Explorer is stopped, which + will make Windows automatically start a new process. But, stopping a + process feels wrong so the resource instead restarts the node when the + value is changed. +#> +function Set-TargetResource +{ + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidGlobalVars', '', Scope = 'Function')] + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [ValidateSet('Administrators', 'Users')] + [System.String] + $Role, + + [Parameter(Mandatory = $true)] + [System.Boolean] + $Enabled, + + [Parameter()] + [System.Boolean] + $SuppressRestart + ) + + $getTargetResourceResult = Get-TargetResource @PSBoundParameters + + if ($getTargetResourceResult.Enabled -ne $Enabled) + { + Write-Verbose -Message ($script:localizedData.SettingStateMessage -f $Role) + + $registryKey = Get-Variable -Name ('registryKey_{0}' -f $Role) -Scope 'Script' -ValueOnly + + try + { + $setItemPropertyParameters = @{ + Path = $registryKey + Name = $script:registryKey_Property + Value = $Enabled + ErrorAction = 'Stop' + } + + Set-ItemProperty @setItemPropertyParameters + } + catch + { + New-InvalidOperationException ` + -Message ($script:localizedData.FailedToSetDesiredState -f $Role) ` + -ErrorRecord $_ + } + + if ($SuppressRestart) + { + Write-Warning -Message $script:localizedData.SuppressRestart + } + else + { + $global:DSCMachineStatus = 1 + } + } + else + { + Write-Verbose -Message ($script:localizedData.InDesiredState -f $Role) + } +} + +<# + .SYNOPSIS + Tests the current state of the IE Enhanced Security Configuration. + + .PARAMETER Role + Specifies the role for which the IE Enhanced Security Configuration + should be changed. + + .PARAMETER Enabled + Specifies if IE Enhanced Security Configuration should be enabled or + disabled. + + .PARAMETER SuppressRestart + Specifies if a restart of the node should be suppressed. By default the + node will be restarted if the value is changed. +#> +function Test-TargetResource +{ + [CmdletBinding()] + [OutputType([System.Boolean])] + param + ( + [Parameter(Mandatory = $true)] + [ValidateSet('Administrators', 'Users')] + [System.String] + $Role, + + [Parameter(Mandatory = $true)] + [System.Boolean] + $Enabled, + + [Parameter()] + [System.Boolean] + $SuppressRestart + ) + + Write-Verbose -Message ($script:localizedData.TestingStateMessage -f $Role) + + $getTargetResourceResult = Get-TargetResource @PSBoundParameters + + if ($getTargetResourceResult.Enabled -ne $Enabled) + { + $testTargetResourceReturnValue = $false + + $currentStateString = Get-BooleanStringValue -Enabled $getTargetResourceResult.Enabled + $desiredStateString = Get-BooleanStringValue -Enabled $Enabled + + Write-Verbose -Message ($script:localizedData.NotInDesiredState -f $Role, $currentStateString, $desiredStateString) + } + else + { + $testTargetResourceReturnValue = $true + + Write-Verbose -Message ($script:localizedData.InDesiredState -f $Role) + } + + return $testTargetResourceReturnValue +} + +<# + .SYNOPSIS + Returns the string representation of a boolean value. + + .PARAMETER Enabled + Specifies the boolean value to return the string representation for. +#> +function Get-BooleanStringValue +{ + [CmdletBinding()] + [OutputType([System.String])] + param + ( + [Parameter(Mandatory = $true)] + [System.Boolean] + $Enabled + ) + + $booleanStringValue = switch ($Enabled) + { + $false + { + 'disabled' + } + + $true + { + 'enabled' + } + } + + return $booleanStringValue +} diff --git a/deployment/dsc/azshcihost/ComputerManagementDsc/8.4.0/DSCResources/DSC_IEEnhancedSecurityConfiguration/DSC_IEEnhancedSecurityConfiguration.schema.mof b/deployment/dsc/azshcihost/ComputerManagementDsc/8.4.0/DSCResources/DSC_IEEnhancedSecurityConfiguration/DSC_IEEnhancedSecurityConfiguration.schema.mof new file mode 100644 index 0000000..10aafd7 --- /dev/null +++ b/deployment/dsc/azshcihost/ComputerManagementDsc/8.4.0/DSCResources/DSC_IEEnhancedSecurityConfiguration/DSC_IEEnhancedSecurityConfiguration.schema.mof @@ -0,0 +1,7 @@ +[ClassVersion("1.0.1.0"), FriendlyName("IEEnhancedSecurityConfiguration")] +class DSC_IEEnhancedSecurityConfiguration : OMI_BaseResource +{ + [Key, Description("Specifies the role for which the IE Enhanced Security Configuration should be changed."), ValueMap{"Administrators","Users"}, Values{"Administrators","Users"}] String Role; + [Required, Description("Specifies if IE Enhanced Security Configuration should be enabled or disabled.")] Boolean Enabled; + [Write, Description("Specifies if a restart of the node should be suppressed. By default the node will be restarted if the value is changed.")] Boolean SuppressRestart; +}; diff --git a/deployment/dsc/azshcihost/ComputerManagementDsc/8.4.0/DSCResources/DSC_IEEnhancedSecurityConfiguration/README.md b/deployment/dsc/azshcihost/ComputerManagementDsc/8.4.0/DSCResources/DSC_IEEnhancedSecurityConfiguration/README.md new file mode 100644 index 0000000..fcbfdc1 --- /dev/null +++ b/deployment/dsc/azshcihost/ComputerManagementDsc/8.4.0/DSCResources/DSC_IEEnhancedSecurityConfiguration/README.md @@ -0,0 +1,4 @@ +# Description + +This resource allows you to configure the IE Enhanced Security Configuration +for administrator or user roles. diff --git a/deployment/dsc/azshcihost/ComputerManagementDsc/8.4.0/DSCResources/DSC_IEEnhancedSecurityConfiguration/en-US/DSC_IEEnhancedSecurityConfiguration.strings.psd1 b/deployment/dsc/azshcihost/ComputerManagementDsc/8.4.0/DSCResources/DSC_IEEnhancedSecurityConfiguration/en-US/DSC_IEEnhancedSecurityConfiguration.strings.psd1 new file mode 100644 index 0000000..d470503 --- /dev/null +++ b/deployment/dsc/azshcihost/ComputerManagementDsc/8.4.0/DSCResources/DSC_IEEnhancedSecurityConfiguration/en-US/DSC_IEEnhancedSecurityConfiguration.strings.psd1 @@ -0,0 +1,10 @@ +ConvertFrom-StringData @' + GettingStateMessage = Getting IE Enhanced Security Configuration state for '{0}'. (IEESC0001) + SettingStateMessage = Setting IE Enhanced Security Configuration state for '{0}'. (IEESC0002) + TestingStateMessage = Testing IE Enhanced Security Configuration state for '{0}'. (IEESC0003) + SuppressRestart = Suppressing the restart. For the change to come in affect the node must be restarted manually. (IEESC0004) + InDesiredState = The IE Enhanced Security Configuration for '{0}' is in desired state. (IEESC0005) + NotInDesiredState = The IE Enhanced Security Configuration for '{0}' was {1}, but expected it to be {2}. (IEESC0006) + UnableToDetermineState = The current state cannot be determined because the registry path '{0}' cannot be read. (IEESC0007) + FailedToSetDesiredState = Failed to set the desired state for '{0}'. (IEESC0008) +'@ diff --git a/deployment/dsc/azshcihost/ComputerManagementDsc/8.4.0/DSCResources/DSC_OfflineDomainJoin/DSC_OfflineDomainJoin.psm1 b/deployment/dsc/azshcihost/ComputerManagementDsc/8.4.0/DSCResources/DSC_OfflineDomainJoin/DSC_OfflineDomainJoin.psm1 new file mode 100644 index 0000000..ecd48e2 --- /dev/null +++ b/deployment/dsc/azshcihost/ComputerManagementDsc/8.4.0/DSCResources/DSC_OfflineDomainJoin/DSC_OfflineDomainJoin.psm1 @@ -0,0 +1,243 @@ +[Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidGlobalVars", "", Scope = "Function")] +param +( +) + +$modulePath = Join-Path -Path (Split-Path -Path (Split-Path -Path $PSScriptRoot -Parent) -Parent) -ChildPath 'Modules' + +# Import the ComputerManagementDsc Common Modules +Import-Module -Name (Join-Path -Path $modulePath ` + -ChildPath (Join-Path -Path 'ComputerManagementDsc.Common' ` + -ChildPath 'ComputerManagementDsc.Common.psm1')) + +Import-Module -Name (Join-Path -Path $modulePath -ChildPath 'DscResource.Common') + +# Import Localization Strings +$script:localizedData = Get-LocalizedData -DefaultUICulture 'en-US' + +<# + .SYNOPSIS + Joins the computer to a domain with a domain join file. + + .PARAMETER IsSingleInstance + Specifies the resource is a single instance, the value must be 'Yes'. + This value is Not used in Get-TargetResource. + + .PARAMETER RequestFile + The full path to the Offline Domain Join Request file to use. + This value is not used in Get-TargetResource. +#> +function Get-TargetResource +{ + [CmdletBinding()] + [OutputType([System.Collections.Hashtable])] + param + ( + [Parameter(Mandatory = $true)] + [ValidateSet('Yes')] + [System.String] + $IsSingleInstance, + + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $RequestFile + ) + + Write-Verbose -Message ( @( "$($MyInvocation.MyCommand): " + $($script:localizedData.GettingOfflineDomainJoinMessage) + ) -join '') + + <# + It is not possible to read the ODJ file that was used to join a domain + So it has to always be returned as blank. + #> + $returnValue = @{ + IsSingleInstance = 'Yes' + RequestFile = '' + } + + return $returnValue +} # Get-TargetResource + +<# + .SYNOPSIS + Sets the current state of the offline domain join. + + .PARAMETER IsSingleInstance + Specifies the resource is a single instance, the value must be 'Yes'. + + .PARAMETER RequestFile + The full path to the Offline Domain Join Request file to use. +#> +function Set-TargetResource +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [ValidateSet('Yes')] + [System.String] + $IsSingleInstance, + + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $RequestFile + ) + + Write-Verbose -Message ( @( "$($MyInvocation.MyCommand): " + $($script:localizedData.ApplyingOfflineDomainJoinMessage) + ) -join '') + + # Check the ODJ Request file exists + if (-not (Test-Path -Path $RequestFile)) + { + New-InvalidArgumentException ` + -Message ($script:localizedData.RequestFileNotFoundError -f $RequestFile) ` + -ArgumentName 'RequestFile' + } # if + + <# + Don't need to check if the domain is already joined because + Set-TargetResource wouldn't fire unless it wasn't. + #> + Join-Domain -RequestFile $RequestFile +} # Set-TargetResource + +<# + .SYNOPSIS + Tests the current state of the machine joining a domain using + an offline domain join file. + + .PARAMETER IsSingleInstance + Specifies the resource is a single instance, the value must be 'Yes'. + + .PARAMETER RequestFile + The full path to the Offline Domain Join Request file to use. +#> +function Test-TargetResource +{ + [CmdletBinding()] + [OutputType([System.Boolean])] + param + ( + [Parameter(Mandatory = $true)] + [ValidateSet('Yes')] + [System.String] + $IsSingleInstance, + + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $RequestFile + ) + + # Flag to signal whether settings are correct + [System.Boolean] $desiredConfigurationMatch = $true + + Write-Verbose -Message ( @("$($MyInvocation.MyCommand): " + $($script:localizedData.CheckingOfflineDomainJoinMessage) + ) -join '') + + # Check the ODJ Request file exists + if (-not (Test-Path -Path $RequestFile)) + { + New-InvalidArgumentException ` + -Message ($script:localizedData.RequestFileNotFoundError -f $RequestFile) ` + -ArgumentName 'RequestFile' + } # if + + $currentDomainName = Get-DomainName + + if ($currentDomainName) + { + # Domain is already joined. + Write-Verbose -Message ( @( + "$($MyInvocation.MyCommand): " + $($script:localizedData.DomainAlreadyJoinedMessage -f $CurrentDomainName) ` + ) -join '' ) + } + else + { + # Domain is not joined, so change is required. + Write-Verbose -Message ( @("$($MyInvocation.MyCommand): " + $($script:localizedData.DomainNotJoinedMessage) + ) -join '') + + $desiredConfigurationMatch = $false + } # if + + return $desiredConfigurationMatch +} # Test-TargetResource + +<# + .SYNOPSIS + Uses DJoin.exe to join a Domain using a ODJ Request File. +#> +function Join-Domain +{ + [CmdletBinding()] + param ( + [Parameter(Mandatory = $true)] + [System.String] + $RequestFile + ) + + Write-Verbose -Message ( @( + "$($MyInvocation.MyCommand): " + $($script:localizedData.AttemptingDomainJoinMessage -f $RequestFile) ` + ) -join '' ) + + $djoinResult = & djoin.exe @( + '/REQUESTODJ' + '/LOADFILE' + $RequestFile + '/WINDOWSPATH' + $ENV:SystemRoot + '/LOCALOS') + + if ($LASTEXITCODE -eq 0) + { + # Notify DSC that a reboot is required. + $global:DSCMachineStatus = 1 + } + else + { + Write-Verbose -Message $djoinResult + + New-InvalidOperationException ` + -Message ($script:localizedData.DjoinError -f $LASTEXITCODE) + } # if + + Write-Verbose -Message ( @( + "$($MyInvocation.MyCommand): " + $($script:localizedData.DomainJoinedMessage -f $RequestFile) ` + ) -join '' ) +} # function Join-Domain + +<# + .SYNOPSIS + Returns the name of the Domain the computer is joined to or + $null if not domain joined. +#> +function Get-DomainName +{ + [CmdletBinding()] + [OutputType([System.String])] + param () + + # Use CIM to detect the domain name so that this will work on Nano Server. + $computerSystem = Get-CimInstance -ClassName 'Win32_ComputerSystem' -Namespace root\cimv2 + + if ($computerSystem.Workgroup) + { + return $null + } + else + { + $computerSystem.Domain + } +} # function Get-DomainName + +Export-ModuleMember -Function *-TargetResource diff --git a/deployment/dsc/azshcihost/ComputerManagementDsc/8.4.0/DSCResources/DSC_OfflineDomainJoin/DSC_OfflineDomainJoin.schema.mof b/deployment/dsc/azshcihost/ComputerManagementDsc/8.4.0/DSCResources/DSC_OfflineDomainJoin/DSC_OfflineDomainJoin.schema.mof new file mode 100644 index 0000000..787469c --- /dev/null +++ b/deployment/dsc/azshcihost/ComputerManagementDsc/8.4.0/DSCResources/DSC_OfflineDomainJoin/DSC_OfflineDomainJoin.schema.mof @@ -0,0 +1,6 @@ +[ClassVersion("1.0.0.0"), FriendlyName("OfflineDomainJoin")] +class DSC_OfflineDomainJoin : OMI_BaseResource +{ + [Key, Description("Specifies the resource is a single instance, the value must be 'Yes'."), ValueMap{"Yes"}, Values{"Yes"}] String IsSingleInstance; + [Required, Description("The full path to the Offline Domain Join Request file to use.")] String RequestFile; +}; diff --git a/deployment/dsc/azshcihost/ComputerManagementDsc/8.4.0/DSCResources/DSC_OfflineDomainJoin/README.md b/deployment/dsc/azshcihost/ComputerManagementDsc/8.4.0/DSCResources/DSC_OfflineDomainJoin/README.md new file mode 100644 index 0000000..578daea --- /dev/null +++ b/deployment/dsc/azshcihost/ComputerManagementDsc/8.4.0/DSCResources/DSC_OfflineDomainJoin/README.md @@ -0,0 +1,5 @@ +# Description + +The resource allows you to join computers to an Active Directory domain using an +[Offline Domain Join](https://technet.microsoft.com/en-us/library/offline-domain-join-djoin-step-by-step(v=ws.10).aspx) +request file. diff --git a/deployment/dsc/azshcihost/ComputerManagementDsc/8.4.0/DSCResources/DSC_OfflineDomainJoin/en-US/DSC_OfflineDomainJoin.strings.psd1 b/deployment/dsc/azshcihost/ComputerManagementDsc/8.4.0/DSCResources/DSC_OfflineDomainJoin/en-US/DSC_OfflineDomainJoin.strings.psd1 new file mode 100644 index 0000000..3cacc14 --- /dev/null +++ b/deployment/dsc/azshcihost/ComputerManagementDsc/8.4.0/DSCResources/DSC_OfflineDomainJoin/en-US/DSC_OfflineDomainJoin.strings.psd1 @@ -0,0 +1,11 @@ +ConvertFrom-StringData @' + GettingOfflineDomainJoinMessage = Getting the Offline Domain Join State. + ApplyingOfflineDomainJoinMessage = Applying the Offline Domain Join State. + AttemptingDomainJoinMessage = Attempting domain join using ODJ Request file '{0}'. + DomainJoinedMessage = Domain joined using ODJ Request file '{0}'. Reboot will be required. + CheckingOfflineDomainJoinMessage = Checking the Offline Domain Join State. + DomainAlreadyJoinedMessage = The computer is already joined to a domain '{0}'. Change not required. + DomainNotJoinedMessage = The computer is not joined to a domain. Change required. + RequestFileNotFoundError = The ODJ Request file '{0}' does not exist. + DjoinError = Error {0} occured requesting the Offline Domain Join. +'@ diff --git a/deployment/dsc/azshcihost/ComputerManagementDsc/8.4.0/DSCResources/DSC_PendingReboot/DSC_PendingReboot.psm1 b/deployment/dsc/azshcihost/ComputerManagementDsc/8.4.0/DSCResources/DSC_PendingReboot/DSC_PendingReboot.psm1 new file mode 100644 index 0000000..c6d01f6 --- /dev/null +++ b/deployment/dsc/azshcihost/ComputerManagementDsc/8.4.0/DSCResources/DSC_PendingReboot/DSC_PendingReboot.psm1 @@ -0,0 +1,387 @@ +[Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidGlobalVars", "", Scope = "Function")] +param () + +$modulePath = Join-Path -Path (Split-Path -Path (Split-Path -Path $PSScriptRoot -Parent) -Parent) -ChildPath 'Modules' + +# Import the ComputerManagementDsc Common Modules +Import-Module -Name (Join-Path -Path $modulePath ` + -ChildPath (Join-Path -Path 'ComputerManagementDsc.Common' ` + -ChildPath 'ComputerManagementDsc.Common.psm1')) + +Import-Module -Name (Join-Path -Path $modulePath -ChildPath 'DscResource.Common') + +# Import Localization Strings +$script:localizedData = Get-LocalizedData -DefaultUICulture 'en-US' + +<# + This data file contains a list of reboot triggers that will be checked + when determining if reboot is required. This is stored in a separate + data file so that it can also be used in testing. +#> +$script:localizedResourceData = Import-LocalizedData ` + -BaseDirectory $PSScriptRoot ` + -FileName 'DSC_PendingReboot.data.psd1' +$script:rebootTriggers = $script:localizedResourceData.RebootTriggers +<# + .SYNOPSIS + Returns the current state of the pending reboot. + + .PARAMETER Name + Specifies the name of this pending reboot check. +#> +function Get-TargetResource +{ + [CmdletBinding()] + [OutputType([System.Collections.Hashtable])] + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $Name + ) + + Write-Verbose -Message ($script:localizedData.GettingPendingRebootStateMessage -f $Name) + + return Get-PendingRebootState @PSBoundParameters +} + +<# + .SYNOPSIS + Sets the current state of the pending reboot. + + .PARAMETER Name + Specifies the name of this pending reboot check. + + .PARAMETER SkipComponentBasedServicing + Specifies whether to skip reboots triggered by the Component-Based Servicing component. + + .PARAMETER SkipWindowsUpdate + Specifies whether to skip reboots triggered by Windows Update. + + .PARAMETER SkipPendingFileRename + Specifies whether to skip pending file rename reboots. + + .PARAMETER SkipPendingComputerRename + Specifies whether to skip reboots triggered by a pending computer rename. + + .PARAMETER SkipCcmClientSDK + Specifies whether to skip reboots triggered by the ConfigMgr client. Defaults to True. +#> +function Set-TargetResource +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $Name, + + [Parameter()] + [System.Boolean] + $SkipComponentBasedServicing, + + [Parameter()] + [System.Boolean] + $SkipWindowsUpdate, + + [Parameter()] + [System.Boolean] + $SkipPendingFileRename, + + [Parameter()] + [System.Boolean] + $SkipPendingComputerRename, + + [Parameter()] + [System.Boolean] + $SkipCcmClientSDK = $true + ) + + Write-Verbose -Message ($script:localizedData.SettingPendingRebootStateMessage -f $Name) + + $currentStatus = Get-PendingRebootState @PSBoundParameters + + if ($currentStatus.RebootRequired) + { + $global:DSCMachineStatus = 1 + } +} + +<# + .SYNOPSIS + Tests the current state of the pending reboot. + + .PARAMETER Name + Specifies the name of this pending reboot check. + + .PARAMETER SkipComponentBasedServicing + Specifies whether to skip reboots triggered by the Component-Based Servicing component. + + .PARAMETER SkipWindowsUpdate + Specifies whether to skip reboots triggered by Windows Update. + + .PARAMETER SkipPendingFileRename + Specifies whether to skip pending file rename reboots. + + .PARAMETER SkipPendingComputerRename + Specifies whether to skip reboots triggered by a pending computer rename. + + .PARAMETER SkipCcmClientSDK + Specifies whether to skip reboots triggered by the ConfigMgr client. Defaults to True. +#> +function Test-TargetResource +{ + [CmdletBinding()] + [OutputType([System.Boolean])] + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $Name, + + [Parameter()] + [System.Boolean] + $SkipComponentBasedServicing, + + [Parameter()] + [System.Boolean] + $SkipWindowsUpdate, + + [Parameter()] + [System.Boolean] + $SkipPendingFileRename, + + [Parameter()] + [System.Boolean] + $SkipPendingComputerRename, + + [Parameter()] + [System.Boolean] + $SkipCcmClientSDK = $true + ) + + Write-Verbose -Message ($script:localizedData.TestingPendingRebootStateMessage -f $Name) + + $currentStatus = Get-PendingRebootState @PSBoundParameters + + return (-not $currentStatus.RebootRequired) +} + +<# + .SYNOPSIS + Returns a hash table containing the current state of the pending reboot + triggers. + + .PARAMETER Name + Specifies the name of this pending reboot check. + + .PARAMETER SkipComponentBasedServicing + Specifies whether to skip reboots triggered by the Component-Based Servicing component. + + .PARAMETER SkipWindowsUpdate + Specifies whether to skip reboots triggered by Windows Update. + + .PARAMETER SkipPendingFileRename + Specifies whether to skip pending file rename reboots. + + .PARAMETER SkipPendingComputerRename + Specifies whether to skip reboots triggered by a pending computer rename. + + .PARAMETER SkipCcmClientSDK + Specifies whether to skip reboots triggered by the ConfigMgr client. Defaults to True. +#> +function Get-PendingRebootHashTable +{ + [CmdletBinding()] + [OutputType([System.Collections.Hashtable])] + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $Name, + + [Parameter()] + [System.Boolean] + $SkipComponentBasedServicing, + + [Parameter()] + [System.Boolean] + $SkipWindowsUpdate, + + [Parameter()] + [System.Boolean] + $SkipPendingFileRename, + + [Parameter()] + [System.Boolean] + $SkipPendingComputerRename, + + [Parameter()] + [System.Boolean] + $SkipCcmClientSDK = $true + ) + + # The list of registry keys that will be used to determine if a reboot is required + $rebootRegistryKeys = @{ + ComponentBasedServicing = 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Component Based Servicing\' + WindowsUpdate = 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\WindowsUpdate\Auto Update\' + PendingFileRename = 'HKLM:\SYSTEM\CurrentControlSet\Control\Session Manager\' + ActiveComputerName = 'HKLM:\SYSTEM\CurrentControlSet\Control\ComputerName\ActiveComputerName' + PendingComputerName = 'HKLM:\SYSTEM\CurrentControlSet\Control\ComputerName\ComputerName' + } + + $componentBasedServicingKeys = (Get-ChildItem -Path $rebootRegistryKeys.ComponentBasedServicing).Name + + if ($componentBasedServicingKeys) + { + $componentBasedServicing = $componentBasedServicingKeys.Split('\') -contains 'RebootPending' + } + else + { + $componentBasedServicing = $false + } + + $windowsUpdateKeys = (Get-ChildItem -Path $rebootRegistryKeys.WindowsUpdate).Name + + if ($windowsUpdateKeys) + { + $windowsUpdate = $windowsUpdateKeys.Split('\') -contains 'RebootRequired' + } + else + { + $windowsUpdate = $false + } + + $pendingFileRename = (Get-ItemProperty -Path $rebootRegistryKeys.PendingFileRename).PendingFileRenameOperations.Length -gt 0 + $activeComputerName = (Get-ItemProperty -Path $rebootRegistryKeys.ActiveComputerName).ComputerName + $pendingComputerName = (Get-ItemProperty -Path $rebootRegistryKeys.PendingComputerName).ComputerName + $pendingComputerRename = $activeComputerName -ne $pendingComputerName + + if ($SkipCcmClientSDK) + { + $ccmClientSDK = $false + } + else + { + $invokeCimMethodParameters = @{ + NameSpace = 'ROOT\ccm\ClientSDK' + ClassName = 'CCM_ClientUtilities' + Name = 'DetermineIfRebootPending' + ErrorAction = 'Stop' + } + + try + { + $ccmClientSDK = Invoke-CimMethod @invokeCimMethodParameters + } + catch + { + Write-Warning -Message ($script:localizedData.QueryCcmClientUtilitiesFailedMessage -f $_) + } + + $ccmClientSDK = ($ccmClientSDK.ReturnValue -eq 0) -and ($ccmClientSDK.IsHardRebootPending -or $ccmClientSDK.RebootPending) + } + + return @{ + Name = $Name + SkipComponentBasedServicing = $SkipComponentBasedServicing + ComponentBasedServicing = $componentBasedServicing + SkipWindowsUpdate = $SkipWindowsUpdate + WindowsUpdate = $windowsUpdate + SkipPendingFileRename = $SkipPendingFileRename + PendingFileRename = $pendingFileRename + SkipPendingComputerRename = $SkipPendingComputerRename + PendingComputerRename = $pendingComputerRename + SkipCcmClientSDK = $SkipCcmClientSDK + CcmClientSDK = $ccmClientSDK + } +} + +<# + .SYNOPSIS + Returns the current state of the pending reboot by assessing the result provided + in a pending reboot hash table. + + .PARAMETER Name + Specifies the name of this pending reboot check. + + .PARAMETER SkipComponentBasedServicing + Specifies whether to skip reboots triggered by the Component-Based Servicing component. + + .PARAMETER SkipWindowsUpdate + Specifies whether to skip reboots triggered by Windows Update. + + .PARAMETER SkipPendingFileRename + Specifies whether to skip pending file rename reboots. + + .PARAMETER SkipPendingComputerRename + Specifies whether to skip reboots triggered by a pending computer rename. + + .PARAMETER SkipCcmClientSDK + Specifies whether to skip reboots triggered by the ConfigMgr client. Defaults to True. +#> +function Get-PendingRebootState +{ + [CmdletBinding()] + [OutputType([System.Collections.Hashtable])] + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $Name, + + [Parameter()] + [System.Boolean] + $SkipComponentBasedServicing, + + [Parameter()] + [System.Boolean] + $SkipWindowsUpdate, + + [Parameter()] + [System.Boolean] + $SkipPendingFileRename, + + [Parameter()] + [System.Boolean] + $SkipPendingComputerRename, + + [Parameter()] + [System.Boolean] + $SkipCcmClientSDK = $true + ) + + $pendingRebootState = Get-PendingRebootHashTable @PSBoundParameters + $rebootRequired = $false + + foreach ($rebootTrigger in $script:rebootTriggers) + { + $skipTriggerName = 'Skip{0}' -f $rebootTrigger.Name + $skipTrigger = $pendingRebootState.$skipTriggerName + + if ($skipTrigger) + { + Write-Verbose -Message ($script:localizedData.RebootRequiredButSkippedMessage -f $rebootTrigger.Description) + } + else + { + if ($pendingRebootState.$($rebootTrigger.Name)) + { + Write-Verbose -Message ($script:localizedData.RebootRequiredMessage -f $rebootTrigger.Description) + $rebootRequired = $true + } + else + { + Write-Verbose -Message ($script:localizedData.RebootNotRequiredMessage -f $rebootTrigger.Description) + } + } + } + + $pendingRebootState += @{ + RebootRequired = $rebootRequired + } + + return $pendingRebootState +} + +Export-ModuleMember -Function *-TargetResource diff --git a/deployment/dsc/azshcihost/ComputerManagementDsc/8.4.0/DSCResources/DSC_PendingReboot/DSC_PendingReboot.schema.mof b/deployment/dsc/azshcihost/ComputerManagementDsc/8.4.0/DSCResources/DSC_PendingReboot/DSC_PendingReboot.schema.mof new file mode 100644 index 0000000..f11e51f --- /dev/null +++ b/deployment/dsc/azshcihost/ComputerManagementDsc/8.4.0/DSCResources/DSC_PendingReboot/DSC_PendingReboot.schema.mof @@ -0,0 +1,16 @@ +[ClassVersion("1.0.0.0"), FriendlyName("PendingReboot")] +class DSC_PendingReboot : OMI_BaseResource +{ + [Key, Description("Specifies the name of this pending reboot check.")] String Name; + [Write, Description("Specifies whether to skip reboots triggered by the Component-Based Servicing component.")] Boolean SkipComponentBasedServicing; + [Read, Description("A value indicating whether the Component-Based Servicing component requested a reboot.")] Boolean ComponentBasedServicing; + [Write, Description("Specifies whether to skip reboots triggered by Windows Update.")] Boolean SkipWindowsUpdate; + [Read, Description("A value indicating whether Windows Update requested a reboot.")] Boolean WindowsUpdate; + [Write, Description("Specifies whether to skip pending file rename reboots.")] Boolean SkipPendingFileRename; + [Read, Description("A value indicating whether a pending file rename triggered a reboot.")] Boolean PendingFileRename; + [Write, Description("Specifies whether to skip reboots triggered by a pending computer rename.")] Boolean SkipPendingComputerRename; + [Read, Description("A value indicating whether a pending computer rename triggered a reboot.")] Boolean PendingComputerRename; + [Write, Description("Specifies whether to skip reboots triggered by the ConfigMgr client. Defaults to True.")] Boolean SkipCcmClientSDK; + [Read, Description("A value indicating whether the ConfigMgr client triggered a reboot.")] Boolean CcmClientSDK; + [Read, Description("A value indicating whether the node requires a reboot.")] Boolean RebootRequired; +}; diff --git a/deployment/dsc/azshcihost/ComputerManagementDsc/8.4.0/DSCResources/DSC_PendingReboot/README.md b/deployment/dsc/azshcihost/ComputerManagementDsc/8.4.0/DSCResources/DSC_PendingReboot/README.md new file mode 100644 index 0000000..d5b4e67 --- /dev/null +++ b/deployment/dsc/azshcihost/ComputerManagementDsc/8.4.0/DSCResources/DSC_PendingReboot/README.md @@ -0,0 +1,16 @@ +# Description + +PendingReboot examines three specific registry locations where a Windows Server +might indicate that a reboot is pending and allows DSC to predictably handle +the condition. + +DSC determines how to handle pending reboot conditions using the Local Configuration +Manager (LCM) setting `RebootNodeIfNeeded`. When DSC resources require reboot, within +a Set statement in a DSC Resource the global variable `DSCMachineStatus` is set to +value '1'. When this condition occurs and RebootNodeIfNeeded is set to 'True', +DSC reboots the machine after a successful Set. Otherwise, the reboot is postponed. + +Note: The expectation is that this resource will be used in conjunction with +knowledge of DSC Local Configuration Manager, which has the ability to manage +whether reboots happen automatically using the RebootIfNeeded parameter. For +more information on configuring the LCM, please reference [this TechNet article](https://technet.microsoft.com/en-us/library/dn249922.aspx). diff --git a/deployment/dsc/azshcihost/ComputerManagementDsc/8.4.0/DSCResources/DSC_PendingReboot/en-US/DSC_PendingReboot.data.psd1 b/deployment/dsc/azshcihost/ComputerManagementDsc/8.4.0/DSCResources/DSC_PendingReboot/en-US/DSC_PendingReboot.data.psd1 new file mode 100644 index 0000000..1736652 --- /dev/null +++ b/deployment/dsc/azshcihost/ComputerManagementDsc/8.4.0/DSCResources/DSC_PendingReboot/en-US/DSC_PendingReboot.data.psd1 @@ -0,0 +1,24 @@ +@{ + RebootTriggers = @( + @{ + Name = 'ComponentBasedServicing' + Description = 'Component based servicing' + }, + @{ + Name = 'WindowsUpdate' + Description = 'Windows Update' + }, + @{ + Name = 'PendingFileRename' + Description = 'Pending file rename' + }, + @{ + Name = 'PendingComputerRename' + Description = 'Pending computer rename' + }, + @{ + Name = 'CcmClientSDK' + Description = 'ConfigMgr' + } + ) +} diff --git a/deployment/dsc/azshcihost/ComputerManagementDsc/8.4.0/DSCResources/DSC_PendingReboot/en-US/DSC_PendingReboot.strings.psd1 b/deployment/dsc/azshcihost/ComputerManagementDsc/8.4.0/DSCResources/DSC_PendingReboot/en-US/DSC_PendingReboot.strings.psd1 new file mode 100644 index 0000000..9cc9d62 --- /dev/null +++ b/deployment/dsc/azshcihost/ComputerManagementDsc/8.4.0/DSCResources/DSC_PendingReboot/en-US/DSC_PendingReboot.strings.psd1 @@ -0,0 +1,9 @@ +ConvertFrom-StringData @' + GettingPendingRebootStateMessage = Getting the Pending Reboot State for '{0}'. (PR0001) + TestingPendingRebootStateMessage = Testing the Pending Reboot State for '{0}'. (PR0002) + RebootRequiredMessage = {0} reboot required. (PR0003) + RebootNotRequiredMessage = {0} reboot is not required. (PR0004) + RebootRequiredButSkippedMessage = {0} reboot required, but is skipped. (PR0005) + SettingPendingRebootStateMessage = Setting the Pending Reboot State for '{0}' to reboot required. (PR0006) + QueryCcmClientUtilitiesFailedMessage = Unable to query CIM Class CCM_ClientUtilities because '{0}'. (PR0007) +'@ diff --git a/deployment/dsc/azshcihost/ComputerManagementDsc/8.4.0/DSCResources/DSC_PowerPlan/DSC_PowerPlan.psm1 b/deployment/dsc/azshcihost/ComputerManagementDsc/8.4.0/DSCResources/DSC_PowerPlan/DSC_PowerPlan.psm1 new file mode 100644 index 0000000..1f8a711 --- /dev/null +++ b/deployment/dsc/azshcihost/ComputerManagementDsc/8.4.0/DSCResources/DSC_PowerPlan/DSC_PowerPlan.psm1 @@ -0,0 +1,158 @@ +$modulePath = Join-Path -Path (Split-Path -Path (Split-Path -Path $PSScriptRoot -Parent) -Parent) -ChildPath 'Modules' + +# Import the ComputerManagementDsc Common Modules +Import-Module -Name (Join-Path -Path $modulePath ` + -ChildPath (Join-Path -Path 'ComputerManagementDsc.Common' ` + -ChildPath 'ComputerManagementDsc.Common.psm1')) + +Import-Module -Name (Join-Path -Path $modulePath -ChildPath 'DscResource.Common') + +# Import Localization Strings +$script:localizedData = Get-LocalizedData -DefaultUICulture 'en-US' + +<# + .SYNOPSIS + Returns the current state of the power plan. + + .PARAMETER IsSingleInstance + Specifies the resource is a single instance, the value must be 'Yes'. + + .PARAMETER Name + Specifies the name or GUID of the power plan to assign to the node. + + .EXAMPLE + Get-TargetResource -IsSingleInstance 'Yes' -Name 'High performance' +#> +function Get-TargetResource +{ + [CmdletBinding()] + [OutputType([System.Collections.Hashtable])] + param + ( + # This is best practice when writing a single-instance DSC resource. + [Parameter(Mandatory = $true)] + [ValidateSet('Yes')] + [System.String] + $IsSingleInstance, + + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $Name + ) + + $desiredPowerPlan = Get-PowerPlan -PowerPlan $Name + + if ($desiredPowerPlan) + { + $activePowerPlan = Get-ActivePowerPlan + + if ($activePowerPlan -eq $desiredPowerPlan.Guid) + { + Write-Verbose -Message ($script:localizedData.PowerPlanIsActive -f $desiredPowerPlan.FriendlyName) + $isActive = $true + } + else + { + Write-Verbose -Message ($script:localizedData.PowerPlanIsNotActive -f $desiredPowerPlan.FriendlyName) + $isActive = $false + } + + return @{ + IsSingleInstance = $IsSingleInstance + Name = $Name + IsActive = $isActive + } + + } + else + { + New-InvalidOperationException ` + -Message ($script:localizedData.PowerPlanNotFound -f $Name) + } +} + +<# + .SYNOPSIS + Assign the power plan to the node. + + .PARAMETER IsSingleInstance + Specifies the resource is a single instance, the value must be 'Yes'. + + .PARAMETER Name + Specifies the name or GUID of the power plan to assign to the node. + + .EXAMPLE + Set-TargetResource -IsSingleInstance 'Yes' -Name 'High performance' +#> +function Set-TargetResource +{ + [CmdletBinding()] + param + ( + # This is best practice when writing a single-instance DSC resource. + [Parameter(Mandatory = $true)] + [ValidateSet('Yes')] + [System.String] + $IsSingleInstance, + + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $Name + ) + + Write-Verbose -Message ($script:localizedData.PowerPlanIsBeingActivated -f $Name) + + $desiredPowerPlan = Get-PowerPlan -PowerPlan $Name + + if ($desiredPowerPlan) + { + Set-ActivePowerPlan -PowerPlanGuid $desiredPowerPlan.Guid + } + else + { + New-InvalidOperationException ` + -Message ($script:localizedData.PowerPlanNotFound -f $Name) + } +} + +<# + .SYNOPSIS + Tests if the power plan is assigned to the node. + + .PARAMETER IsSingleInstance + Specifies the resource is a single instance, the value must be 'Yes'. + + .PARAMETER Name + Specifies the name or GUID of the power plan to assign to the node. + + .EXAMPLE + Test-TargetResource -IsSingleInstance 'Yes' -Name 'High performance' +#> +function Test-TargetResource +{ + [CmdletBinding()] + [OutputType([System.Boolean])] + param + ( + # This is best practice when writing a single-instance DSC resource. + [Parameter(Mandatory = $true)] + [ValidateSet('Yes')] + [System.String] + $IsSingleInstance, + + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $Name + ) + + Write-Verbose -Message ($script:localizedData.PowerPlanIsBeingValidated -f $Name) + + $getTargetResourceResult = Get-TargetResource -IsSingleInstance $IsSingleInstance -Name $Name + + return $getTargetResourceResult.IsActive +} + +Export-ModuleMember -Function *-TargetResource diff --git a/deployment/dsc/azshcihost/ComputerManagementDsc/8.4.0/DSCResources/DSC_PowerPlan/DSC_PowerPlan.schema.mof b/deployment/dsc/azshcihost/ComputerManagementDsc/8.4.0/DSCResources/DSC_PowerPlan/DSC_PowerPlan.schema.mof new file mode 100644 index 0000000..f8f0a62 --- /dev/null +++ b/deployment/dsc/azshcihost/ComputerManagementDsc/8.4.0/DSCResources/DSC_PowerPlan/DSC_PowerPlan.schema.mof @@ -0,0 +1,7 @@ +[ClassVersion("1.0.0.0"), FriendlyName("PowerPlan")] +class DSC_PowerPlan : OMI_BaseResource +{ + [Key, Description("Specifies the resource is a single instance, the value must be 'Yes'."), ValueMap{"Yes"}, Values{"Yes"}] String IsSingleInstance; + [Required, Description("The name or GUID of the power plan to activate.")] String Name; + [Read, Description("Determines if the power plan is active.")] Boolean IsActive; +}; diff --git a/deployment/dsc/azshcihost/ComputerManagementDsc/8.4.0/DSCResources/DSC_PowerPlan/README.md b/deployment/dsc/azshcihost/ComputerManagementDsc/8.4.0/DSCResources/DSC_PowerPlan/README.md new file mode 100644 index 0000000..f7497b5 --- /dev/null +++ b/deployment/dsc/azshcihost/ComputerManagementDsc/8.4.0/DSCResources/DSC_PowerPlan/README.md @@ -0,0 +1,3 @@ +# Description + +The resource allows specifying a power plan to activate. diff --git a/deployment/dsc/azshcihost/ComputerManagementDsc/8.4.0/DSCResources/DSC_PowerPlan/en-US/DSC_PowerPlan.schema.mfl b/deployment/dsc/azshcihost/ComputerManagementDsc/8.4.0/DSCResources/DSC_PowerPlan/en-US/DSC_PowerPlan.schema.mfl new file mode 100644 index 0000000..6cd3ff2 --- /dev/null +++ b/deployment/dsc/azshcihost/ComputerManagementDsc/8.4.0/DSCResources/DSC_PowerPlan/en-US/DSC_PowerPlan.schema.mfl @@ -0,0 +1,6 @@ +[Description("This resource is used to activate a power plan.") : Amended,AMENDMENT, LOCALE("MS_409")] +class DSC_PowerPlan : OMI_BaseResource +{ + [Key, Description("Specifies the resource is a single instance, the value must be 'Yes'.") : Amended] String IsSingleInstance; + [Description("The name or GUID of the power plan to activate.") : Amended] String Name; +}; diff --git a/deployment/dsc/azshcihost/ComputerManagementDsc/8.4.0/DSCResources/DSC_PowerPlan/en-US/DSC_PowerPlan.strings.psd1 b/deployment/dsc/azshcihost/ComputerManagementDsc/8.4.0/DSCResources/DSC_PowerPlan/en-US/DSC_PowerPlan.strings.psd1 new file mode 100644 index 0000000..137cba7 --- /dev/null +++ b/deployment/dsc/azshcihost/ComputerManagementDsc/8.4.0/DSCResources/DSC_PowerPlan/en-US/DSC_PowerPlan.strings.psd1 @@ -0,0 +1,9 @@ +# Localized resources for WindowsOptionalFeature + +ConvertFrom-StringData @' + PowerPlanIsActive = The power plan '{0}' is the active plan. + PowerPlanIsNotActive = The power plan '{0}' is not the active plan. + PowerPlanNotFound = Unable to find the power plan '{0}'. + PowerPlanIsBeingActivated = Activating power plan '{0}'. + PowerPlanIsBeingValidated = Validating power plan '{0}'. +'@ diff --git a/deployment/dsc/azshcihost/ComputerManagementDsc/8.4.0/DSCResources/DSC_PowerShellExecutionPolicy/DSC_PowerShellExecutionPolicy.psm1 b/deployment/dsc/azshcihost/ComputerManagementDsc/8.4.0/DSCResources/DSC_PowerShellExecutionPolicy/DSC_PowerShellExecutionPolicy.psm1 new file mode 100644 index 0000000..b858d2f --- /dev/null +++ b/deployment/dsc/azshcihost/ComputerManagementDsc/8.4.0/DSCResources/DSC_PowerShellExecutionPolicy/DSC_PowerShellExecutionPolicy.psm1 @@ -0,0 +1,138 @@ +$modulePath = Join-Path -Path (Split-Path -Path (Split-Path -Path $PSScriptRoot -Parent) -Parent) -ChildPath 'Modules' + +# Import the ComputerManagementDsc Common Modules +Import-Module -Name (Join-Path -Path $modulePath ` + -ChildPath (Join-Path -Path 'ComputerManagementDsc.Common' ` + -ChildPath 'ComputerManagementDsc.Common.psm1')) + +Import-Module -Name (Join-Path -Path $modulePath -ChildPath 'DscResource.Common') + +# Import Localization Strings +$script:localizedData = Get-LocalizedData -DefaultUICulture 'en-US' + +<# + .SYNOPSIS + Gets the current resource state. + + .PARAMETER ExecutionPolicy + Specifies the given Powershell Execution Policy + + .PARAMETER ExecutionPolicyScope + Specifies the given Powershell Execution Policy Scope +#> +function Get-TargetResource +{ + [CmdletBinding()] + [OutputType([System.Collections.Hashtable])] + param + ( + [Parameter(Mandatory = $true)] + [ValidateSet('CurrentUser','LocalMachine','MachinePolicy','Process','UserPolicy')] + [System.String] + $ExecutionPolicyScope, + + [Parameter(Mandatory = $true)] + [ValidateSet('Bypass','Restricted','AllSigned','RemoteSigned','Unrestricted')] + [System.String] + $ExecutionPolicy + ) + + Write-Verbose -Message ($script:localizedData.GettingPowerShellExecutionPolicy -f $ExecutionPolicyScope, $ExecutionPolicy) + + # Gets the execution policies for the current session. + $returnValue = @{ + ExecutionPolicyScope = $ExecutionPolicyScope + ExecutionPolicy = $(Get-ExecutionPolicy -Scope $ExecutionPolicyScope) + } + + return $returnValue +} + +<# + .SYNOPSIS + Sets the desired resource state. + + .PARAMETER ExecutionPolicy + Specifies the given Powershell Execution Policy + + .PARAMETER ExecutionPolicyScope + Specifies the given Powershell Execution Policy Scope +#> + +function Set-TargetResource +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [ValidateSet('CurrentUser','LocalMachine','MachinePolicy','Process','UserPolicy')] + [System.String] + $ExecutionPolicyScope, + + [Parameter(Mandatory = $true)] + [ValidateSet('Bypass','Restricted','AllSigned','RemoteSigned','Unrestricted')] + [System.String] + $ExecutionPolicy + ) + + Write-Verbose -Message ($script:localizedData.SettingPowerShellExecutionPolicy -f $ExecutionPolicyScope, $ExecutionPolicy) + + try + { + Set-ExecutionPolicy -ExecutionPolicy $ExecutionPolicy -Scope $ExecutionPolicyScope -Force -ErrorAction Stop + Write-Verbose -Message ($script:localizedData.UpdatePowershellExecutionPolicySuccess -f $ExecutionPolicyScope, $ExecutionPolicy) + } + catch + { + if ($_.FullyQualifiedErrorId -eq 'ExecutionPolicyOverride,Microsoft.PowerShell.Commands.SetExecutionPolicyCommand') + { + Write-Verbose -Message ($script:localizedData.UpdatePowershellExecutionPolicyFailed -f $ExecutionPolicyScope, $ExecutionPolicy) + } + else + { + throw + } + } +} + +<# + .SYNOPSIS + Tests if the current resource state matches the desired resource state. + + .PARAMETER ExecutionPolicy + Specifies the given Powershell Execution Policy + + .PARAMETER ExecutionPolicyScope + Specifies the given Powershell Execution Policy Scope +#> + +function Test-TargetResource +{ + [CmdletBinding()] + [OutputType([System.Boolean])] + param + ( + [Parameter(Mandatory = $true)] + [ValidateSet('CurrentUser','LocalMachine','MachinePolicy','Process','UserPolicy')] + [System.String] + $ExecutionPolicyScope, + + [Parameter(Mandatory = $true)] + [ValidateSet('Bypass','Restricted','AllSigned','RemoteSigned','Unrestricted')] + [System.String] + $ExecutionPolicy + ) + + Write-Verbose -Message ($script:localizedData.TestingPowerShellExecutionPolicy -f $ExecutionPolicyScope, $ExecutionPolicy) + + if ((Get-ExecutionPolicy -Scope $ExecutionPolicyScope) -eq $ExecutionPolicy) + { + return $true + } + else + { + return $false + } +} + +Export-ModuleMember -Function *-TargetResource diff --git a/deployment/dsc/azshcihost/ComputerManagementDsc/8.4.0/DSCResources/DSC_PowerShellExecutionPolicy/DSC_PowerShellExecutionPolicy.schema.mof b/deployment/dsc/azshcihost/ComputerManagementDsc/8.4.0/DSCResources/DSC_PowerShellExecutionPolicy/DSC_PowerShellExecutionPolicy.schema.mof new file mode 100644 index 0000000..f0a9cef --- /dev/null +++ b/deployment/dsc/azshcihost/ComputerManagementDsc/8.4.0/DSCResources/DSC_PowerShellExecutionPolicy/DSC_PowerShellExecutionPolicy.schema.mof @@ -0,0 +1,6 @@ +[ClassVersion("1.0.0.0"), FriendlyName("PowerShellExecutionPolicy")] +class DSC_PowerShellExecutionPolicy : OMI_BaseResource +{ + [Key, Description("Defines the scope for the preference of the Windows PowerShell execution policy."), ValueMap{"CurrentUser","LocalMachine","MachinePolicy","Process","UserPolicy"},Values{"CurrentUser","LocalMachine","MachinePolicy","Process","UserPolicy"}] String ExecutionPolicyScope; + [Required, Description("Changes the preference for the Windows PowerShell execution policy."), ValueMap{"Bypass","Restricted","AllSigned","RemoteSigned","Unrestricted"}, Values{"Bypass","Restricted","AllSigned","RemoteSigned","Unrestricted"}] String ExecutionPolicy; +}; diff --git a/deployment/dsc/azshcihost/ComputerManagementDsc/8.4.0/DSCResources/DSC_PowerShellExecutionPolicy/README.md b/deployment/dsc/azshcihost/ComputerManagementDsc/8.4.0/DSCResources/DSC_PowerShellExecutionPolicy/README.md new file mode 100644 index 0000000..b2341ad --- /dev/null +++ b/deployment/dsc/azshcihost/ComputerManagementDsc/8.4.0/DSCResources/DSC_PowerShellExecutionPolicy/README.md @@ -0,0 +1,4 @@ +# Description + +This resource allows configuration of the PowerShell execution +policy for different execution scopes. diff --git a/deployment/dsc/azshcihost/ComputerManagementDsc/8.4.0/DSCResources/DSC_PowerShellExecutionPolicy/en-US/DSC_PowerShellExecutionPolicy.strings.psd1 b/deployment/dsc/azshcihost/ComputerManagementDsc/8.4.0/DSCResources/DSC_PowerShellExecutionPolicy/en-US/DSC_PowerShellExecutionPolicy.strings.psd1 new file mode 100644 index 0000000..218bbb7 --- /dev/null +++ b/deployment/dsc/azshcihost/ComputerManagementDsc/8.4.0/DSCResources/DSC_PowerShellExecutionPolicy/en-US/DSC_PowerShellExecutionPolicy.strings.psd1 @@ -0,0 +1,8 @@ +# culture="en-US" +ConvertFrom-StringData -StringData @' + GettingPowerShellExecutionPolicy = The current execution policy for '{0}' is '{1}'. + SettingPowerShellExecutionPolicy = Setting the execution policy for '{0}' to '{1}'. + UpdatePowershellExecutionPolicySuccess = Updating PowerShell Execution policy for '{0}' to '{1}' successfully. + UpdatePowershellExecutionPolicyFailed = Updating PowerShell Execution policy for '{0}' to '{1}' failed. + TestingPowerShellExecutionPolicy = Testing the current execution policy for '{0}' is '{1}'. +'@ diff --git a/deployment/dsc/azshcihost/ComputerManagementDsc/8.4.0/DSCResources/DSC_RemoteDesktopAdmin/DSC_RemoteDesktopAdmin.psm1 b/deployment/dsc/azshcihost/ComputerManagementDsc/8.4.0/DSCResources/DSC_RemoteDesktopAdmin/DSC_RemoteDesktopAdmin.psm1 new file mode 100644 index 0000000..98534e8 --- /dev/null +++ b/deployment/dsc/azshcihost/ComputerManagementDsc/8.4.0/DSCResources/DSC_RemoteDesktopAdmin/DSC_RemoteDesktopAdmin.psm1 @@ -0,0 +1,184 @@ +$modulePath = Join-Path -Path (Split-Path -Path (Split-Path -Path $PSScriptRoot -Parent) -Parent) -ChildPath 'Modules' + +# Import the ComputerManagementDsc Common Modules +Import-Module -Name (Join-Path -Path $modulePath ` + -ChildPath (Join-Path -Path 'ComputerManagementDsc.Common' ` + -ChildPath 'ComputerManagementDsc.Common.psm1')) + +Import-Module -Name (Join-Path -Path $modulePath -ChildPath 'DscResource.Common') + +# Import Localization Strings +$script:localizedData = Get-LocalizedData -DefaultUICulture 'en-US' + +$script:tSRegistryKey = 'HKLM:\System\CurrentControlSet\Control\Terminal Server' +$script:winStationsRegistryKey = 'HKLM:\System\CurrentControlSet\Control\Terminal Server\WinStations\RDP-Tcp' + +<# + .SYNOPSIS + Returns the current Remote Desktop Admin Settings. + + .PARAMETER IsSingleInstance + Specifies the resource is a single instance, the value must be 'Yes'. +#> +function Get-TargetResource +{ + [CmdletBinding()] + [OutputType([System.Collections.Hashtable])] + param + ( + [Parameter(Mandatory = $true)] + [ValidateSet('Yes')] + [System.String] + $IsSingleInstance + ) + + Write-Verbose -Message $script:localizedData.GettingRemoteDesktopAdminSettingsMessage + $fDenyTSConnectionsRegistry = (Get-ItemProperty -Path $script:tSRegistryKey -Name 'fDenyTSConnections').fDenyTSConnections + $UserAuthenticationRegistry = (Get-ItemProperty -Path $script:winStationsRegistryKey -Name 'UserAuthentication').UserAuthentication + + if ($fDenyTSConnectionsRegistry -eq 0) + { + $ensure = 'Present' + } + else + { + $ensure = 'Absent' + } + + if ($UserAuthenticationRegistry -eq 1) + { + $userAuthentication = 'Secure' + } + else + { + $userAuthentication = 'NonSecure' + } + + $targetResource = @{ + IsSingleInstance = $IsSingleInstance + Ensure = $ensure + UserAuthentication = $userAuthentication + } + + return $targetResource +} + +<# + .SYNOPSIS + Tests the state of the Remote Desktop Admin Settings. + + .PARAMETER IsSingleInstance + Specifies the resource is a single instance, the value must be 'Yes'. + + .PARAMETER Ensure + Specifies whether Remote Desktop connections should be allowed (Present) or denied (Absent). + + .PARAMETER UserAuthentication + Specifies whether Remote Desktop connnections will require Network Level Authentication (Secure) + or not (NonSecure). +#> +function Test-TargetResource +{ + [CmdletBinding()] + [OutputType([System.Boolean])] + param + ( + [Parameter(Mandatory = $true)] + [ValidateSet('Yes')] + [String] + $IsSingleInstance, + + [Parameter()] + [ValidateSet('Present', 'Absent')] + [System.String] + $Ensure = 'Present', + + [Parameter()] + [ValidateSet('NonSecure', 'Secure')] + [System.String] + $UserAuthentication + ) + + $targetResource = Get-TargetResource -IsSingleInstance 'Yes' + $inDesiredState = $true + + if ($targetResource.Ensure -ne $Ensure) + { + Write-Verbose -Message ($script:localizedData.NotInDesiredStateMessage ` + -f $Ensure, $targetResource.Ensure) + + $inDesiredState = $false + } + + if ($PSBoundParameters.ContainsKey('UserAuthentication') -and $targetResource.UserAuthentication -ne $UserAuthentication) + { + Write-Verbose -Message ($script:localizedData.ParameterNeedsUpdateMessage ` + -f 'UserAuthentication', $UserAuthentication, $targetResource.UserAuthentication) + + $inDesiredState = $false + } + + return $inDesiredState +} + +<# + .SYNOPSIS + Sets the Remote Desktop Admin Settings. + + .PARAMETER IsSingleInstance + Specifies the resource is a single instance, the value must be 'Yes'. + + .PARAMETER Ensure + Specifies whether Remote Desktop connections should be allowed (Present) or denied (Absent). + + .PARAMETER UserAuthentication + Specifies whether Remote Desktop connnections will require Network Level Authentication (Secure) + or not (NonSecure). +#> +function Set-TargetResource +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [ValidateSet('Yes')] + [String] + $IsSingleInstance, + + [Parameter()] + [ValidateSet('Present', 'Absent')] + [System.String] + $Ensure = 'Present', + + [Parameter()] + [ValidateSet('NonSecure', 'Secure')] + [System.String] + $UserAuthentication + ) + + $fDenyTSConnectionsRegistry = @{ + Present = 0 + Absent = 1 + }[$Ensure] + + $UserAuthenticationRegistry = @{ + NonSecure = 0 + Secure = 1 + }[$UserAuthentication] + + $targetResource = Get-TargetResource -IsSingleInstance 'Yes' + + if ($Ensure -ne $targetResource.Ensure) + { + Write-Verbose -Message ($script:localizedData.SettingRemoteDesktopAdminMessage -f $Ensure) + Set-ItemProperty -Path $script:tSRegistryKey -Name "fDenyTSConnections" -Value $fDenyTSConnectionsRegistry + } + + if ($UserAuthentication -ne $targetResource.UserAuthentication) + { + Write-Verbose -Message ($script:localizedData.SettingUserAuthenticationMessage -f $UserAuthentication) + Set-ItemProperty -Path $script:winStationsRegistryKey -Name "UserAuthentication" -Value $UserAuthenticationRegistry + } +} + +Export-ModuleMember -Function *-TargetResource diff --git a/deployment/dsc/azshcihost/ComputerManagementDsc/8.4.0/DSCResources/DSC_RemoteDesktopAdmin/DSC_RemoteDesktopAdmin.schema.mof b/deployment/dsc/azshcihost/ComputerManagementDsc/8.4.0/DSCResources/DSC_RemoteDesktopAdmin/DSC_RemoteDesktopAdmin.schema.mof new file mode 100644 index 0000000..6b2bac1 --- /dev/null +++ b/deployment/dsc/azshcihost/ComputerManagementDsc/8.4.0/DSCResources/DSC_RemoteDesktopAdmin/DSC_RemoteDesktopAdmin.schema.mof @@ -0,0 +1,8 @@ + +[ClassVersion("1.0.0.0"), FriendlyName("RemoteDesktopAdmin")] +class DSC_RemoteDesktopAdmin : OMI_BaseResource +{ + [Key, Description("Specifies the resource is a single instance, the value must be 'Yes'"), ValueMap{"Yes"}, Values{"Yes"}] String IsSingleInstance; + [Write, Description("Determines whether or not the computer should accept remote desktop connections. Present sets the value to Enabled and Absent sets the value to Disabled."), ValueMap{"Present","Absent"}, Values{"Present","Absent"}] String Ensure; + [Write, Description("Setting this value to Secure configures the machine to require Network Level Authentication (NLA) for remote desktop connections."), ValueMap{"Secure","NonSecure"}, Values{"Secure","NonSecure"}] String UserAuthentication; +}; diff --git a/deployment/dsc/azshcihost/ComputerManagementDsc/8.4.0/DSCResources/DSC_RemoteDesktopAdmin/README.md b/deployment/dsc/azshcihost/ComputerManagementDsc/8.4.0/DSCResources/DSC_RemoteDesktopAdmin/README.md new file mode 100644 index 0000000..7de74ef --- /dev/null +++ b/deployment/dsc/azshcihost/ComputerManagementDsc/8.4.0/DSCResources/DSC_RemoteDesktopAdmin/README.md @@ -0,0 +1,5 @@ +# Description + +This resource will manage the remote desktop administration settings on a computer. +This includes whether remote desktop connections are allowed or denied and whether +network level authentication is required. diff --git a/deployment/dsc/azshcihost/ComputerManagementDsc/8.4.0/DSCResources/DSC_RemoteDesktopAdmin/en-US/DSC_RemoteDesktopAdmin.strings.psd1 b/deployment/dsc/azshcihost/ComputerManagementDsc/8.4.0/DSCResources/DSC_RemoteDesktopAdmin/en-US/DSC_RemoteDesktopAdmin.strings.psd1 new file mode 100644 index 0000000..7b4fd29 --- /dev/null +++ b/deployment/dsc/azshcihost/ComputerManagementDsc/8.4.0/DSCResources/DSC_RemoteDesktopAdmin/en-US/DSC_RemoteDesktopAdmin.strings.psd1 @@ -0,0 +1,8 @@ +# culture="en-US" +ConvertFrom-StringData @' + ParameterNeedsUpdateMessage = Remote Desktop Admin setting '{0}' is '{1}' but should be '{2}'. Change required. + NotInDesiredStateMessage = Remote Desktop Admin is set to '{0}' but should be'{1}'. Change required. + GettingRemoteDesktopAdminSettingsMessage = Getting Remote Desktop Admin settings. + SettingRemoteDesktopAdminMessage = Setting Remote Desktop Admin to '{0}'. + SettingUserAuthenticationMessage = Setting Remote Desktop Admin user authentication to '{0}'. +'@ diff --git a/deployment/dsc/azshcihost/ComputerManagementDsc/8.4.0/DSCResources/DSC_ScheduledTask/DSC_ScheduledTask.psm1 b/deployment/dsc/azshcihost/ComputerManagementDsc/8.4.0/DSCResources/DSC_ScheduledTask/DSC_ScheduledTask.psm1 new file mode 100644 index 0000000..79b366e --- /dev/null +++ b/deployment/dsc/azshcihost/ComputerManagementDsc/8.4.0/DSCResources/DSC_ScheduledTask/DSC_ScheduledTask.psm1 @@ -0,0 +1,1996 @@ +Add-Type -TypeDefinition @' +namespace ScheduledTask +{ + public enum DaysOfWeek + { + Sunday = 1, + Monday = 2, + Tuesday = 4, + Wednesday = 8, + Thursday = 16, + Friday = 32, + Saturday = 64 + } +} +'@ + +$modulePath = Join-Path -Path (Split-Path -Path (Split-Path -Path $PSScriptRoot -Parent) -Parent) -ChildPath 'Modules' + +# Import the ComputerManagementDsc Common Modules +Import-Module -Name (Join-Path -Path $modulePath ` + -ChildPath (Join-Path -Path 'ComputerManagementDsc.Common' ` + -ChildPath 'ComputerManagementDsc.Common.psm1')) + +Import-Module -Name (Join-Path -Path $modulePath -ChildPath 'DscResource.Common') + +# Import Localization Strings +$script:localizedData = Get-LocalizedData -DefaultUICulture 'en-US' + +<# + .SYNOPSIS + Gets the current state of the resource. + + .PARAMETER TaskName + The name of the task. + + .PARAMETER TaskPath + The path to the task - defaults to the root directory. +#> +function Get-TargetResource +{ + [CmdletBinding()] + [OutputType([System.Collections.Hashtable])] + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $TaskName, + + [Parameter()] + [System.String] + $TaskPath = '\' + ) + + $TaskPath = ConvertTo-NormalizedTaskPath -TaskPath $TaskPath + + Write-Verbose -Message ($script:localizedData.GetScheduledTaskMessage -f $TaskName, $TaskPath) + + return Get-CurrentResource @PSBoundParameters +} + +<# + .SYNOPSIS + Tests if the current resource state matches the desired resource state. + + .PARAMETER TaskName + The name of the task. + + .PARAMETER TaskPath + The path to the task - defaults to the root directory. + + .PARAMETER Description + The task description. + + .PARAMETER ActionExecutable + The path to the .exe for this task. + + .PARAMETER ActionArguments + The arguments to pass the executable. + + .PARAMETER ActionWorkingPath + The working path to specify for the executable. + + .PARAMETER ScheduleType + When should the task be executed. + + .PARAMETER RepeatInterval + How many units (minutes, hours, days) between each run of this task? + + .PARAMETER StartTime + The time of day this task should start at - defaults to 12:00 AM. Not valid for + AtLogon and AtStartup tasks. + + .PARAMETER SynchronizeAcrossTimeZone + Enable the scheduled task option to synchronize across time zones. This is enabled + by including the timezone offset in the scheduled task trigger. Defaults to false + which does not include the timezone offset. + + .PARAMETER Ensure + Present if the task should exist, Absent if it should be removed. + + .PARAMETER Enable + True if the task should be enabled, false if it should be disabled. + + .PARAMETER BuiltInAccount + Run the task as one of the built in service accounts. + When set ExecuteAsCredential will be ignored and LogonType will be set to 'ServiceAccount' + + .PARAMETER ExecuteAsCredential + The credential this task should execute as. If not specified defaults to running + as the local system account. Cannot be used in combination with ExecuteAsGMSA. + + .PARAMETER ExecuteAsGMSA + The gMSA (Group Managed Service Account) this task should execute as. Cannot be + used in combination with ExecuteAsCredential. + + .PARAMETER DaysInterval + Specifies the interval between the days in the schedule. An interval of 1 produces + a daily schedule. An interval of 2 produces an every-other day schedule. + + .PARAMETER RandomDelay + Specifies a random amount of time to delay the start time of the trigger. The + delay time is a random time between the time the task triggers and the time that + you specify in this setting. + + .PARAMETER RepetitionDuration + Specifies how long the repetition pattern repeats after the task starts. + + .PARAMETER DaysOfWeek + Specifies an array of the days of the week on which Task Scheduler runs the task. + + .PARAMETER WeeksInterval + Specifies the interval between the weeks in the schedule. An interval of 1 produces + a weekly schedule. An interval of 2 produces an every-other week schedule. + + .PARAMETER User + Specifies the identifier of the user for a trigger that starts a task when a + user logs on. + + .PARAMETER DisallowDemandStart + Indicates whether the task is prohibited to run on demand or not. Defaults + to $false. + + .PARAMETER DisallowHardTerminate + Indicates whether the task is prohibited to be terminated or not. Defaults + to $false. + + .PARAMETER Compatibility + The task compatibility level. Defaults to Vista. + + .PARAMETER AllowStartIfOnBatteries + Indicates whether the task should start if the machine is on batteries or not. + Defaults to $false. + + .PARAMETER Hidden + Indicates that the task is hidden in the Task Scheduler UI. + + .PARAMETER RunOnlyIfIdle + Indicates that Task Scheduler runs the task only when the computer is idle. + + .PARAMETER IdleWaitTimeout + Specifies the amount of time that Task Scheduler waits for an idle condition to occur. + + .PARAMETER NetworkName + Specifies the name of a network profile that Task Scheduler uses to determine + if the task can run. + The Task Scheduler UI uses this setting for display purposes. Specify a network + name if you specify the RunOnlyIfNetworkAvailable parameter. + + .PARAMETER DisallowStartOnRemoteAppSession + Indicates that the task does not start if the task is triggered to run in a Remote + Applications Integrated Locally (RAIL) session. + + .PARAMETER StartWhenAvailable + Indicates that Task Scheduler can start the task at any time after its scheduled + time has passed. + + .PARAMETER DontStopIfGoingOnBatteries + Indicates that the task does not stop if the computer switches to battery power. + + .PARAMETER WakeToRun + Indicates that Task Scheduler wakes the computer before it runs the task. + + .PARAMETER IdleDuration + Specifies the amount of time that the computer must be in an idle state before + Task Scheduler runs the task. + + .PARAMETER RestartOnIdle + Indicates that Task Scheduler restarts the task when the computer cycles into an + idle condition more than once. + + .PARAMETER DontStopOnIdleEnd + Indicates that Task Scheduler does not terminate the task if the idle condition + ends before the task is completed. + + .PARAMETER ExecutionTimeLimit + Specifies the amount of time that Task Scheduler is allowed to complete the task. + + .PARAMETER MultipleInstances + Specifies the policy that defines how Task Scheduler handles multiple instances + of the task. + + .PARAMETER Priority + Specifies the priority level of the task. Priority must be an integer from 0 (highest priority) + to 10 (lowest priority). The default value is 7. Priority levels 7 and 8 are + used for background tasks. Priority levels 4, 5, and 6 are used for interactive tasks. + + .PARAMETER RestartCount + Specifies the number of times that Task Scheduler attempts to restart the task. + + .PARAMETER RestartInterval + Specifies the amount of time that Task Scheduler attempts to restart the task. + + .PARAMETER RunOnlyIfNetworkAvailable + Indicates that Task Scheduler runs the task only when a network is available. Task + Scheduler uses the NetworkID parameter and NetworkName parameter that you specify + in this cmdlet to determine if the network is available.\ + + .PARAMETER RunLevel + Specifies the level of user rights that Task Scheduler uses to run the tasks that + are associated with the principal. Defaults to 'Limited'. + + .PARAMETER LogonType + Specifies the security logon method that Task Scheduler uses to run the tasks that + are associated with the principal. + + .PARAMETER EventSubscription + The event subscription in a string that can be parsed as valid XML. This parameter is only + valid in combination with the OnEvent Schedule Type. For the query schema please check: + https://docs.microsoft.com/en-us/windows/desktop/WES/queryschema-schema + + .PARAMETER Delay + The time to wait after an event based trigger was triggered. This parameter is only + valid in combination with the OnEvent Schedule Type. +#> +function Set-TargetResource +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $TaskName, + + [Parameter()] + [System.String] + $TaskPath = '\', + + [Parameter()] + [System.String] + $Description, + + [Parameter()] + [System.String] + $ActionExecutable, + + [Parameter()] + [System.String] + $ActionArguments, + + [Parameter()] + [System.String] + $ActionWorkingPath, + + [Parameter()] + [System.String] + [ValidateSet('Once', 'Daily', 'Weekly', 'AtStartup', 'AtLogOn', 'OnEvent')] + $ScheduleType, + + [Parameter()] + [System.String] + $RepeatInterval = '00:00:00', + + [Parameter()] + [System.DateTime] + $StartTime = [System.DateTime]::Today, + + [Parameter()] + [System.Boolean] + $SynchronizeAcrossTimeZone = $false, + + [Parameter()] + [System.String] + [ValidateSet('Present', 'Absent')] + $Ensure = 'Present', + + [Parameter()] + [System.Boolean] + $Enable = $true, + + [Parameter()] + [ValidateSet('SYSTEM', 'LOCAL SERVICE', 'NETWORK SERVICE')] + [System.String] + $BuiltInAccount, + + [Parameter()] + [System.Management.Automation.PSCredential] + $ExecuteAsCredential, + + [Parameter()] + [System.String] + $ExecuteAsGMSA, + + [Parameter()] + [System.UInt32] + $DaysInterval = 1, + + [Parameter()] + [System.String] + $RandomDelay = '00:00:00', + + [Parameter()] + [System.String] + $RepetitionDuration = '00:00:00', + + [Parameter()] + [System.String[]] + $DaysOfWeek, + + [Parameter()] + [System.UInt32] + $WeeksInterval = 1, + + [Parameter()] + [System.String] + $User, + + [Parameter()] + [System.Boolean] + $DisallowDemandStart = $false, + + [Parameter()] + [System.Boolean] + $DisallowHardTerminate = $false, + + [Parameter()] + [ValidateSet('AT', 'V1', 'Vista', 'Win7', 'Win8')] + [System.String] + $Compatibility = 'Vista', + + [Parameter()] + [System.Boolean] + $AllowStartIfOnBatteries = $false, + + [Parameter()] + [System.Boolean] + $Hidden = $false, + + [Parameter()] + [System.Boolean] + $RunOnlyIfIdle = $false, + + [Parameter()] + [System.String] + $IdleWaitTimeout = '02:00:00', + + [Parameter()] + [System.String] + $NetworkName, + + [Parameter()] + [System.Boolean] + $DisallowStartOnRemoteAppSession = $false, + + [Parameter()] + [System.Boolean] + $StartWhenAvailable = $false, + + [Parameter()] + [System.Boolean] + $DontStopIfGoingOnBatteries = $false, + + [Parameter()] + [System.Boolean] + $WakeToRun = $false, + + [Parameter()] + [System.String] + $IdleDuration = '01:00:00', + + [Parameter()] + [System.Boolean] + $RestartOnIdle = $false, + + [Parameter()] + [System.Boolean] + $DontStopOnIdleEnd = $false, + + [Parameter()] + [System.String] + $ExecutionTimeLimit = '08:00:00', + + [Parameter()] + [ValidateSet('IgnoreNew', 'Parallel', 'Queue', 'StopExisting')] + [System.String] + $MultipleInstances = 'Queue', + + [Parameter()] + [System.UInt32] + $Priority = 7, + + [Parameter()] + [System.UInt32] + $RestartCount = 0, + + [Parameter()] + [System.String] + $RestartInterval = '00:00:00', + + [Parameter()] + [System.Boolean] + $RunOnlyIfNetworkAvailable = $false, + + [Parameter()] + [ValidateSet('Limited', 'Highest')] + [System.String] + $RunLevel = 'Limited', + + [Parameter()] + [ValidateSet('Group', 'Interactive', 'InteractiveOrPassword', 'None', 'Password', 'S4U', 'ServiceAccount')] + [System.String] + $LogonType, + + [Parameter()] + [System.String] + $EventSubscription, + + [Parameter()] + [System.String] + $Delay = '00:00:00' + ) + + $TaskPath = ConvertTo-NormalizedTaskPath -TaskPath $TaskPath + + Write-Verbose -Message ($script:localizedData.SetScheduledTaskMessage -f $TaskName, $TaskPath) + + # Convert the strings containing time spans to TimeSpan Objects + [System.TimeSpan] $RepeatInterval = ConvertTo-TimeSpanFromTimeSpanString -TimeSpanString $RepeatInterval + [System.TimeSpan] $RandomDelay = ConvertTo-TimeSpanFromTimeSpanString -TimeSpanString $RandomDelay + [System.TimeSpan] $RepetitionDuration = ConvertTo-TimeSpanFromTimeSpanString -TimeSpanString $RepetitionDuration -AllowIndefinitely + [System.TimeSpan] $IdleWaitTimeout = ConvertTo-TimeSpanFromTimeSpanString -TimeSpanString $IdleWaitTimeout + [System.TimeSpan] $IdleDuration = ConvertTo-TimeSpanFromTimeSpanString -TimeSpanString $IdleDuration + [System.TimeSpan] $ExecutionTimeLimit = ConvertTo-TimeSpanFromTimeSpanString -TimeSpanString $ExecutionTimeLimit + [System.TimeSpan] $RestartInterval = ConvertTo-TimeSpanFromTimeSpanString -TimeSpanString $RestartInterval + + $currentValues = Get-CurrentResource -TaskName $TaskName -TaskPath $TaskPath + + if ($Ensure -eq 'Present') + { + <# + If the scheduled task already exists and is enabled but it needs to be disabled + and the action executable isn't specified then disable the task + #> + if ($currentValues.Ensure -eq 'Present' ` + -and $currentValues.Enable ` + -and -not $Enable ` + -and -not $PSBoundParameters.ContainsKey('ActionExecutable')) + { + Write-Verbose -Message ($script:localizedData.DisablingExistingScheduledTask -f $TaskName, $TaskPath) + Disable-ScheduledTask -TaskName $TaskName -TaskPath $TaskPath + + return + } + + if ($RepetitionDuration -lt $RepeatInterval) + { + New-InvalidArgumentException ` + -Message ($script:localizedData.RepetitionDurationLessThanIntervalError -f $RepetitionDuration, $RepeatInterval) ` + -ArgumentName RepeatInterval + } + + if ($ScheduleType -eq 'Daily' -and $DaysInterval -eq 0) + { + New-InvalidArgumentException ` + -Message ($script:localizedData.DaysIntervalError -f $DaysInterval) ` + -ArgumentName DaysInterval + } + + if ($ScheduleType -eq 'Weekly' -and $WeeksInterval -eq 0) + { + New-InvalidArgumentException ` + -Message ($script:localizedData.WeeksIntervalError -f $WeeksInterval) ` + -ArgumentName WeeksInterval + } + + if ($ScheduleType -eq 'Weekly' -and $DaysOfWeek.Count -eq 0) + { + New-InvalidArgumentException ` + -Message ($script:localizedData.WeekDayMissingError) ` + -ArgumentName DaysOfWeek + } + + if ($ScheduleType -eq 'OnEvent' -and -not ([xml]$EventSubscription)) + { + New-InvalidArgumentException ` + -Message ($script:localizedData.OnEventSubscriptionError) ` + -ArgumentName EventSubscription + } + + if ($ExecuteAsGMSA -and ($ExecuteAsCredential -or $BuiltInAccount)) + { + New-InvalidArgumentException ` + -Message ($script:localizedData.gMSAandCredentialError) ` + -ArgumentName ExecuteAsGMSA + } + + if ($SynchronizeAcrossTimeZone -and ($ScheduleType -notin @('Once', 'Daily', 'Weekly'))) + { + New-InvalidArgumentException ` + -Message ($script:localizedData.SynchronizeAcrossTimeZoneInvalidScheduleType) ` + -ArgumentName SynchronizeAcrossTimeZone + } + + # Configure the action + $actionParameters = @{ + Execute = $ActionExecutable + } + + if ($ActionArguments) + { + $actionParameters.Add('Argument', $ActionArguments) + } + + if ($ActionWorkingPath) + { + $actionParameters.Add('WorkingDirectory', $ActionWorkingPath) + } + + $action = New-ScheduledTaskAction @actionParameters + + $scheduledTaskArguments += @{ + Action = $action + } + + # Configure the settings + $settingParameters = @{ + DisallowDemandStart = $DisallowDemandStart + DisallowHardTerminate = $DisallowHardTerminate + Compatibility = $Compatibility + AllowStartIfOnBatteries = $AllowStartIfOnBatteries + Disable = -not $Enable + Hidden = $Hidden + RunOnlyIfIdle = $RunOnlyIfIdle + DisallowStartOnRemoteAppSession = $DisallowStartOnRemoteAppSession + StartWhenAvailable = $StartWhenAvailable + DontStopIfGoingOnBatteries = $DontStopIfGoingOnBatteries + WakeToRun = $WakeToRun + RestartOnIdle = $RestartOnIdle + DontStopOnIdleEnd = $DontStopOnIdleEnd + Priority = $Priority + RestartCount = $RestartCount + RunOnlyIfNetworkAvailable = $RunOnlyIfNetworkAvailable + } + + if ($MultipleInstances -ne 'StopExisting') + { + $settingParameters.Add('MultipleInstances', $MultipleInstances) + } + + if ($IdleDuration -gt [System.TimeSpan] '00:00:00') + { + $settingParameters.Add('IdleDuration', $IdleDuration) + } + + if ($IdleWaitTimeout -gt [System.TimeSpan] '00:00:00') + { + $settingParameters.Add('IdleWaitTimeout', $IdleWaitTimeout) + } + + if ($PSBoundParameters.ContainsKey('ExecutionTimeLimit')) + { + $settingParameters.Add('ExecutionTimeLimit', $ExecutionTimeLimit) + } + + if ($RestartInterval -gt [System.TimeSpan] '00:00:00') + { + $settingParameters.Add('RestartInterval', $RestartInterval) + } + + if (-not [System.String]::IsNullOrWhiteSpace($NetworkName)) + { + $settingParameters.Add('NetworkName', $NetworkName) + } + + $setting = New-ScheduledTaskSettingsSet @settingParameters + + <# The following workaround is needed because the TASK_INSTANCES_STOP_EXISTING value of + https://docs.microsoft.com/en-us/windows/win32/taskschd/tasksettings-multipleinstances is missing + from the Microsoft.PowerShell.Cmdletization.GeneratedTypes.ScheduledTask.MultipleInstancesEnum, + which is used for the other values of $MultipleInstances. (at least currently, as of June, 2020) + #> + if ($MultipleInstances -eq 'StopExisting') + { + $setting.CimInstanceProperties.Item('MultipleInstances').Value = 3 + } + + $scheduledTaskArguments += @{ + Settings = $setting + } + + <# + On Windows Server 2012 R2 setting a blank timespan for ExecutionTimeLimit + does not result in the PT0S timespan value being set. So set this + if it has not been set. + #> + if ($PSBoundParameters.ContainsKey('ExecutionTimeLimit') -and ` + [System.String]::IsNullOrEmpty($setting.ExecutionTimeLimit)) + { + $setting.ExecutionTimeLimit = 'PT0S' + } + + # Configure the trigger + $triggerParameters = @{} + + # A random delay is not supported when the scheduleType is set to OnEvent + if ($RandomDelay -gt [System.TimeSpan]::FromSeconds(0) -and $ScheduleType -ne 'OnEvent') + { + $triggerParameters.Add('RandomDelay', $RandomDelay) + } + + switch ($ScheduleType) + { + 'Once' + { + $triggerParameters.Add('Once', $true) + $triggerParameters.Add('At', $StartTime) + + break + } + + 'Daily' + { + $triggerParameters.Add('Daily', $true) + $triggerParameters.Add('At', $StartTime) + $triggerParameters.Add('DaysInterval', $DaysInterval) + + break + } + + 'Weekly' + { + $triggerParameters.Add('Weekly', $true) + $triggerParameters.Add('At', $StartTime) + + if ($DaysOfWeek.Count -gt 0) + { + $triggerParameters.Add('DaysOfWeek', $DaysOfWeek) + } + + if ($WeeksInterval -gt 0) + { + $triggerParameters.Add('WeeksInterval', $WeeksInterval) + } + + break + } + + 'AtStartup' + { + $triggerParameters.Add('AtStartup', $true) + + break + } + + 'AtLogOn' + { + $triggerParameters.Add('AtLogOn', $true) + + if (-not [System.String]::IsNullOrWhiteSpace($User) -and $LogonType -ne 'Group') + { + $triggerParameters.Add('User', $User) + } + + break + } + + 'OnEvent' + { + Write-Verbose -Message ($script:localizedData.ConfigureTaskEventTrigger -f $TaskName) + + $cimTriggerClass = Get-CimClass -ClassName MSFT_TaskEventTrigger -Namespace Root/Microsoft/Windows/TaskScheduler:MSFT_TaskEventTrigger + $trigger = New-CimInstance -CimClass $cimTriggerClass -ClientOnly + $trigger.Enabled = $true + $trigger.Subscription = $EventSubscription + } + } + + if ($ScheduleType -ne 'OnEvent') + { + $trigger = New-ScheduledTaskTrigger @triggerParameters -ErrorAction SilentlyContinue + } + + if (-not $trigger) + { + New-InvalidOperationException ` + -Message ($script:localizedData.TriggerCreationError) ` + -ErrorRecord $_ + } + + if ($RepeatInterval -gt [System.TimeSpan]::Parse('0:0:0')) + { + # A repetition pattern is required so create it and attach it to the trigger object + Write-Verbose -Message ($script:localizedData.ConfigureTriggerRepetitionMessage) + + if ($RepetitionDuration -le $RepeatInterval) + { + New-InvalidArgumentException ` + -Message ($script:localizedData.RepetitionIntervalError -f $RepeatInterval, $RepetitionDuration) ` + -ArgumentName RepetitionDuration + } + + $tempTriggerParameters = @{ + Once = $true + At = '6:6:6' + RepetitionInterval = $RepeatInterval + } + + Write-Verbose -Message ($script:localizedData.CreateRepetitionPatternMessage) + + switch ($trigger.GetType().FullName) + { + 'Microsoft.PowerShell.ScheduledJob.ScheduledJobTrigger' + { + # This is the type of trigger object returned in Windows Server 2012 R2/Windows 8.1 and below + Write-Verbose -Message ($script:localizedData.CreateTemporaryTaskMessage) + + $tempTriggerParameters.Add('RepetitionDuration', $RepetitionDuration) + + # Create a temporary trigger and task and copy the repetition CIM object from the temporary task + $tempTrigger = New-ScheduledTaskTrigger @tempTriggerParameters + $tempTask = New-ScheduledTask -Action $action -Trigger $tempTrigger + + # Store the repetition settings + $repetition = $tempTask.Triggers[0].Repetition + } + + 'Microsoft.Management.Infrastructure.CimInstance' + { + # This is the type of trigger object returned in Windows Server 2016/Windows 10 and above + Write-Verbose -Message ($script:localizedData.CreateTemporaryTriggerMessage) + + if ($RepetitionDuration -gt [System.TimeSpan]::Parse('0:0:0') -and $RepetitionDuration -lt [System.TimeSpan]::MaxValue) + { + $tempTriggerParameters.Add('RepetitionDuration', $RepetitionDuration) + } + + # Create a temporary trigger and copy the repetition CIM object from it to the actual trigger + $tempTrigger = New-ScheduledTaskTrigger @tempTriggerParameters + + # Store the repetition settings + $repetition = $tempTrigger.Repetition + } + + default + { + New-InvalidOperationException ` + -Message ($script:localizedData.TriggerUnexpectedTypeError -f $trigger.GetType().FullName) + } + } + } + + if ($trigger.GetType().FullName -eq 'Microsoft.Management.Infrastructure.CimInstance') + { + # On W2016+ / W10+ the Delay property is supported on the AtLogon, AtStartup and OnEvent trigger types + $triggerSupportsDelayProperty = @('AtLogon', 'AtStartup', 'OnEvent') + + if ($ScheduleType -in $triggerSupportsDelayProperty) + { + $trigger.Delay = [System.Xml.XmlConvert]::ToString([System.TimeSpan]$Delay) + } + } + + $scheduledTaskArguments += @{ + Trigger = $trigger + } + + # Prepare the register arguments + $registerArguments = @{} + $username = $null + + if ($PSBoundParameters.ContainsKey('BuiltInAccount')) + { + <# + The validateset on BuiltInAccount has already checked the + non-null value to be 'LOCAL SERVICE', 'NETWORK SERVICE' or + 'SYSTEM' + #> + $username = 'NT AUTHORITY\' + $BuiltInAccount + $registerArguments.Add('User', $username) + $LogonType = 'ServiceAccount' + } + elseif ($PSBoundParameters.ContainsKey('ExecuteAsGMSA')) + { + $username = $ExecuteAsGMSA + $LogonType = 'Password' + } + elseif ($PSBoundParameters.ContainsKey('ExecuteAsCredential')) + { + $username = $ExecuteAsCredential.UserName + + # If the LogonType is not specified then set it to password + if ([System.String]::IsNullOrEmpty($LogonType)) + { + $LogonType = 'Password' + } + + if ($LogonType -ne 'Group') + { + $registerArguments.Add('User', $username) + } + + if ($LogonType -notin ('Interactive', 'S4U', 'Group')) + { + # Only set the password if the LogonType is not interactive or S4U + $registerArguments.Add('Password', $ExecuteAsCredential.GetNetworkCredential().Password) + } + } + else + { + <# + 'NT AUTHORITY\SYSTEM' basically gives the schedule task admin + privileges, should we default to 'NT AUTHORITY\LOCAL SERVICE' + instead? + #> + $username = 'NT AUTHORITY\SYSTEM' + $registerArguments.Add('User', $username) + $LogonType = 'ServiceAccount' + } + + # Prepare the principal arguments + $principalArguments = @{ + Id = 'Author' + } + + if ($LogonType -eq 'Group') + { + $principalArguments.GroupId = $username + } + else + { + $principalArguments.LogonType = $LogonType + $principalArguments.UserId = $username + } + + # Set the Run Level if defined + if ($PSBoundParameters.ContainsKey('RunLevel')) + { + $principalArguments.Add('RunLevel', $RunLevel) + } + + # Create the principal object + Write-Verbose -Message ($script:localizedData.CreateScheduledTaskPrincipalMessage -f $username, $LogonType) + + $principal = New-ScheduledTaskPrincipal @principalArguments + + $scheduledTaskArguments += @{ + Principal = $principal + } + + $tempScheduledTask = New-ScheduledTask @scheduledTaskArguments -ErrorAction Stop + + if ($currentValues.Ensure -eq 'Present') + { + Write-Verbose -Message ($script:localizedData.RetrieveScheduledTaskMessage -f $TaskName, $TaskPath) + $tempScheduledTask = New-ScheduledTask @scheduledTaskArguments -ErrorAction Stop + + $scheduledTask = ScheduledTasks\Get-ScheduledTask ` + -TaskName $currentValues.TaskName ` + -TaskPath $currentValues.TaskPath ` + -ErrorAction Stop + $scheduledTask.Actions = $action + $scheduledTask.Triggers = $tempScheduledTask.Triggers + $scheduledTask.Settings = $setting + $scheduledTask.Principal = $principal + } + else + { + $scheduledTask = $tempScheduledTask + } + + Write-Verbose -Message ($script:localizedData.CreateNewScheduledTaskMessage -f $TaskName, $TaskPath) + + if ($repetition) + { + Write-Verbose -Message ($script:localizedData.SetRepetitionTriggerMessage -f $TaskName, $TaskPath) + + $scheduledTask.Triggers[0].Repetition = $repetition + } + + if (-not [System.String]::IsNullOrWhiteSpace($Description)) + { + $scheduledTask.Description = $Description + } + + if ($scheduledTask.Triggers[0].StartBoundary) + { + <# + The way New-ScheduledTaskTrigger writes the StartBoundary has issues because it does not take + the setting "Synchronize across time zones" in consideration. What happens if synchronize across + time zone is enabled in the scheduled task GUI is that the time is written like this: + + 2018-09-27T18:45:08+02:00 + + When the setting synchronize across time zones is disabled, the time is written as: + + 2018-09-27T18:45:08 + + The problem in New-ScheduledTaskTrigger is that it always writes the time the format that + includes the full timezone offset (W2016 behaviour, W2012R2 does it the other way around). + Which means "Synchronize across time zones" is enabled by default on W2016 and disabled by + default on W2012R2. To prevent that, we are overwriting the StartBoundary here to insert + the time in the format we want it, so we can enable or disable "Synchronize across time zones". + #> + + $scheduledTask.Triggers[0].StartBoundary = Get-DateTimeString -Date $StartTime -SynchronizeAcrossTimeZone $SynchronizeAcrossTimeZone + } + + if ($currentValues.Ensure -eq 'Present') + { + # Updating the scheduled task + + Write-Verbose -Message ($script:localizedData.UpdateScheduledTaskMessage -f $TaskName, $TaskPath) + $null = Set-ScheduledTask -InputObject $scheduledTask @registerArguments + } + else + { + Write-Verbose -Message ($script:localizedData.CreateNewScheduledTaskMessage -f $TaskName, $TaskPath) + + # Register the scheduled task + + $registerArguments.Add('TaskName', $TaskName) + $registerArguments.Add('TaskPath', $TaskPath) + $registerArguments.Add('InputObject', $scheduledTask) + + $null = Register-ScheduledTask @registerArguments + } + } + + if ($Ensure -eq 'Absent') + { + Write-Verbose -Message ($script:localizedData.RemoveScheduledTaskMessage -f $TaskName, $TaskPath) + + Unregister-ScheduledTask -TaskName $TaskName -TaskPath $TaskPath -Confirm:$false -ErrorAction Stop + } +} + +<# + .SYNOPSIS + Tests if the current resource state matches the desired resource state. + + .PARAMETER TaskName + The name of the task. + + .PARAMETER TaskPath + The path to the task - defaults to the root directory. + + .PARAMETER Description + The task description. + + .PARAMETER ActionExecutable + The path to the .exe for this task. + + .PARAMETER ActionArguments + The arguments to pass the executable. + + .PARAMETER ActionWorkingPath + The working path to specify for the executable. + + .PARAMETER ScheduleType + When should the task be executed. + + .PARAMETER RepeatInterval + How many units (minutes, hours, days) between each run of this task? + + .PARAMETER StartTime + The time of day this task should start at - defaults to 12:00 AM. Not valid for + AtLogon and AtStartup tasks. + + .PARAMETER SynchronizeAcrossTimeZone + Enable the scheduled task option to synchronize across time zones. This is enabled + by including the timezone offset in the scheduled task trigger. Defaults to false + which does not include the timezone offset. + + .PARAMETER Ensure + Present if the task should exist, Absent if it should be removed. + + .PARAMETER Enable + True if the task should be enabled, false if it should be disabled. + + .PARAMETER BuiltInAccount + Run the task as one of the built in service accounts. + When set ExecuteAsCredential will be ignored and LogonType will be set to 'ServiceAccount' + + .PARAMETER ExecuteAsCredential + The credential this task should execute as. If not specified defaults to running + as the local system account. Cannot be used in combination with ExecuteAsGMSA. + + .PARAMETER ExecuteAsGMSA + The gMSA (Group Managed Service Account) this task should execute as. Cannot be + used in combination with ExecuteAsCredential. + + .PARAMETER DaysInterval + Specifies the interval between the days in the schedule. An interval of 1 produces + a daily schedule. An interval of 2 produces an every-other day schedule. + + .PARAMETER RandomDelay + Specifies a random amount of time to delay the start time of the trigger. The + delay time is a random time between the time the task triggers and the time that + you specify in this setting. + + .PARAMETER RepetitionDuration + Specifies how long the repetition pattern repeats after the task starts. + + .PARAMETER DaysOfWeek + Specifies an array of the days of the week on which Task Scheduler runs the task. + + .PARAMETER WeeksInterval + Specifies the interval between the weeks in the schedule. An interval of 1 produces + a weekly schedule. An interval of 2 produces an every-other week schedule. + + .PARAMETER User + Specifies the identifier of the user for a trigger that starts a task when a + user logs on. + + .PARAMETER DisallowDemandStart + Indicates whether the task is prohibited to run on demand or not. Defaults + to $false. + + .PARAMETER DisallowHardTerminate + Indicates whether the task is prohibited to be terminated or not. Defaults + to $false. + + .PARAMETER Compatibility + The task compatibility level. Defaults to Vista. + + .PARAMETER AllowStartIfOnBatteries + Indicates whether the task should start if the machine is on batteries or not. + Defaults to $false. + + .PARAMETER Hidden + Indicates that the task is hidden in the Task Scheduler UI. + + .PARAMETER RunOnlyIfIdle + Indicates that Task Scheduler runs the task only when the computer is idle. + + .PARAMETER IdleWaitTimeout + Specifies the amount of time that Task Scheduler waits for an idle condition to occur. + + .PARAMETER NetworkName + Specifies the name of a network profile that Task Scheduler uses to determine + if the task can run. + The Task Scheduler UI uses this setting for display purposes. Specify a network + name if you specify the RunOnlyIfNetworkAvailable parameter. + + .PARAMETER DisallowStartOnRemoteAppSession + Indicates that the task does not start if the task is triggered to run in a Remote + Applications Integrated Locally (RAIL) session. + + .PARAMETER StartWhenAvailable + Indicates that Task Scheduler can start the task at any time after its scheduled + time has passed. + + .PARAMETER DontStopIfGoingOnBatteries + Indicates that the task does not stop if the computer switches to battery power. + + .PARAMETER WakeToRun + Indicates that Task Scheduler wakes the computer before it runs the task. + + .PARAMETER IdleDuration + Specifies the amount of time that the computer must be in an idle state before + Task Scheduler runs the task. + + .PARAMETER RestartOnIdle + Indicates that Task Scheduler restarts the task when the computer cycles into an + idle condition more than once. + + .PARAMETER DontStopOnIdleEnd + Indicates that Task Scheduler does not terminate the task if the idle condition + ends before the task is completed. + + .PARAMETER ExecutionTimeLimit + Specifies the amount of time that Task Scheduler is allowed to complete the task. + + .PARAMETER MultipleInstances + Specifies the policy that defines how Task Scheduler handles multiple instances + of the task. + + .PARAMETER Priority + Specifies the priority level of the task. Priority must be an integer from 0 (highest priority) + to 10 (lowest priority). The default value is 7. Priority levels 7 and 8 are + used for background tasks. Priority levels 4, 5, and 6 are used for interactive tasks. + + .PARAMETER RestartCount + Specifies the number of times that Task Scheduler attempts to restart the task. + + .PARAMETER RestartInterval + Specifies the amount of time that Task Scheduler attempts to restart the task. + + .PARAMETER RunOnlyIfNetworkAvailable + Indicates that Task Scheduler runs the task only when a network is available. Task + Scheduler uses the NetworkID parameter and NetworkName parameter that you specify + in this cmdlet to determine if the network is available. + + .PARAMETER RunLevel + Specifies the level of user rights that Task Scheduler uses to run the tasks that + are associated with the principal. Defaults to 'Limited'. + + .PARAMETER LogonType + Specifies the security logon method that Task Scheduler uses to run the tasks that + are associated with the principal. + + .PARAMETER EventSubscription + The event subscription in a string that can be parsed as valid XML. This parameter is only + valid in combination with the OnEvent Schedule Type. For the query schema please check: + https://docs.microsoft.com/en-us/windows/desktop/WES/queryschema-schema + + .PARAMETER Delay + The time to wait after an event based trigger was triggered. This parameter is only + valid in combination with the OnEvent Schedule Type. +#> +function Test-TargetResource +{ + [CmdletBinding()] + [OutputType([System.Boolean])] + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $TaskName, + + [Parameter()] + [System.String] + $TaskPath = '\', + + [Parameter()] + [System.String] + $Description, + + [Parameter()] + [System.String] + $ActionExecutable, + + [Parameter()] + [System.String] + $ActionArguments, + + [Parameter()] + [System.String] + $ActionWorkingPath, + + [Parameter()] + [System.String] + [ValidateSet('Once', 'Daily', 'Weekly', 'AtStartup', 'AtLogOn', 'OnEvent')] + $ScheduleType, + + [Parameter()] + [System.String] + $RepeatInterval = '00:00:00', + + [Parameter()] + [System.DateTime] + $StartTime = [System.DateTime]::Today, + + [Parameter()] + [System.Boolean] + $SynchronizeAcrossTimeZone = $false, + + [Parameter()] + [System.String] + [ValidateSet('Present', 'Absent')] + $Ensure = 'Present', + + [Parameter()] + [System.Boolean] + $Enable = $true, + + [Parameter()] + [ValidateSet('SYSTEM', 'LOCAL SERVICE', 'NETWORK SERVICE')] + [System.String] + $BuiltInAccount, + + [Parameter()] + [System.Management.Automation.PSCredential] + $ExecuteAsCredential, + + [Parameter()] + [System.String] + $ExecuteAsGMSA, + + [Parameter()] + [System.UInt32] + $DaysInterval = 1, + + [Parameter()] + [System.String] + $RandomDelay = '00:00:00', + + [Parameter()] + [System.String] + $RepetitionDuration = '00:00:00', + + [Parameter()] + [System.String[]] + $DaysOfWeek, + + [Parameter()] + [System.UInt32] + $WeeksInterval = 1, + + [Parameter()] + [System.String] + $User, + + [Parameter()] + [System.Boolean] + $DisallowDemandStart = $false, + + [Parameter()] + [System.Boolean] + $DisallowHardTerminate = $false, + + [Parameter()] + [ValidateSet('AT', 'V1', 'Vista', 'Win7', 'Win8')] + [System.String] + $Compatibility = 'Vista', + + [Parameter()] + [System.Boolean] + $AllowStartIfOnBatteries = $false, + + [Parameter()] + [System.Boolean] + $Hidden = $false, + + [Parameter()] + [System.Boolean] + $RunOnlyIfIdle = $false, + + [Parameter()] + [System.String] + $IdleWaitTimeout = '02:00:00', + + [Parameter()] + [System.String] + $NetworkName, + + [Parameter()] + [System.Boolean] + $DisallowStartOnRemoteAppSession = $false, + + [Parameter()] + [System.Boolean] + $StartWhenAvailable = $false, + + [Parameter()] + [System.Boolean] + $DontStopIfGoingOnBatteries = $false, + + [Parameter()] + [System.Boolean] + $WakeToRun = $false, + + [Parameter()] + [System.String] + $IdleDuration = '01:00:00', + + [Parameter()] + [System.Boolean] + $RestartOnIdle = $false, + + [Parameter()] + [System.Boolean] + $DontStopOnIdleEnd = $false, + + [Parameter()] + [System.String] + $ExecutionTimeLimit = '08:00:00', + + [Parameter()] + [ValidateSet('IgnoreNew', 'Parallel', 'Queue', 'StopExisting')] + [System.String] + $MultipleInstances = 'Queue', + + [Parameter()] + [System.UInt32] + $Priority = 7, + + [Parameter()] + [System.UInt32] + $RestartCount = 0, + + [Parameter()] + [System.String] + $RestartInterval = '00:00:00', + + [Parameter()] + [System.Boolean] + $RunOnlyIfNetworkAvailable = $false, + + [Parameter()] + [ValidateSet('Limited', 'Highest')] + [System.String] + $RunLevel = 'Limited', + + [Parameter()] + [ValidateSet('Group', 'Interactive', 'InteractiveOrPassword', 'None', 'Password', 'S4U', 'ServiceAccount')] + [System.String] + $LogonType, + + [Parameter()] + [System.String] + $EventSubscription, + + [Parameter()] + [System.String] + $Delay = '00:00:00' + ) + + $TaskPath = ConvertTo-NormalizedTaskPath -TaskPath $TaskPath + + Write-Verbose -Message ($script:localizedData.TestScheduledTaskMessage -f $TaskName, $TaskPath) + + $currentValues = Get-CurrentResource -TaskName $TaskName -TaskPath $TaskPath + + # Convert the strings containing time spans to TimeSpan Objects + if ($PSBoundParameters.ContainsKey('RepeatInterval')) + { + $PSBoundParameters['RepeatInterval'] = (ConvertTo-TimeSpanFromTimeSpanString -TimeSpanString $RepeatInterval).ToString() + } + + if ($PSBoundParameters.ContainsKey('RandomDelay')) + { + if ($ScheduleType -eq 'OnEvent') + { + # A random delay is not supported when the ScheduleType is set to OnEvent. + Write-Verbose -Message ($script:localizedData.IgnoreRandomDelayWithTriggerTypeOnEvent -f $TaskName) + $null = $PSBoundParameters.Remove('RandomDelay') + } + else + { + $PSBoundParameters['RandomDelay'] = (ConvertTo-TimeSpanFromTimeSpanString -TimeSpanString $RandomDelay).ToString() + } + } + + if ($PSBoundParameters.ContainsKey('RepetitionDuration')) + { + $RepetitionDuration = ConvertTo-TimeSpanFromTimeSpanString -TimeSpanString $RepetitionDuration -AllowIndefinitely + if ($RepetitionDuration -eq [System.TimeSpan]::MaxValue) + { + $PSBoundParameters['RepetitionDuration'] = 'Indefinitely' + } + else + { + $PSBoundParameters['RepetitionDuration'] = $RepetitionDuration.ToString() + } + } + + if ($PSBoundParameters.ContainsKey('IdleWaitTimeout')) + { + $PSBoundParameters['IdleWaitTimeout'] = (ConvertTo-TimeSpanFromTimeSpanString -TimeSpanString $IdleWaitTimeout).ToString() + } + + if ($PSBoundParameters.ContainsKey('IdleDuration')) + { + $PSBoundParameters['IdleDuration'] = (ConvertTo-TimeSpanFromTimeSpanString -TimeSpanString $IdleDuration).ToString() + } + + if ($PSBoundParameters.ContainsKey('ExecutionTimeLimit')) + { + $PSBoundParameters['ExecutionTimeLimit'] = (ConvertTo-TimeSpanFromTimeSpanString -TimeSpanString $ExecutionTimeLimit).ToString() + } + + if ($PSBoundParameters.ContainsKey('RestartInterval')) + { + $PSBoundParameters['RestartInterval'] = (ConvertTo-TimeSpanFromTimeSpanString -TimeSpanString $RestartInterval).ToString() + } + + if ($ScheduleType -in @('Once', 'Daily', 'Weekly')) + { + $PSBoundParameters['StartTime'] = Get-DateTimeString -Date $StartTime -SynchronizeAcrossTimeZone $SynchronizeAcrossTimeZone + <# + If the current StartTime is null then we need to set it to + the desired StartTime (which defaults to Today if not passed) + so that the test does not fail. + #> + if ($currentValues['StartTime']) + { + $currentValues['StartTime'] = Get-DateTimeString ` + -Date $currentValues['StartTime'] ` + -SynchronizeAcrossTimeZone $currentValues['SynchronizeAcrossTimeZone'] + } + else + { + $currentValues['StartTime'] = Get-DateTimeString ` + -Date $StartTime ` + -SynchronizeAcrossTimeZone $SynchronizeAcrossTimeZone + } + } + else + { + # Do not compare StartTime for triggers that aren't Once, Daily or Weekly. + $null = $PSBoundParameters.Remove('StartTime') + $null = $currentValues.Remove('StartTime') + } + + if ($Ensure -eq 'Absent' -and $currentValues.Ensure -eq 'Absent') + { + return $true + } + + if ($null -eq $currentValues) + { + Write-Verbose -Message ($script:localizedData.CurrentTaskValuesNullMessage) + + return $false + } + + if ($PSBoundParameters.ContainsKey('BuiltInAccount')) + { + $PSBoundParameters.User = $BuiltInAccount + $currentValues.User = $BuiltInAccount + + $PSBoundParameters.ExecuteAsCredential = $BuiltInAccount + $currentValues.ExecuteAsCredential = $BuiltInAccount + + $PSBoundParameters['LogonType'] = 'ServiceAccount' + $currentValues['LogonType'] = 'ServiceAccount' + + $PSBoundParameters['BuiltInAccount'] = 'NT AUTHORITY\' + $BuiltInAccount + } + elseif ($PSBoundParameters.ContainsKey('ExecuteAsCredential')) + { + # The password of the execution credential can not be compared + $username = $ExecuteAsCredential.UserName + $PSBoundParameters['ExecuteAsCredential'] = $username + } + else + { + # Must be running as System, login type is ServiceAccount + $PSBoundParameters['LogonType'] = 'ServiceAccount' + $currentValues['LogonType'] = 'ServiceAccount' + } + + if ($PSBoundParameters.ContainsKey('WeeksInterval') ` + -and ((-not $currentValues.ContainsKey('WeeksInterval')) -or ($null -eq $currentValues['WeeksInterval']))) + { + <# + The WeeksInterval parameter of this function defaults to 1, + even though the value of the WeeksInterval property maybe + unset/undefined in the object $currentValues returned from + Get-TargetResouce. To avoid Test-TargetResouce returning false + and generating spurious calls to Set-TargetResouce, default + an undefined $currentValues.WeeksInterval to the value of + $WeeksInterval. + #> + $currentValues.WeeksInterval = $PSBoundParameters['WeeksInterval'] + } + + if ($PSBoundParameters.ContainsKey('ExecuteAsGMSA')) + { + <# + There is a difference in W2012R2 and W2016 behaviour, + W2012R2 returns the gMSA including the DOMAIN prefix, + W2016 returns this without. So to be sure strip off the + domain part in Get & Test. This means we either need to + remove everything before \ in the case of the DOMAIN\User + format, or we need to remove everything after @ in case + when the UPN format (User@domain.fqdn) is used. + #> + $PSBoundParameters['ExecuteAsGMSA'] = $PSBoundParameters.ExecuteAsGMSA -replace '^.+\\|@.+', $null + } + + if ($PSBoundParameters.ContainsKey('Description')) + { + <# + All forms of whitespace is automatically trimmed from the description + when it is set, so we must not compare it here. See issue #258: + https://github.com/dsccommunity/ComputerManagementDsc/issues/258 + #> + $PSBoundParameters['Description'] = $PSBoundParameters.Description.Trim() + } + + $desiredValues = $PSBoundParameters + $desiredValues.TaskPath = $TaskPath + + if ($desiredValues.ContainsKey('Verbose')) + { + <# + Initialise a missing or null Verbose to avoid spurious + calls to Set-TargetResouce + #> + $currentValues.Add('Verbose', $desiredValues['Verbose']) + } + + Write-Verbose -Message ($script:localizedData.TestingDscParameterStateMessage) + + return Test-DscParameterState ` + -CurrentValues $currentValues ` + -DesiredValues $desiredValues ` + -Verbose:$VerbosePreference +} + +<# + .SYNOPSIS + Helper function to convert TaskPath to the right form + + .PARAMETER TaskPath + The path to the task +#> +function ConvertTo-NormalizedTaskPath +{ + [CmdletBinding()] + [OutputType([System.String])] + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $TaskPath + ) + + $pathArray = $TaskPath.Split('\').Where( {$_}) + if ($pathArray.Count -gt 0) + { + $TaskPath = "\$($pathArray -join '\')\" + } + + return $TaskPath +} + +<# + .SYNOPSIS + Helper function convert a standard timespan string + into a TimeSpan object. It can support returning the + maximum timespan if the AllowIndefinitely switch is set + and the timespan is set to 'indefinte'. + + .PARAMETER TimeSpan + The standard timespan string to convert to a TimeSpan + object. + + .PARAMETER AllowIndefinitely + Allow the keyword 'Indefinitely' to be translated into + the maximum valid timespan. +#> +function ConvertTo-TimeSpanFromTimeSpanString +{ + [CmdletBinding()] + [OutputType([System.TimeSpan])] + param + ( + [Parameter()] + [System.String] + $TimeSpanString = '00:00:00', + + [Parameter()] + [Switch] + $AllowIndefinitely + ) + + if ($AllowIndefinitely -eq $True -and $TimeSpanString -eq 'Indefinitely') + { + return [System.TimeSpan]::MaxValue + } + + return [System.TimeSpan]::Parse($TimeSpanString) +} + +<# + .SYNOPSIS + Helper function convert a task schedule timespan string + into a TimeSpan string. If AllowIndefinitely is set to + true and the TimeSpan string is empty then return + 'Indefinitely'. + + .PARAMETER TimeSpan + The scheduled task timespan string to convert to a TimeSpan + string. + + .PARAMETER AllowIndefinitely + Allow an empty TimeSpan to return the keyword 'Indefinitely'. + +#> +function ConvertTo-TimeSpanStringFromScheduledTaskString +{ + [CmdletBinding()] + [OutputType([System.String])] + param + ( + [Parameter()] + [System.String] + $TimeSpan, + + [Parameter()] + [Switch] + $AllowIndefinitely + ) + + # If AllowIndefinitely is true and the timespan is empty then return Indefinitely + if ($AllowIndefinitely -eq $true -and [System.String]::IsNullOrEmpty($TimeSpan)) + { + return 'Indefinitely' + } + + $days = $hours = $minutes = $seconds = 0 + + if ($TimeSpan -match 'P(?\d{0,3})D') + { + $days = $matches.Days + } + + if ($TimeSpan -match '(?\d{0,2})H') + { + $hours = $matches.Hours + } + + if ($TimeSpan -match '(?\d{0,2})M') + { + $minutes = $matches.Minutes + } + + if ($TimeSpan -match '(?\d{0,2})S') + { + $seconds = $matches.Seconds + } + + return (New-TimeSpan -Days $days -Hours $hours -Minutes $minutes -Seconds $seconds).ToString() +} + +<# + .SYNOPSIS + Helper function to disable an existing scheduled task. + + .PARAMETER TaskName + The name of the task to disable. + + .PARAMETER TaskPath + The path to the task to disable. +#> +function Disable-ScheduledTask +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $TaskName, + + [Parameter()] + [System.String] + $TaskPath = '\' + ) + + $existingTask = ScheduledTasks\Get-ScheduledTask @PSBoundParameters + $existingTask.Settings.Enabled = $false + $null = $existingTask | Register-ScheduledTask @PSBoundParameters -Force +} + +<# + .SYNOPSIS + Returns a formatted datetime string for use in ScheduledTask resource. + + .PARAMETER Date + The date to format. + + .PARAMETER SynchronizeAcrossTimeZone + Boolean to specifiy if the returned string is formatted in synchronize + across time zone format. +#> +function Get-DateTimeString +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [System.DateTime] + $Date, + + [Parameter(Mandatory = $true)] + [System.Boolean] + $SynchronizeAcrossTimeZone + ) + + $format = (Get-Culture).DateTimeFormat.SortableDateTimePattern + + if ($SynchronizeAcrossTimeZone) + { + $returnDate = (Get-Date -Date $Date -Format $format) + (Get-Date -Format 'zzz') + } + else + { + $returnDate = Get-Date -Date $Date -Format $format + } + + return $returnDate +} + +<# + .SYNOPSIS + Returns the current values of the resource. + + .PARAMETER TaskName + The name of the task. + + .PARAMETER TaskPath + The path to the task - defaults to the root directory. +#> +function Get-CurrentResource +{ + [CmdletBinding()] + [OutputType([System.Collections.Hashtable])] + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $TaskName, + + [Parameter()] + [System.String] + $TaskPath = '\' + ) + + $TaskPath = ConvertTo-NormalizedTaskPath -TaskPath $TaskPath + + Write-Verbose -Message ($script:localizedData.GettingCurrentTaskValuesMessage -f $TaskName, $TaskPath) + + $task = ScheduledTasks\Get-ScheduledTask -TaskName $TaskName -TaskPath $TaskPath -ErrorAction SilentlyContinue + + if ($null -eq $task) + { + Write-Verbose -Message ($script:localizedData.TaskNotFoundMessage -f $TaskName, $TaskPath) + + $result = @{ + TaskName = $TaskName + TaskPath = $TaskPath + Ensure = 'Absent' + } + } + else + { + Write-Verbose -Message ($script:localizedData.TaskFoundMessage -f $TaskName, $TaskPath) + + $action = $task.Actions | Select-Object -First 1 + $trigger = $task.Triggers | Select-Object -First 1 + $settings = $task.Settings + $returnScheduleType = 'Unknown' + + switch ($trigger.CimClass.CimClassName) + { + 'MSFT_TaskTimeTrigger' + { + $returnScheduleType = 'Once' + break + } + + 'MSFT_TaskDailyTrigger' + { + $returnScheduleType = 'Daily' + break + } + + 'MSFT_TaskWeeklyTrigger' + { + $returnScheduleType = 'Weekly' + break + } + + 'MSFT_TaskBootTrigger' + { + $returnScheduleType = 'AtStartup' + break + } + + 'MSFT_TaskLogonTrigger' + { + $returnScheduleType = 'AtLogon' + break + } + + 'MSFT_TaskEventTrigger' + { + $returnScheduleType = 'OnEvent' + break + } + + default + { + $returnScheduleType = '' + Write-Verbose -Message ($script:localizedData.TriggerTypeUnknown -f $trigger.CimClass.CimClassName) + } + } + + Write-Verbose -Message ($script:localizedData.DetectedScheduleTypeMessage -f $returnScheduleType) + + $daysOfWeek = @() + + foreach ($binaryAdductor in 1, 2, 4, 8, 16, 32, 64) + { + $day = $trigger.DaysOfWeek -band $binaryAdductor + + if ($day -ne 0) + { + $daysOfWeek += [System.String][ScheduledTask.DaysOfWeek] $day + } + } + + $startAt = $trigger.StartBoundary + + if ($startAt) + { + $synchronizeAcrossTimeZone = Test-DateStringContainsTimeZone -DateString $startAt + $startTime = [System.DateTime] $startAt + } + else + { + $startTime = $null + $synchronizeAcrossTimeZone = $false + } + + if ($task.Principal.LogonType -ieq 'Group') + { + $PrincipalId = 'GroupId' + } + else + { + $PrincipalId = 'UserId' + } + + <# The following workaround is needed because Get-StartedTask currently returns NULL for the value + of $settings.MultipleInstances when the started task is set to "Stop the existing instance". + https://windowsserver.uservoice.com/forums/301869-powershell/suggestions/40685125-bug-get-scheduledtask-returns-null-for-value-of-m + #> + $MultipleInstances = [System.String] $settings.MultipleInstances + if ([System.String]::IsNullOrEmpty($MultipleInstances)) + { + if ($task.settings.CimInstanceProperties.Item('MultipleInstances').Value -eq 3) + { + $MultipleInstances = 'StopExisting' + } + } + + $result = @{ + TaskName = $task.TaskName + TaskPath = $task.TaskPath + StartTime = $startTime + SynchronizeAcrossTimeZone = $synchronizeAcrossTimeZone + Ensure = 'Present' + Description = $task.Description + ActionExecutable = $action.Execute + ActionArguments = $action.Arguments + ActionWorkingPath = $action.WorkingDirectory + ScheduleType = $returnScheduleType + RepeatInterval = ConvertTo-TimeSpanStringFromScheduledTaskString -TimeSpan $trigger.Repetition.Interval + ExecuteAsCredential = $task.Principal.$PrincipalId + ExecuteAsGMSA = $task.Principal.UserId -replace '^.+\\|@.+', $null + Enable = $settings.Enabled + DaysInterval = [System.Uint32] $trigger.DaysInterval + RandomDelay = ConvertTo-TimeSpanStringFromScheduledTaskString -TimeSpan $trigger.RandomDelay + RepetitionDuration = ConvertTo-TimeSpanStringFromScheduledTaskString -TimeSpan $trigger.Repetition.Duration -AllowIndefinitely + DaysOfWeek = [System.String[]] $daysOfWeek + WeeksInterval = [System.Uint32] $trigger.WeeksInterval + User = $task.Principal.UserId + DisallowDemandStart = -not $settings.AllowDemandStart + DisallowHardTerminate = -not $settings.AllowHardTerminate + Compatibility = [System.String] $settings.Compatibility + AllowStartIfOnBatteries = -not $settings.DisallowStartIfOnBatteries + Hidden = $settings.Hidden + RunOnlyIfIdle = $settings.RunOnlyIfIdle + IdleWaitTimeout = ConvertTo-TimeSpanStringFromScheduledTaskString -TimeSpan $settings.IdleSettings.WaitTimeout + NetworkName = $settings.NetworkSettings.Name + DisallowStartOnRemoteAppSession = $settings.DisallowStartOnRemoteAppSession + StartWhenAvailable = $settings.StartWhenAvailable + DontStopIfGoingOnBatteries = -not $settings.StopIfGoingOnBatteries + WakeToRun = $settings.WakeToRun + IdleDuration = ConvertTo-TimeSpanStringFromScheduledTaskString -TimeSpan $settings.IdleSettings.IdleDuration + RestartOnIdle = $settings.IdleSettings.RestartOnIdle + DontStopOnIdleEnd = -not $settings.IdleSettings.StopOnIdleEnd + ExecutionTimeLimit = ConvertTo-TimeSpanStringFromScheduledTaskString -TimeSpan $settings.ExecutionTimeLimit + MultipleInstances = $MultipleInstances + Priority = $settings.Priority + RestartCount = $settings.RestartCount + RestartInterval = ConvertTo-TimeSpanStringFromScheduledTaskString -TimeSpan $settings.RestartInterval + RunOnlyIfNetworkAvailable = $settings.RunOnlyIfNetworkAvailable + RunLevel = [System.String] $task.Principal.RunLevel + LogonType = [System.String] $task.Principal.LogonType + EventSubscription = $trigger.Subscription + Delay = ConvertTo-TimeSpanStringFromScheduledTaskString -TimeSpan $trigger.Delay + } + + if (($result.ContainsKey('LogonType')) -and ($result['LogonType'] -ieq 'ServiceAccount')) + { + $builtInAccount = Set-DomainNameInAccountName -AccountName $task.Principal.UserId -DomainName 'NT AUTHORITY' + $result.Add('BuiltInAccount', $builtInAccount) + } + } + + Write-Verbose -Message ($script:localizedData.CurrentTaskValuesRetrievedMessage -f $TaskName, $TaskPath) + + return $result +} + +<# + .SYNOPSIS + Test if a date string contains a time zone. + + .DESCRIPTION + This function returns true if the string contains a time + zone appended to it. This is used to determine if the + SynchronizeAcrossTimeZone parameter has been set in a + trigger. + + .PARAMETER DateString + The date string to test. +#> +function Test-DateStringContainsTimeZone +{ + [CmdletBinding()] + [OutputType([System.Boolean])] + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $DateString + ) + + return $DateString.Contains('+') +} + +<# + .SYNOPSIS + Set domain name in a down-level user or group name. + + .DESCRIPTION + Set the domain name in a down-level user or group name. + + .PARAMETER AccountName + The user or group name to set the domain name in. + + .PARAMETER DomainName + If the AccountName does not contain a domain name them prefix + it with this value. If the AccountName already contains a domain + name then it will only be updated if the Force switch is set. + + .PARAMETER Force + If the identity already contains a domain prefix then force + it to the value in Domain. + + .EXAMPLE + Set-DomainNameInAccountName -AccountName 'Users' -DomainName 'NT AUTHORITY' + + Returns 'NT AUTHORITY\Users'. + + .EXAMPLE + Set-DomainNameInAccountName -AccountName 'MyDomain\Users' -DomainName 'NT AUTHORITY' + + Returns 'MyDomain\Users'. + + .EXAMPLE + Set-DomainNameInAccountName -AccountName 'MyDomain\Users' -DomainName 'NT AUTHORITY' -Force + + Returns 'NT AUTHORITY\Users'. +#> +function Set-DomainNameInAccountName +{ + [CmdletBinding()] + [OutputType([System.String])] + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $AccountName, + + [Parameter(Mandatory = $true)] + [System.String] + $DomainName, + + [Parameter()] + [Switch] + $Force + ) + + if ($AccountName.Contains('\')) + { + $existingDomainName, $name = ($AccountName -Split '\\') + + if (-not [System.String]::IsNullOrEmpty($existingDomainName) -and -not $force.IsPresent) + { + # Keep the existing domain name if it is set and force is not specified + $DomainName = $existingDomainName + } + } + else + { + $name = $AccountName + } + + return "$DomainName\$name" +} diff --git a/deployment/dsc/azshcihost/ComputerManagementDsc/8.4.0/DSCResources/DSC_ScheduledTask/DSC_ScheduledTask.schema.mof b/deployment/dsc/azshcihost/ComputerManagementDsc/8.4.0/DSCResources/DSC_ScheduledTask/DSC_ScheduledTask.schema.mof new file mode 100644 index 0000000..941b833 --- /dev/null +++ b/deployment/dsc/azshcihost/ComputerManagementDsc/8.4.0/DSCResources/DSC_ScheduledTask/DSC_ScheduledTask.schema.mof @@ -0,0 +1,50 @@ +[ClassVersion("1.0.0.0"), FriendlyName("ScheduledTask")] +class DSC_ScheduledTask : OMI_BaseResource +{ + [Key, Description("The name of the task.")] string TaskName; + [Write, Description("The path to the task - defaults to the root directory.")] string TaskPath; + [Write, Description("The task description.")] string Description; + [Write, Description("The path to the .exe for this task.")] string ActionExecutable; + [Write, Description("The arguments to pass the executable.")] string ActionArguments; + [Write, Description("The working path to specify for the executable.")] string ActionWorkingPath; + [Write, Description("When should the task be executed."), ValueMap{"Once", "Daily", "Weekly", "AtStartup", "AtLogOn", "OnEvent"}, Values{"Once", "Daily", "Weekly", "AtStartup", "AtLogOn", "OnEvent"}] string ScheduleType; + [Write, Description("How many units (minutes, hours, days) between each run of this task?")] String RepeatInterval; + [Write, Description("The time of day this task should start at - defaults to 12:00 AM. Not valid for AtLogon and AtStartup tasks.")] DateTime StartTime; + [Write, Description("Enable the scheduled task option to synchronize across time zones. This is enabled by including the timezone offset in the scheduled task trigger. Defaults to false which does not include the timezone offset.")] boolean SynchronizeAcrossTimeZone; + [Write, Description("Present if the task should exist, Absent if it should be removed."), ValueMap{"Present","Absent"}, Values{"Present","Absent"}] string Ensure; + [Write, Description("True if the task should be enabled, false if it should be disabled.")] boolean Enable; + [Write, Description("Run the task as one of the built in service accounts. When set ExecuteAsCredential will be ignored and LogonType will be set to 'ServiceAccount'."), ValueMap{"SYSTEM", "LOCAL SERVICE", "NETWORK SERVICE"}, Values{"SYSTEM", "LOCAL SERVICE", "NETWORK SERVICE"}] string BuiltInAccount; + [Write, Description("The credential this task should execute as. If not specified defaults to running as the local system account."), EmbeddedInstance("MSFT_Credential")] string ExecuteAsCredential; + [Write, Description("The gMSA (Group Managed Service Account) this task should execute as. Cannot be used in combination with ExecuteAsCredential or BuiltInAccount.")] string ExecuteAsGMSA; + [Write, Description("Specifies the interval between the days in the schedule. An interval of 1 produces a daily schedule. An interval of 2 produces an every-other day schedule.")] Uint32 DaysInterval; + [Write, Description("Specifies a random amount of time to delay the start time of the trigger. The delay time is a random time between the time the task triggers and the time that you specify in this setting.")] String RandomDelay; + [Write, Description("Specifies how long the repetition pattern repeats after the task starts. May be set to `Indefinitely` to specify an indefinite duration.")] String RepetitionDuration; + [Write, Description("Specifies an array of the days of the week on which Task Scheduler runs the task.")] String DaysOfWeek[]; + [Write, Description("Specifies the interval between the weeks in the schedule. An interval of 1 produces a weekly schedule. An interval of 2 produces an every-other week schedule.")] Uint32 WeeksInterval; + [Write, Description("Specifies the identifier of the user for a trigger that starts a task when a user logs on.")] String User; + [Write, Description("Indicates whether the task is prohibited to run on demand or not. Defaults to $false.")] Boolean DisallowDemandStart; + [Write, Description("Indicates whether the task is prohibited to be terminated or not. Defaults to $false.")] Boolean DisallowHardTerminate; + [Write, Description("The task compatibility level. Defaults to Vista."), ValueMap{"AT","V1","Vista","Win7","Win8"}, Values{"AT","V1","Vista","Win7","Win8"}] String Compatibility; + [Write, Description("Indicates whether the task should start if the machine is on batteries or not. Defaults to $false.")] Boolean AllowStartIfOnBatteries; + [Write, Description("Indicates that the task is hidden in the Task Scheduler UI.")] Boolean Hidden; + [Write, Description("Indicates that Task Scheduler runs the task only when the computer is idle.")] Boolean RunOnlyIfIdle; + [Write, Description("Specifies the amount of time that Task Scheduler waits for an idle condition to occur.")] String IdleWaitTimeout; + [Write, Description("Specifies the name of a network profile that Task Scheduler uses to determine if the task can run. The Task Scheduler UI uses this setting for display purposes. Specify a network name if you specify the RunOnlyIfNetworkAvailable parameter.")] String NetworkName; + [Write, Description("Indicates that the task does not start if the task is triggered to run in a Remote Applications Integrated Locally (RAIL) session.")] Boolean DisallowStartOnRemoteAppSession; + [Write, Description("Indicates that Task Scheduler can start the task at any time after its scheduled time has passed.")] Boolean StartWhenAvailable; + [Write, Description("Indicates that the task does not stop if the computer switches to battery power.")] Boolean DontStopIfGoingOnBatteries; + [Write, Description("Indicates that Task Scheduler wakes the computer before it runs the task.")] Boolean WakeToRun; + [Write, Description("Specifies the amount of time that the computer must be in an idle state before Task Scheduler runs the task.")] String IdleDuration; + [Write, Description("Indicates that Task Scheduler restarts the task when the computer cycles into an idle condition more than once.")] Boolean RestartOnIdle; + [Write, Description("Indicates that Task Scheduler does not terminate the task if the idle condition ends before the task is completed.")] Boolean DontStopOnIdleEnd; + [Write, Description("Specifies the amount of time that Task Scheduler is allowed to complete the task.")] String ExecutionTimeLimit; + [Write, Description("Specifies the policy that defines how Task Scheduler handles multiple instances of the task."), ValueMap{"IgnoreNew","Parallel","Queue", "StopExisting"}, Values{"IgnoreNew","Parallel","Queue", "StopExisting"}] String MultipleInstances; + [Write, Description("Specifies the priority level of the task. Priority must be an integer from 0 (highest priority) to 10 (lowest priority). The default value is 7. Priority levels 7 and 8 are used for background tasks. Priority levels 4, 5, and 6 are used for interactive tasks.")] Uint32 Priority; + [Write, Description("Specifies the number of times that Task Scheduler attempts to restart the task.")] Uint32 RestartCount; + [Write, Description("Specifies the amount of time that Task Scheduler attempts to restart the task.")] String RestartInterval; + [Write, Description("Indicates that Task Scheduler runs the task only when a network is available. Task Scheduler uses the NetworkID parameter and NetworkName parameter that you specify in this cmdlet to determine if the network is available.")] Boolean RunOnlyIfNetworkAvailable; + [Write, Description("Specifies the level of user rights that Task Scheduler uses to run the tasks that are associated with the principal. Defaults to 'Limited'."), ValueMap{"Limited","Highest"}, Values{"Limited","Highest"}] String RunLevel; + [Write, Description("Specifies the security logon method that Task Scheduler uses to run the tasks that are associated with the principal."), ValueMap{"Group","Interactive","InteractiveOrPassword","None","Password","S4U","ServiceAccount"}, Values{"Group","Interactive","InteractiveOrPassword","None","Password","S4U","ServiceAccount"}] String LogonType; + [Write, Description("Specifies the EventSubscription in XML. This can be easily generated using the Windows Eventlog Viewer. For the query schema please check: https://docs.microsoft.com/en-us/windows/desktop/WES/queryschema-schema. Can only be used in combination with ScheduleType OnEvent.")] String EventSubscription; + [Write, Description("Specifies a delay to the start of the trigger. The delay is a static delay before the task is executed. Can only be used in combination with ScheduleType OnEvent.")] String Delay; +}; diff --git a/deployment/dsc/azshcihost/ComputerManagementDsc/8.4.0/DSCResources/DSC_ScheduledTask/README.md b/deployment/dsc/azshcihost/ComputerManagementDsc/8.4.0/DSCResources/DSC_ScheduledTask/README.md new file mode 100644 index 0000000..efc6df4 --- /dev/null +++ b/deployment/dsc/azshcihost/ComputerManagementDsc/8.4.0/DSCResources/DSC_ScheduledTask/README.md @@ -0,0 +1,39 @@ +# Description + +The resource is used to define basic run once or recurring scheduled tasks +on the local computer. It can also be used to delete or disable built-in +scheduled tasks. + +## Known Issues + +One of the values needed for the `MultipleInstances` parameter is missing from the +`Microsoft.PowerShell.Cmdletization.GeneratedTypes.ScheduledTask.MultipleInstancesEnum` +enumerator. There are four valid values defined for the `MultipleInstances` property of the +Task Settings ([TaskSettings.MultipleInstances Property](https://docs.microsoft.com/en-us/windows/win32/taskschd/tasksettings-multipleinstances "TaskSettings.MultipleInstances Property")). +The `MultipleInstancesEnum` enumerator has three values, which can be mapped to three +of the four valid values, but there is no value corresponding to `TASK_INSTANCES_STOP_EXISTING`. +The result of this omission is that a workaround is required to +accommodate the `StopExisting` value for the `MultipleInstances` parameter, +which would not be necessary if the enumerator had all four valid values. + +### ExecuteAsCredential + +#### When Using a BUILTIN Group + +When creating a scheduled task that uses an `ExecuteAsCredential` that +is one of the 'BUILTIN' groups (e.g. 'BUILTIN\Users'), specifying the +username to include the 'BUILTIN' domain name will result in the resource +never going into state. The same behavior will also occur if setting a +'BUILTIN' group in the UI. + +To prevent this issue, set the username in the `ExecuteAsCredential` to the +name of the group only (e.g. 'Users'). + +#### When Using a Domain User/Group + +When creating a scheduled task that uses an `ExecuteAsCredential` that +is a domain user or group, (e.g. 'CONTOSO\ServiceUser'), the domain +name must be included, otherwise the resource will not go into state. + +To prevent this issue, set the username in the `ExecuteAsCredential` to the +name of the group only (e.g. 'CONTOSO\ServiceUser'). diff --git a/deployment/dsc/azshcihost/ComputerManagementDsc/8.4.0/DSCResources/DSC_ScheduledTask/en-US/DSC_ScheduledTask.strings.psd1 b/deployment/dsc/azshcihost/ComputerManagementDsc/8.4.0/DSCResources/DSC_ScheduledTask/en-US/DSC_ScheduledTask.strings.psd1 new file mode 100644 index 0000000..8e6ef84 --- /dev/null +++ b/deployment/dsc/azshcihost/ComputerManagementDsc/8.4.0/DSCResources/DSC_ScheduledTask/en-US/DSC_ScheduledTask.strings.psd1 @@ -0,0 +1,36 @@ +ConvertFrom-StringData @' + GetScheduledTaskMessage = Getting scheduled task '{0}' in '{1}'. + TaskNotFoundMessage = Task '{0}' not found in '{1}'. Returning an empty task with Ensure = "Absent". + TaskFoundMessage = Task '{0}' found in '{1}'. Retrieving settings, first action, first trigger and repetition settings. + TriggerTypeUnknown = Trigger type '{0}' not recognized. + DetectedScheduleTypeMessage = Detected schedule type '{0}' for first trigger. + SetScheduledTaskMessage = Setting scheduled task '{0}' in '{1}'. + DisablingExistingScheduledTask = Disabling existing scheduled task '{0}' in '{1}'. + RepetitionDurationLessThanIntervalError = Repetition duration '{0}' is less than repetition interval '{1}'. Please set RepeatInterval to a value lower or equal to RepetitionDuration. + DaysIntervalError = DaysInterval must be greater than zero (0) for Daily schedules. DaysInterval specified is '{0}'. + WeeksIntervalError = WeeksInterval must be greater than zero (0) for Weekly schedules. WeeksInterval specified is '{0}'. + WeekDayMissingError = At least one weekday must be selected for Weekly schedule. + OnEventSubscriptionError = No (valid) XML Event Subscription was provided. This is required when the scheduletype is OnEvent. + gMSAandCredentialError = Both ExecuteAsGMSA and (ExecuteAsCredential or BuiltInAccount) parameters have been specified. A task can run as a gMSA (Group Managed Service Account), a builtin service account or as a custom credential. Please modify your configuration to include just one of the three options. + SynchronizeAcrossTimeZoneInvalidScheduleType = Setting SynchronizeAcrossTimeZone to true when the ScheduleType is not Once, Daily or Weekly is not a valid configuration. Please keep the default value of false when using other schedule types. + TriggerCreationError = Error creating new scheduled task trigger. + ConfigureTriggerRepetitionMessage = Configuring trigger repetition. + RepetitionIntervalError = Repetition interval is set to '{0}' but repetition duration is '{1}'. + CreateRepetitionPatternMessage = Creating MSFT_TaskRepetitionPattern CIM instance to configure repetition in trigger. + CreateTemporaryTaskMessage = Creating temporary task and trigger to get MSFT_TaskRepetitionPattern CIM instance. + CreateTemporaryTriggerMessage = Creating temporary trigger to get MSFT_TaskRepetitionPattern CIM instance. + TriggerUnexpectedTypeError = Trigger object that was created was of unexpected type '{0}'. + CreateScheduledTaskPrincipalMessage = Creating scheduled task principal for account '{0}' using logon type '{1}'. + CreateNewScheduledTaskMessage = Creating new scheduled task '{0}' in '{1}'. + ConfigureTaskEventTrigger = Setting up an event based trigger on task {0}. + IgnoreRandomDelayWithTriggerTypeOnEvent = The parameter RandomDelay in task {0} is ignored. A random delay is not supported when the trigger type is set to OnEvent. + SetRepetitionTriggerMessage = Setting repetition trigger settings on task '{0}' in '{1}'. + RetrieveScheduledTaskMessage = Retrieving the scheduled task '{0}' from '{1}'. + RemoveScheduledTaskMessage = Removing scheduled task '{0}' from '{1}'. + UpdateScheduledTaskMessage = Updating scheduled task '{0}' in '{1}'. + TestScheduledTaskMessage = Testing scheduled task '{0}' in '{1}'. + GettingCurrentTaskValuesMessage = Getting current scheduled task values for task '{0}' in '{1}'. + CurrentTaskValuesRetrievedMessage = Current scheduled task values for task '{0}' in '{1}' retrieved. + CurrentTaskValuesNullMessage = Current scheduled values were null. + TestingDscParameterStateMessage = Testing DSC parameter state. +'@ diff --git a/deployment/dsc/azshcihost/ComputerManagementDsc/8.4.0/DSCResources/DSC_SmbServerConfiguration/DSC_SmbServerConfiguration.data.psd1 b/deployment/dsc/azshcihost/ComputerManagementDsc/8.4.0/DSCResources/DSC_SmbServerConfiguration/DSC_SmbServerConfiguration.data.psd1 new file mode 100644 index 0000000..dc2fb90 --- /dev/null +++ b/deployment/dsc/azshcihost/ComputerManagementDsc/8.4.0/DSCResources/DSC_SmbServerConfiguration/DSC_SmbServerConfiguration.data.psd1 @@ -0,0 +1,46 @@ +@{ + smbServerSettings = @( + 'AnnounceComment' + 'AnnounceServer' + 'AsynchronousCredits' + 'AuditSmb1Access' + 'AutoDisconnectTimeout' + 'AutoShareServer' + 'AutoShareWorkstation' + 'CachedOpenLimit' + 'DurableHandleV2TimeoutInSeconds' + 'EnableAuthenticateUserSharing' + 'EnableDownlevelTimewarp' + 'EnableForcedLogoff' + 'EnableLeasing' + 'EnableMultiChannel' + 'EnableOplocks' + 'EnableSecuritySignature' + 'EnableSMB1Protocol' + 'EnableSMB2Protocol' + 'EnableStrictNameChecking' + 'EncryptData' + 'IrpStackSize' + 'KeepAliveTime' + 'MaxChannelPerSession' + 'MaxMpxCount' + 'MaxSessionPerConnection' + 'MaxThreadsPerQueue' + 'MaxWorkItems' + 'NullSessionPipes' + 'NullSessionShares' + 'OplockBreakWait' + 'PendingClientTimeoutInSeconds' + 'RejectUnencryptedAccess' + 'RequireSecuritySignature' + 'ServerHidden' + 'Smb2CreditsMax' + 'Smb2CreditsMin' + 'SmbServerNameHardeningLevel' + 'TreatHostAsStableStorage' + 'ValidateAliasNotCircular' + 'ValidateShareScope' + 'ValidateShareScopeNotAliased' + 'ValidateTargetName' + ) +} diff --git a/deployment/dsc/azshcihost/ComputerManagementDsc/8.4.0/DSCResources/DSC_SmbServerConfiguration/DSC_SmbServerConfiguration.psm1 b/deployment/dsc/azshcihost/ComputerManagementDsc/8.4.0/DSCResources/DSC_SmbServerConfiguration/DSC_SmbServerConfiguration.psm1 new file mode 100644 index 0000000..2413bdf --- /dev/null +++ b/deployment/dsc/azshcihost/ComputerManagementDsc/8.4.0/DSCResources/DSC_SmbServerConfiguration/DSC_SmbServerConfiguration.psm1 @@ -0,0 +1,708 @@ +$modulePath = Join-Path -Path (Split-Path -Path (Split-Path -Path $PSScriptRoot -Parent) -Parent) -ChildPath 'Modules' + +# Import the ComputerManagementDsc Common Modules +Import-Module -Name (Join-Path -Path $modulePath ` + -ChildPath (Join-Path -Path 'ComputerManagementDsc.Common' ` + -ChildPath 'ComputerManagementDsc.Common.psm1')) -Force + +Import-Module -Name (Join-Path -Path $modulePath -ChildPath 'DscResource.Common') + +# Import Localization Strings +$script:localizedData = Get-LocalizedData -DefaultUICulture 'en-US' + +$resourceData = Import-LocalizedData ` + -BaseDirectory $PSScriptRoot ` + -FileName 'DSC_SmbServerConfiguration.data.psd1' + +$script:smbServerSettings = $resourceData.smbServerSettings + +<# + .SYNOPSIS + Returns the current state of the SMB Server. + + .PARAMETER IsSingleInstance + Specifies the resource is a single instance, the value must be 'Yes'. +#> +function Get-TargetResource +{ + [CmdletBinding()] + [OutputType([System.Collections.Hashtable])] + param + ( + [Parameter(Mandatory = $true)] + [ValidateSet('Yes')] + [System.String] + $IsSingleInstance + ) + + Write-Verbose -Message ($script:localizedData.GetTargetResourceMessage -f $Name) + + $smbReturn = @{} + $smbServer = Get-SmbServerConfiguration -ErrorAction 'SilentlyContinue' + $smbReturn.Add('IsSingleInstance', $IsSingleInstance) + + foreach ($smbServerSetting in $script:smbServerSettings) + { + $smbReturn.Add($smbServerSetting, $smbServer.$smbServerSetting) + } + + return $smbReturn +} + +<# + .SYNOPSIS + Determines if the SMB Server is in the desired state. + + .PARAMETER IsSingleInstance + Specifies the resource is a single instance, the value must be 'Yes'. + + .PARAMETER AnnounceComment + Specifies the announce comment string. + + .PARAMETER AnnounceServer + Indicates that this server announces itself by using browser announcements. + + .PARAMETER AsynchronousCredits + Specifies the asynchronous credits. + + .PARAMETER AuditSmb1Access + Enables auditing of SMB version 1 protocol in Windows Event Log. + + .PARAMETER AutoDisconnectTimeout + Specifies the auto disconnect time-out. + + .PARAMETER AutoShareServer + Specifies that the default server shares are shared out. + + .PARAMETER AutoShareWorkstation + Specifies whether the default workstation shares are shared out. + + .PARAMETER CachedOpenLimit + Specifies the maximum number of cached open files. + + .PARAMETER DurableHandleV2TimeoutInSeconds + Specifies the durable handle v2 time-out period, in seconds. + + .PARAMETER EnableAuthenticateUserSharing + Specifies whether authenticate user sharing is enabled. + + .PARAMETER EnableDownlevelTimewarp + Specifies whether down-level timewarp support is disabled. + + .PARAMETER EnableForcedLogoff + Specifies whether forced logoff is enabled. + + .PARAMETER EnableLeasing + Specifies whether leasing is disabled. + + .PARAMETER EnableMultiChannel + Specifies whether multi-channel is disabled. + + .PARAMETER EnableOplocks + Specifies whether the opportunistic locks are enabled. + + .PARAMETER EnableSMB1Protocol + Specifies whether the SMB1 protocol is enabled. + + .PARAMETER EnableSMB2Protocol + Specifies whether the SMB2 protocol is enabled. + + .PARAMETER EnableSecuritySignature + Specifies whether the security signature is enabled. + + .PARAMETER EnableStrictNameChecking + Specifies whether the server should perform strict name checking on incoming connects. + + .PARAMETER EncryptData + Specifies whether the sessions established on this server are encrypted. + + .PARAMETER IrpStackSize + Specifies the default IRP stack size. + + .PARAMETER KeepAliveTime + Specifies the keep alive time. + + .PARAMETER MaxChannelPerSession + Specifies the maximum channels per session. + + .PARAMETER MaxMpxCount + Specifies the maximum MPX count for SMB1. + + .PARAMETER MaxSessionPerConnection + Specifies the maximum sessions per connection. + + .PARAMETER MaxThreadsPerQueue + Specifies the maximum threads per queue. + + .PARAMETER MaxWorkItems + Specifies the maximum SMB1 work items. + + .PARAMETER NullSessionPipes + Specifies the null session pipes. + + .PARAMETER NullSessionShares + Specifies the null session shares. + + .PARAMETER OplockBreakWait + Specifies how long the create caller waits for an opportunistic lock break. + + .PARAMETER PendingClientTimeoutInSeconds + Specifies the pending client time-out period, in seconds. + + .PARAMETER RejectUnencryptedAccess + Specifies whether the client that does not support encryption is denied access if it attempts to connect to an encrypted share. + + .PARAMETER RequireSecuritySignature + Specifies whether the security signature is required. + + .PARAMETER ServerHidden + Specifies whether the server announces itself. + + .PARAMETER Smb2CreditsMax + Specifies the maximum SMB2 credits. + + .PARAMETER Smb2CreditsMin + Specifies the minimum SMB2 credits. + + .PARAMETER SmbServerNameHardeningLevel + Specifies the SMB Service name hardening level. + + .PARAMETER TreatHostAsStableStorage + Specifies whether the host is treated as the stable storage. + + .PARAMETER ValidateAliasNotCircular + Specifies whether the aliases that are not circular are validated. + + .PARAMETER ValidateShareScope + Specifies whether the existence of share scopes is checked during share creation. + + .PARAMETER ValidateShareScopeNotAliased + Specifies whether the share scope being aliased is validated. + + .PARAMETER ValidateTargetName + Specifies whether the target name is validated. +#> +function Set-TargetResource +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [ValidateSet('Yes')] + [System.String] + $IsSingleInstance, + + [Parameter()] + [System.String] + $AnnounceComment, + + [Parameter()] + [System.Boolean] + $AnnounceServer, + + [Parameter()] + [System.Uint32] + $AsynchronousCredits, + + [Parameter()] + [System.Boolean] + $AuditSmb1Access, + + [Parameter()] + [System.Uint32] + $AutoDisconnectTimeout, + + [Parameter()] + [System.Boolean] + $AutoShareServer, + + [Parameter()] + [System.Boolean] + $AutoShareWorkstation, + + [Parameter()] + [System.Uint32] + $CachedOpenLimit, + + [Parameter()] + [System.Uint32] + $DurableHandleV2TimeoutInSeconds, + + [Parameter()] + [System.Boolean] + $EnableAuthenticateUserSharing, + + [Parameter()] + [System.Boolean] + $EnableDownlevelTimewarp, + + [Parameter()] + [System.Boolean] + $EnableForcedLogoff, + + [Parameter()] + [System.Boolean] + $EnableLeasing, + + [Parameter()] + [System.Boolean] + $EnableMultiChannel, + + [Parameter()] + [System.Boolean] + $EnableOplocks, + + [Parameter()] + [System.Boolean] + $EnableSMB1Protocol, + + [Parameter()] + [System.Boolean] + $EnableSMB2Protocol, + + [Parameter()] + [System.Boolean] + $EnableSecuritySignature, + + [Parameter()] + [System.Boolean] + $EnableStrictNameChecking, + + [Parameter()] + [System.Boolean] + $EncryptData, + + [Parameter()] + [System.Uint32] + $IrpStackSize, + + [Parameter()] + [System.Uint32] + $KeepAliveTime, + + [Parameter()] + [System.Uint32] + $MaxChannelPerSession, + + [Parameter()] + [System.Uint32] + $MaxMpxCount, + + [Parameter()] + [System.Uint32] + $MaxSessionPerConnection, + + [Parameter()] + [System.Uint32] + $MaxThreadsPerQueue, + + [Parameter()] + [System.Uint32] + $MaxWorkItems, + + [Parameter()] + [System.String] + $NullSessionPipes, + + [Parameter()] + [System.String] + $NullSessionShares, + + [Parameter()] + [System.Uint32] + $OplockBreakWait, + + [Parameter()] + [System.Uint32] + $PendingClientTimeoutInSeconds, + + [Parameter()] + [System.Boolean] + $RejectUnencryptedAccess, + + [Parameter()] + [System.Boolean] + $RequireSecuritySignature, + + [Parameter()] + [System.Boolean] + $ServerHidden, + + [Parameter()] + [System.Uint32] + $Smb2CreditsMax, + + [Parameter()] + [System.Uint32] + $Smb2CreditsMin, + + [Parameter()] + [System.Uint32] + $SmbServerNameHardeningLevel, + + [Parameter()] + [System.Boolean] + $TreatHostAsStableStorage, + + [Parameter()] + [System.Boolean] + $ValidateAliasNotCircular, + + [Parameter()] + [System.Boolean] + $ValidateShareScope, + + [Parameter()] + [System.Boolean] + $ValidateShareScopeNotAliased, + + [Parameter()] + [System.Boolean] + $ValidateTargetName + ) + + $null = $PSBoundParameters.Remove('IsSingleInstance') + $null = $PSBoundParameters.Add('Confirm', $false) + + Write-Verbose -Message ($script:localizedData.UpdatingProperties) + + Set-SmbServerConfiguration @PSBoundParameters +} + +<# + .SYNOPSIS + Determines if the SMB Server is in the desired state. + + .PARAMETER IsSingleInstance + Specifies the resource is a single instance, the value must be 'Yes'. + + .PARAMETER AnnounceComment + Specifies the announce comment string. + + .PARAMETER AnnounceServer + Indicates that this server announces itself by using browser announcements. + + .PARAMETER AsynchronousCredits + Specifies the asynchronous credits. + + .PARAMETER AuditSmb1Access + Enables auditing of SMB version 1 protocol in Windows Event Log. + + .PARAMETER AutoDisconnectTimeout + Specifies the auto disconnect time-out. + + .PARAMETER AutoShareServer + Specifies that the default server shares are shared out. + + .PARAMETER AutoShareWorkstation + Specifies whether the default workstation shares are shared out. + + .PARAMETER CachedOpenLimit + Specifies the maximum number of cached open files. + + .PARAMETER DurableHandleV2TimeoutInSeconds + Specifies the durable handle v2 time-out period, in seconds. + + .PARAMETER EnableAuthenticateUserSharing + Specifies whether authenticate user sharing is enabled. + + .PARAMETER EnableDownlevelTimewarp + Specifies whether down-level timewarp support is disabled. + + .PARAMETER EnableForcedLogoff + Specifies whether forced logoff is enabled. + + .PARAMETER EnableLeasing + Specifies whether leasing is disabled. + + .PARAMETER EnableMultiChannel + Specifies whether multi-channel is disabled. + + .PARAMETER EnableOplocks + Specifies whether the opportunistic locks are enabled. + + .PARAMETER EnableSMB1Protocol + Specifies whether the SMB1 protocol is enabled. + + .PARAMETER EnableSMB2Protocol + Specifies whether the SMB2 protocol is enabled. + + .PARAMETER EnableSecuritySignature + Specifies whether the security signature is enabled. + + .PARAMETER EnableStrictNameChecking + Specifies whether the server should perform strict name checking on incoming connects. + + .PARAMETER EncryptData + Specifies whether the sessions established on this server are encrypted. + + .PARAMETER IrpStackSize + Specifies the default IRP stack size. + + .PARAMETER KeepAliveTime + Specifies the keep alive time. + + .PARAMETER MaxChannelPerSession + Specifies the maximum channels per session. + + .PARAMETER MaxMpxCount + Specifies the maximum MPX count for SMB1. + + .PARAMETER MaxSessionPerConnection + Specifies the maximum sessions per connection. + + .PARAMETER MaxThreadsPerQueue + Specifies the maximum threads per queue. + + .PARAMETER MaxWorkItems + Specifies the maximum SMB1 work items. + + .PARAMETER NullSessionPipes + Specifies the null session pipes. + + .PARAMETER NullSessionShares + Specifies the null session shares. + + .PARAMETER OplockBreakWait + Specifies how long the create caller waits for an opportunistic lock break. + + .PARAMETER PendingClientTimeoutInSeconds + Specifies the pending client time-out period, in seconds. + + .PARAMETER RejectUnencryptedAccess + Specifies whether the client that does not support encryption is denied access if it attempts to connect to an encrypted share. + + .PARAMETER RequireSecuritySignature + Specifies whether the security signature is required. + + .PARAMETER ServerHidden + Specifies whether the server announces itself. + + .PARAMETER Smb2CreditsMax + Specifies the maximum SMB2 credits. + + .PARAMETER Smb2CreditsMin + Specifies the minimum SMB2 credits. + + .PARAMETER SmbServerNameHardeningLevel + Specifies the SMB Service name hardening level. + + .PARAMETER TreatHostAsStableStorage + Specifies whether the host is treated as the stable storage. + + .PARAMETER ValidateAliasNotCircular + Specifies whether the aliases that are not circular are validated. + + .PARAMETER ValidateShareScope + Specifies whether the existence of share scopes is checked during share creation. + + .PARAMETER ValidateShareScopeNotAliased + Specifies whether the share scope being aliased is validated. + + .PARAMETER ValidateTargetName + Specifies whether the target name is validated. +#> +function Test-TargetResource +{ + [CmdletBinding()] + [OutputType([System.Boolean])] + param + ( + [Parameter(Mandatory = $true)] + [ValidateSet('Yes')] + [System.String] + $IsSingleInstance, + + [Parameter()] + [System.String] + $AnnounceComment, + + [Parameter()] + [System.Boolean] + $AnnounceServer, + + [Parameter()] + [System.Uint32] + $AsynchronousCredits, + + [Parameter()] + [System.Boolean] + $AuditSmb1Access, + + [Parameter()] + [System.Uint32] + $AutoDisconnectTimeout, + + [Parameter()] + [System.Boolean] + $AutoShareServer, + + [Parameter()] + [System.Boolean] + $AutoShareWorkstation, + + [Parameter()] + [System.Uint32] + $CachedOpenLimit, + + [Parameter()] + [System.Uint32] + $DurableHandleV2TimeoutInSeconds, + + [Parameter()] + [System.Boolean] + $EnableAuthenticateUserSharing, + + [Parameter()] + [System.Boolean] + $EnableDownlevelTimewarp, + + [Parameter()] + [System.Boolean] + $EnableForcedLogoff, + + [Parameter()] + [System.Boolean] + $EnableLeasing, + + [Parameter()] + [System.Boolean] + $EnableMultiChannel, + + [Parameter()] + [System.Boolean] + $EnableOplocks, + + [Parameter()] + [System.Boolean] + $EnableSMB1Protocol, + + [Parameter()] + [System.Boolean] + $EnableSMB2Protocol, + + [Parameter()] + [System.Boolean] + $EnableSecuritySignature, + + [Parameter()] + [System.Boolean] + $EnableStrictNameChecking, + + [Parameter()] + [System.Boolean] + $EncryptData, + + [Parameter()] + [System.Uint32] + $IrpStackSize, + + [Parameter()] + [System.Uint32] + $KeepAliveTime, + + [Parameter()] + [System.Uint32] + $MaxChannelPerSession, + + [Parameter()] + [System.Uint32] + $MaxMpxCount, + + [Parameter()] + [System.Uint32] + $MaxSessionPerConnection, + + [Parameter()] + [System.Uint32] + $MaxThreadsPerQueue, + + [Parameter()] + [System.Uint32] + $MaxWorkItems, + + [Parameter()] + [System.String] + $NullSessionPipes, + + [Parameter()] + [System.String] + $NullSessionShares, + + [Parameter()] + [System.Uint32] + $OplockBreakWait, + + [Parameter()] + [System.Uint32] + $PendingClientTimeoutInSeconds, + + [Parameter()] + [System.Boolean] + $RejectUnencryptedAccess, + + [Parameter()] + [System.Boolean] + $RequireSecuritySignature, + + [Parameter()] + [System.Boolean] + $ServerHidden, + + [Parameter()] + [System.Uint32] + $Smb2CreditsMax, + + [Parameter()] + [System.Uint32] + $Smb2CreditsMin, + + [Parameter()] + [System.Uint32] + $SmbServerNameHardeningLevel, + + [Parameter()] + [System.Boolean] + $TreatHostAsStableStorage, + + [Parameter()] + [System.Boolean] + $ValidateAliasNotCircular, + + [Parameter()] + [System.Boolean] + $ValidateShareScope, + + [Parameter()] + [System.Boolean] + $ValidateShareScopeNotAliased, + + [Parameter()] + [System.Boolean] + $ValidateTargetName + ) + + Write-Verbose -Message ($script:localizedData.TestTargetResourceMessage) + + $resourceCompliant = $true + + $currentSmbServerConfiguration = Get-TargetResource -IsSingleInstance Yes + + foreach ($smbParameter in $script:smbServerSettings) + { + if ($PSBoundParameters.ContainsKey($smbParameter)) + { + Write-Verbose -Message ($script:localizedData.EvaluatingProperties ` + -f $smbParameter, $currentSmbServerConfiguration.$smbParameter, $PSBoundParameters.$smbParameter) + + if ($PSBoundParameters.$smbParameter -ne $currentSmbServerConfiguration.$smbParameter) + { + $resourceCompliant = $false + } + } + } + + return $resourceCompliant +} + +Export-ModuleMember -Function *-TargetResource diff --git a/deployment/dsc/azshcihost/ComputerManagementDsc/8.4.0/DSCResources/DSC_SmbServerConfiguration/DSC_SmbServerConfiguration.schema.mof b/deployment/dsc/azshcihost/ComputerManagementDsc/8.4.0/DSCResources/DSC_SmbServerConfiguration/DSC_SmbServerConfiguration.schema.mof new file mode 100644 index 0000000..4c43458 --- /dev/null +++ b/deployment/dsc/azshcihost/ComputerManagementDsc/8.4.0/DSCResources/DSC_SmbServerConfiguration/DSC_SmbServerConfiguration.schema.mof @@ -0,0 +1,51 @@ + +[ClassVersion("1.0.0.0"), FriendlyName("SmbServerConfiguration")] +class DSC_SmbServerConfiguration : OMI_BaseResource +{ + [Key, Description("Specifies the resource is a single instance, the value must be 'Yes'"), ValueMap{"Yes"}, Values{"Yes"}] String IsSingleInstance; + [Write, Description("Specifies the announce comment string.")] String AnnounceComment; + [Write, Description("Specifies whether this server announces itself by using browser announcements.")] Boolean AnnounceServer; + [Write, Description("Specifies the asynchronous credits.")] UInt32 AsynchronousCredits; + [Write, Description("Enables auditing of SMB version 1 protocol in Windows Event Log.")] Boolean AuditSmb1Access; + [Write, Description("Specifies the auto disconnect time-out.")] UInt32 AutoDisconnectTimeout; + [Write, Description("Specifies that the default server shares are shared out.")] Boolean AutoShareServer; + [Write, Description("Specifies whether the default workstation shares are shared out.")] Boolean AutoShareWorkstation; + [Write, Description("Specifies the maximum number of cached open files.")] UInt32 CachedOpenLimit; + [Write, Description("Specifies the durable handle v2 time-out period, in seconds.")] UInt32 DurableHandleV2TimeoutInSeconds; + [Write, Description("Specifies whether authenticate user sharing is enabled.")] Boolean EnableAuthenticateUserSharing; + [Write, Description("Specifies whether down-level timewarp support is disabled.")] Boolean EnableDownlevelTimewarp; + [Write, Description("Specifies whether forced logoff is enabled.")] Boolean EnableForcedLogoff; + [Write, Description("Specifies whether leasing is disabled.")] Boolean EnableLeasing; + [Write, Description("Specifies whether multi-channel is disabled.")] Boolean EnableMultiChannel; + [Write, Description("Specifies whether the opportunistic locks are enabled.")] Boolean EnableOplocks; + [Write, Description("Specifies whether the SMB1 protocol is enabled.")] Boolean EnableSMB1Protocol; + [Write, Description("Specifies whether the SMB2 protocol is enabled.")] Boolean EnableSMB2Protocol; + [Write, Description("Specifies whether the security signature is enabled.")] Boolean EnableSecuritySignature; + [Write, Description("Specifies whether the server should perform strict name checking on incoming connects.")] Boolean EnableStrictNameChecking; + [Write, Description("Specifies whether the sessions established on this server are encrypted.")] Boolean EncryptData; + [Write, Description("Specifies the default IRP stack size.")] UInt32 IrpStackSize; + [Write, Description("Specifies the keep alive time.")] UInt32 KeepAliveTime; + [Write, Description("Specifies the maximum channels per session.")] UInt32 MaxChannelPerSession; + [Write, Description("Specifies the maximum MPX count for SMB1.")] UInt32 MaxMpxCount; + [Write, Description("Specifies the maximum sessions per connection.")] UInt32 MaxSessionPerConnection; + [Write, Description("Specifies the maximum threads per queue.")] UInt32 MaxThreadsPerQueue; + [Write, Description("Specifies the maximum SMB1 work items.")] UInt32 MaxWorkItems; + [Write, Description("Specifies the null session pipes.")] String NullSessionPipes; + [Write, Description("Specifies the null session shares.")] String NullSessionShares; + [Write, Description("Specifies how long the create caller waits for an opportunistic lock break.")] UInt32 OplockBreakWait; + [Write, Description("Specifies the pending client time-out period, in seconds.")] UInt32 PendingClientTimeoutInSeconds; + [Write, Description("Specifies whether the client that does not support encryption is denied access if it attempts to connect to an encrypted share.")] Boolean RejectUnencryptedAccess; + [Write, Description("Specifies whether the security signature is required.")] Boolean RequireSecuritySignature; + [Write, Description("Specifies whether the server announces itself.")] Boolean ServerHidden; + [Write, Description("Specifies the maximum SMB2 credits.")] UInt32 Smb2CreditsMax; + [Write, Description("Specifies the minimum SMB2 credits.")] UInt32 Smb2CreditsMin; + [Write, Description("Specifies the SMB Service name hardening level.")] UInt32 SmbServerNameHardeningLevel; + [Write, Description("Specifies whether the host is treated as the stable storage.")] Boolean TreatHostAsStableStorage; + [Write, Description("Specifies whether the aliases that are not circular are validated.")] Boolean ValidateAliasNotCircular; + [Write, Description("Specifies whether the existence of share scopes is checked during share creation.")] Boolean ValidateShareScope; + [Write, Description("Specifies whether the share scope being aliased is validated.")] Boolean ValidateShareScopeNotAliased; + [Write, Description("Specifies whether the target name is validated.")] Boolean ValidateTargetName; +}; + + + diff --git a/deployment/dsc/azshcihost/ComputerManagementDsc/8.4.0/DSCResources/DSC_SmbServerConfiguration/README.md b/deployment/dsc/azshcihost/ComputerManagementDsc/8.4.0/DSCResources/DSC_SmbServerConfiguration/README.md new file mode 100644 index 0000000..bad7af9 --- /dev/null +++ b/deployment/dsc/azshcihost/ComputerManagementDsc/8.4.0/DSCResources/DSC_SmbServerConfiguration/README.md @@ -0,0 +1,7 @@ +# Description + +The resource is used to manage SMB Server Settings. + +## Requirements + +Windows Server 2012 or newer. diff --git a/deployment/dsc/azshcihost/ComputerManagementDsc/8.4.0/DSCResources/DSC_SmbServerConfiguration/en-US/DSC_SmbServerConfiguration.schema.mfl b/deployment/dsc/azshcihost/ComputerManagementDsc/8.4.0/DSCResources/DSC_SmbServerConfiguration/en-US/DSC_SmbServerConfiguration.schema.mfl new file mode 100644 index 0000000..6b862f4 --- /dev/null +++ b/deployment/dsc/azshcihost/ComputerManagementDsc/8.4.0/DSCResources/DSC_SmbServerConfiguration/en-US/DSC_SmbServerConfiguration.schema.mfl @@ -0,0 +1,47 @@ +[Write, Description("This resource is used to configure SMB Server.")] +class DSC_SmbServerConfiguration : OMI_BaseResource +{ + [Key, Description("Specifies the resource is a single instance, the value must be 'Yes'"), ValueMap{"Yes"}, Values{"Yes"}] String IsSingleInstance; + [Write, Description("Specifies the announce comment string.")] String AnnounceComment; + [Write, Description("Specifies whether this server announces itself by using browser announcements.")] Boolean AnnounceServer; + [Write, Description("Specifies the asynchronous credits.")] UInt32 AsynchronousCredits; + [Write, Description("Enables auditing of SMB version 1 protocol in Windows Event Log.")] Boolean AuditSmb1Access; + [Write, Description("Specifies the auto disconnect time-out.")] UInt32 AutoDisconnectTimeout; + [Write, Description("Specifies that the default server shares are shared out.")] Boolean AutoShareServer; + [Write, Description("Specifies whether the default workstation shares are shared out.")] Boolean AutoShareWorkstation; + [Write, Description("Specifies the maximum number of cached open files.")] UInt32 CachedOpenLimit; + [Write, Description("Specifies the durable handle v2 time-out period, in seconds.")] UInt32 DurableHandleV2TimeoutInSeconds; + [Write, Description("Specifies whether authenticate user sharing is enabled.")] Boolean EnableAuthenticateUserSharing; + [Write, Description("Specifies whether down-level timewarp support is disabled.")] Boolean EnableDownlevelTimewarp; + [Write, Description("Specifies whether forced logoff is enabled.")] Boolean EnableForcedLogoff; + [Write, Description("Specifies whether leasing is disabled.")] Boolean EnableLeasing; + [Write, Description("Specifies whether multi-channel is disabled.")] Boolean EnableMultiChannel; + [Write, Description("Specifies whether the opportunistic locks are enabled.")] Boolean EnableOplocks; + [Write, Description("Specifies whether the SMB1 protocol is enabled.")] Boolean EnableSMB1Protocol; + [Write, Description("Specifies whether the SMB2 protocol is enabled.")] Boolean EnableSMB2Protocol; + [Write, Description("Specifies whether the security signature is enabled.")] Boolean EnableSecuritySignature; + [Write, Description("Specifies whether the server should perform strict name checking on incoming connects.")] Boolean EnableStrictNameChecking; + [Write, Description("Specifies whether the sessions established on this server are encrypted.")] Boolean EncryptData; + [Write, Description("Specifies the default IRP stack size.")] UInt32 IrpStackSize; + [Write, Description("Specifies the keep alive time.")] UInt32 KeepAliveTime; + [Write, Description("Specifies the maximum channels per session.")] UInt32 MaxChannelPerSession; + [Write, Description("Specifies the maximum MPX count for SMB1.")] UInt32 MaxMpxCount; + [Write, Description("Specifies the maximum sessions per connection.")] UInt32 MaxSessionPerConnection; + [Write, Description("Specifies the maximum threads per queue.")] UInt32 MaxThreadsPerQueue; + [Write, Description("Specifies the maximum SMB1 work items.")] UInt32 MaxWorkItems; + [Write, Description("Specifies the null session pipes.")] String NullSessionPipes; + [Write, Description("Specifies the null session shares.")] String NullSessionShares; + [Write, Description("Specifies how long the create caller waits for an opportunistic lock break.")] UInt32 OplockBreakWait; + [Write, Description("Specifies the pending client time-out period, in seconds.")] UInt32 PendingClientTimeoutInSeconds; + [Write, Description("Specifies whether the client that does not support encryption is denied access if it attempts to connect to an encrypted share.")] Boolean RejectUnencryptedAccess; + [Write, Description("Specifies whether the security signature is required.")] Boolean RequireSecuritySignature; + [Write, Description("Specifies whether the server announces itself.")] Boolean ServerHidden; + [Write, Description("Specifies the maximum SMB2 credits.")] UInt32 Smb2CreditsMax; + [Write, Description("Specifies the minimum SMB2 credits.")] UInt32 Smb2CreditsMin; + [Write, Description("Specifies the SMB Service name hardening level.")] UInt32 SmbServerNameHardeningLevel; + [Write, Description("Specifies whether the host is treated as the stable storage.")] Boolean TreatHostAsStableStorage; + [Write, Description("Specifies whether the aliases that are not circular are validated.")] Boolean ValidateAliasNotCircular; + [Write, Description("Specifies whether the existence of share scopes is checked during share creation.")] Boolean ValidateShareScope; + [Write, Description("Specifies whether the share scope being aliased is validated.")] Boolean ValidateShareScopeNotAliased; + [Write, Description("Specifies whether the target name is validated.")] Boolean ValidateTargetName; +}; diff --git a/deployment/dsc/azshcihost/ComputerManagementDsc/8.4.0/DSCResources/DSC_SmbServerConfiguration/en-US/DSC_SmbServerConfiguration.strings.psd1 b/deployment/dsc/azshcihost/ComputerManagementDsc/8.4.0/DSCResources/DSC_SmbServerConfiguration/en-US/DSC_SmbServerConfiguration.strings.psd1 new file mode 100644 index 0000000..3d3155d --- /dev/null +++ b/deployment/dsc/azshcihost/ComputerManagementDsc/8.4.0/DSCResources/DSC_SmbServerConfiguration/en-US/DSC_SmbServerConfiguration.strings.psd1 @@ -0,0 +1,8 @@ +# Localized resources for WindowsOptionalFeature + +ConvertFrom-StringData @' + GetTargetResourceMessage = Getting the current state of the SMB Server. + TestTargetResourceMessage = Determining if the SMB Server is in the desired state. + EvaluatingProperties = Evaluating the '{0}' property of the SMB Server. Current value '{1}'. Requested value '{2}'. + UpdatingProperties = Updating properties on the SMB Server. +'@ diff --git a/deployment/dsc/azshcihost/ComputerManagementDsc/8.4.0/DSCResources/DSC_SmbShare/DSC_SmbShare.psm1 b/deployment/dsc/azshcihost/ComputerManagementDsc/8.4.0/DSCResources/DSC_SmbShare/DSC_SmbShare.psm1 new file mode 100644 index 0000000..06fd539 --- /dev/null +++ b/deployment/dsc/azshcihost/ComputerManagementDsc/8.4.0/DSCResources/DSC_SmbShare/DSC_SmbShare.psm1 @@ -0,0 +1,853 @@ +$modulePath = Join-Path -Path (Split-Path -Path (Split-Path -Path $PSScriptRoot -Parent) -Parent) -ChildPath 'Modules' + +# Import the ComputerManagementDsc Common Modules +Import-Module -Name (Join-Path -Path $modulePath ` + -ChildPath (Join-Path -Path 'ComputerManagementDsc.Common' ` + -ChildPath 'ComputerManagementDsc.Common.psm1')) -Force + +Import-Module -Name (Join-Path -Path $modulePath -ChildPath 'DscResource.Common') + +# Import Localization Strings +$script:localizedData = Get-LocalizedData -DefaultUICulture 'en-US' + +<# + .SYNOPSIS + Returns the current state of the SMB share. + + .PARAMETER Name + Specifies the name of the SMB share. + + .PARAMETER Path + Specifies the path of the SMB share. + + Not used in Get-TargetResource. +#> +function Get-TargetResource +{ + [CmdletBinding()] + [OutputType([System.Collections.Hashtable])] + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $Name, + + [Parameter(Mandatory = $true)] + [System.String] + $Path + ) + + Write-Verbose -Message ($script:localizedData.GetTargetResourceMessage -f $Name) + + $returnValue = @{ + Ensure = 'Absent' + Name = $Name + Path = [System.String] $null + Description = [System.String] $null + ConcurrentUserLimit = 0 + EncryptData = $false + FolderEnumerationMode = [System.String] $null + CachingMode = [System.String] $null + ContinuouslyAvailable = $false + ShareState = [System.String] $null + ShareType = [System.String] $null + ShadowCopy = $false + Special = $false + ScopeName = [System.String] $null + } + + $accountsFullAccess = [system.string[]] @() + $accountsChangeAccess = [system.string[]] @() + $accountsReadAccess = [system.string[]] @() + $accountsNoAccess = [system.string[]] @() + + $smbShare = Get-SmbShare -Name $Name -ErrorAction 'SilentlyContinue' + + if ($smbShare) + { + $returnValue['Ensure'] = 'Present' + $returnValue['Name'] = $smbShare.Name + $returnValue['Path'] = $smbShare.Path + $returnValue['Description'] = $smbShare.Description + $returnValue['ConcurrentUserLimit'] = $smbShare.ConcurrentUserLimit + $returnValue['EncryptData'] = $smbShare.EncryptData + $returnValue['FolderEnumerationMode'] = $smbShare.FolderEnumerationMode.ToString() + $returnValue['CachingMode'] = $smbShare.CachingMode.ToString() + $returnValue['ContinuouslyAvailable'] = $smbShare.ContinuouslyAvailable + $returnValue['ShareState'] = $smbShare.ShareState.ToString() + $returnValue['ShareType'] = $smbShare.ShareType.ToString() + $returnValue['ShadowCopy'] = $smbShare.ShadowCopy + $returnValue['Special'] = $smbShare.Special + $returnValue['ScopeName'] = $smbShare.ScopeName + + $currentSmbShareAccessPermissions = Get-SmbShareAccess -Name $Name + + foreach ($access in $currentSmbShareAccessPermissions) + { + switch ($access.AccessRight) + { + 'Change' + { + if ($access.AccessControlType -eq 'Allow') + { + $accountsChangeAccess += @($access.AccountName) + } + } + + 'Read' + { + if ($access.AccessControlType -eq 'Allow') + { + $accountsReadAccess += @($access.AccountName) + } + } + + 'Full' + { + if ($access.AccessControlType -eq 'Allow') + { + $accountsFullAccess += @($access.AccountName) + } + + if ($access.AccessControlType -eq 'Deny') + { + $accountsNoAccess += @($access.AccountName) + } + } + } + } + } + else + { + Write-Verbose -Message ($script:localizedData.ShareNotFound -f $Name) + } + + <# + This adds either an empty array, or a populated array depending + if accounts with the respectively access was found. + #> + $returnValue['FullAccess'] = [System.String[]] $accountsFullAccess + $returnValue['ChangeAccess'] = [System.String[]] $accountsChangeAccess + $returnValue['ReadAccess'] = [System.String[]] $accountsReadAccess + $returnValue['NoAccess'] = [System.String[]] $accountsNoAccess + + return $returnValue +} + +<# + .SYNOPSIS + Creates or removes the SMB share. + + .PARAMETER Name + Specifies the name of the SMB share. + + .PARAMETER Path + Specifies the path of the SMB share. + + .PARAMETER Description + Specifies the description of the SMB share. + + .PARAMETER ConcurrentUserLimit + Specifies the maximum number of concurrently connected users that the + new SMB share may accommodate. If this parameter is set to zero (0), + then the number of users is unlimited. The default value is zero (0). + + .PARAMETER EncryptData + Indicates that the SMB share is encrypted. + + .PARAMETER FolderEnumerationMode + Specifies which files and folders in the new SMB share are visible to + users. { AccessBased | Unrestricted } + + .PARAMETER CachingMode + Specifies the caching mode of the offline files for the SMB share. + { 'None' | 'Manual' | 'Programs' | 'Documents' | 'BranchCache' } + + .PARAMETER ContinuouslyAvailable + Specifies whether the SMB share should be continuously available. + + .PARAMETER FullAccess + Specifies which accounts are granted full permission to access the + SMB share. + + .PARAMETER ChangeAccess + Specifies which accounts will be granted modify permission to access the + SMB share. + + .PARAMETER ReadAccess + Specifies which accounts is granted read permission to access the SMB share. + + .PARAMETER NoAccess + Specifies which accounts are denied access to the SMB share. + + .PARAMETER Ensure + Specifies if the SMB share should be added or removed. + + .PARAMETER ScopeName + Specifies the scope in which the share should be created. + + .PARAMETER Force + Specifies if the SMB share is allowed to be dropped and recreated (required + when the path changes). +#> +function Set-TargetResource +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $Name, + + [Parameter(Mandatory = $true)] + [System.String] + $Path, + + [Parameter()] + [System.String] + $Description, + + [Parameter()] + [System.UInt32] + $ConcurrentUserLimit, + + [Parameter()] + [System.Boolean] + $EncryptData, + + [Parameter()] + [ValidateSet('AccessBased', 'Unrestricted')] + [System.String] + $FolderEnumerationMode, + + [Parameter()] + [ValidateSet('None', 'Manual', 'Programs', 'Documents', 'BranchCache')] + [System.String] + $CachingMode, + + [Parameter()] + [System.Boolean] + $ContinuouslyAvailable, + + [Parameter()] + [System.String[]] + $FullAccess, + + [Parameter()] + [System.String[]] + $ChangeAccess, + + [Parameter()] + [System.String[]] + $ReadAccess, + + [Parameter()] + [System.String[]] + $NoAccess, + + [Parameter()] + [ValidateSet('Present', 'Absent')] + [System.String] + $Ensure = 'Present', + + [Parameter()] + [System.String] + $ScopeName = '*', + + [Parameter()] + [System.Boolean] + $Force + ) + + Assert-AccessPermissionParameters @PSBoundParameters + + <# + Copy the $PSBoundParameters to a new hash table, so we have the + original intact. + #> + $smbShareParameters = @{} + $PSBoundParameters + + $currentSmbShareConfiguration = Get-TargetResource -Name $Name -Path $Path + + if ($currentSmbShareConfiguration.Ensure -eq 'Present') + { + Write-Verbose -Message ($script:localizedData.IsPresent -f $Name) + + if ($Ensure -eq 'Present') + { + if ( + ($currentSmbShareConfiguration.Path -ne $Path -or + $currentSmbShareConfiguration.ScopeName -ne $ScopeName) -and + $Force + ) + { + Write-Verbose -Message ($script:localizedData.RecreateShare -f $Name) + + try + { + Remove-SmbShare -Name $Name -Force -ErrorAction Stop + New-SmbShare -Name $Name -Path $Path -ErrorAction Stop + } + catch + { + Write-Error -Message ($script:localizedData.RecreateShareError -f $Name, $_) + } + } + else + { + Write-Warning -Message ( + $script:localizedData.NoRecreateShare -f $Name, $currentSmbShareConfiguration.Path, $Path + ) + } + + Write-Verbose -Message $script:localizedData.UpdatingProperties + + $parametersToRemove = $smbShareParameters.Keys | + Where-Object -FilterScript { + $_ -in ('ChangeAccess','ReadAccess','FullAccess','NoAccess','Ensure','Path','Force') + } + + $parametersToRemove | ForEach-Object -Process { + $smbShareParameters.Remove($_) + } + + # Use Set-SmbShare for performing operations other than changing access + Set-SmbShare @smbShareParameters -Force -ErrorAction 'Stop' + + $smbShareAccessPermissionParameters = @{ + Name = $Name + } + + if ($PSBoundParameters.ContainsKey('FullAccess')) + { + $smbShareAccessPermissionParameters['FullAccess'] = $FullAccess + } + + if ($PSBoundParameters.ContainsKey('ChangeAccess')) + { + $smbShareAccessPermissionParameters['ChangeAccess'] = $ChangeAccess + } + + if ($PSBoundParameters.ContainsKey('ReadAccess')) + { + $smbShareAccessPermissionParameters['ReadAccess'] = $ReadAccess + } + + if ($PSBoundParameters.ContainsKey('NoAccess')) + { + $smbShareAccessPermissionParameters['NoAccess'] = $NoAccess + } + + # We should only pass the access collections that the user wants to enforce. + Remove-SmbShareAccessPermission @smbShareAccessPermissionParameters + + Add-SmbShareAccessPermission @smbShareAccessPermissionParameters + } + else + { + Write-Verbose -Message ($script:localizedData.RemoveShare -f $Name) + + Remove-SmbShare -name $Name -Force -ErrorAction 'Stop' + } + } + else + { + if ($Ensure -eq 'Present') + { + $smbShareParameters.Remove('Ensure') + $smbShareParameters.Remove('Force') + + Write-Verbose -Message ($script:localizedData.CreateShare -f $Name) + + <# + Remove access collections that are empty, since empty + collections are not allowed to be provided to the cmdlet + New-SmbShare. + #> + foreach ($accessProperty in ('ChangeAccess','ReadAccess','FullAccess','NoAccess')) + { + if ($smbShareParameters.ContainsKey($accessProperty) -and -not $smbShareParameters[$accessProperty]) + { + $smbShareParameters.Remove($accessProperty) + } + } + + New-SmbShare @smbShareParameters -ErrorAction 'Stop' + } + } +} + +<# + .SYNOPSIS + Determines if the SMB share is in the desired state. + + .PARAMETER Name + Specifies the name of the SMB share. + + .PARAMETER Path + Specifies the path of the SMB share. + + .PARAMETER Description + Specifies the description of the SMB share. + + .PARAMETER ConcurrentUserLimit + Specifies the maximum number of concurrently connected users that the + new SMB share may accommodate. If this parameter is set to zero (0), + then the number of users is unlimited. The default value is zero (0). + + .PARAMETER EncryptData + Indicates that the SMB share is encrypted. + + .PARAMETER FolderEnumerationMode + Specifies which files and folders in the new SMB share are visible to + users. { AccessBased | Unrestricted } + + .PARAMETER CachingMode + Specifies the caching mode of the offline files for the SMB share. + { 'None' | 'Manual' | 'Programs' | 'Documents' | 'BranchCache' } + + .PARAMETER ContinuouslyAvailable + Specifies whether the SMB share should be continuously available. + + .PARAMETER FullAccess + Specifies which accounts are granted full permission to access the + SMB share. + + .PARAMETER ChangeAccess + Specifies which accounts will be granted modify permission to access the + SMB share. + + .PARAMETER ReadAccess + Specifies which accounts is granted read permission to access the SMB share. + + .PARAMETER NoAccess + Specifies which accounts are denied access to the SMB share. + + .PARAMETER Ensure + Specifies if the SMB share should be added or removed. + + .PARAMETER ScopeName + Specifies the scope in which the share should be created. + + .PARAMETER Force + Specifies if the SMB share is allowed to be dropped and recreated (required + when the path changes). +#> +function Test-TargetResource +{ + [CmdletBinding()] + [OutputType([System.Boolean])] + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $Name, + + [Parameter(Mandatory = $true)] + [System.String] + $Path, + + [Parameter()] + [System.String] + $Description, + + [Parameter()] + [System.UInt32] + $ConcurrentUserLimit, + + [Parameter()] + [System.Boolean] + $EncryptData, + + [Parameter()] + [ValidateSet('AccessBased', 'Unrestricted')] + [System.String] + $FolderEnumerationMode, + + [Parameter()] + [ValidateSet('None', 'Manual', 'Programs', 'Documents', 'BranchCache')] + [System.String] + $CachingMode, + + [Parameter()] + [System.Boolean] + $ContinuouslyAvailable, + + [Parameter()] + [System.String[]] + $FullAccess, + + [Parameter()] + [System.String[]] + $ChangeAccess, + + [Parameter()] + [System.String[]] + $ReadAccess, + + [Parameter()] + [System.String[]] + $NoAccess, + + [Parameter()] + [ValidateSet('Present', 'Absent')] + [System.String] + $Ensure = 'Present', + + [Parameter()] + [System.String] + $ScopeName = '*', + + [Parameter()] + [System.Boolean] + $Force + ) + + $null = $PSBoundParameters.Remove('Force') + + Assert-AccessPermissionParameters @PSBoundParameters + + Write-Verbose -Message ($script:localizedData.TestTargetResourceMessage -f $Name) + + $resourceRequiresUpdate = $false + + $currentSmbShareConfiguration = Get-TargetResource -Name $Name -Path $Path + + if ($currentSmbShareConfiguration.Ensure -eq $Ensure) + { + if ($Ensure -eq 'Present') + { + Write-Verbose -Message ( + '{0} {1}' -f ` + ($script:localizedData.IsPresent -f $Name), + $script:localizedData.EvaluatingProperties + ) + + <# + Using $VerbosePreference so that the verbose messages in + Test-DscParameterState is outputted, if the user requested + verbose messages. + #> + $resourceRequiresUpdate = Test-DscParameterState ` + -CurrentValues $currentSmbShareConfiguration ` + -DesiredValues $PSBoundParameters ` + -Verbose:$VerbosePreference + } + else + { + Write-Verbose -Message ($script:localizedData.IsAbsent -f $Name) + + $resourceRequiresUpdate = $true + } + } + + return $resourceRequiresUpdate +} + +<# + .SYNOPSIS + Removes the access permission for accounts that are no longer part + of the respectively access collections (FullAccess, ChangeAccess, + ReadAccess, and NoAccess). + + .PARAMETER Name + The name of the SMB share for which to remove access permission. + + .PARAMETER FullAccess + A string collection of account names that _should have_ full access + permission. The accounts not in this collection will be removed from + the SMB share. + + .PARAMETER ChangeAccess + A string collection of account names that _should have_ change access + permission. The accounts not in this collection will be removed from + the SMB share. + + .PARAMETER ReadAccess + A string collection of account names that _should have_ read access + permission. The accounts not in this collection will be removed from + the SMB share. + + .PARAMETER NoAccess + A string collection of account names that _should be_ denied access + to the SMB share. The accounts not in this collection will be removed + from the SMB share. + + .NOTES + The access permission is only removed if the parameter was passed + into the function. +#> +function Remove-SmbShareAccessPermission +{ + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $Name, + + [Parameter()] + [System.String[]] + $FullAccess, + + [Parameter()] + [System.String[]] + $ChangeAccess, + + [Parameter()] + [System.String[]] + $ReadAccess, + + [Parameter()] + [System.String[]] + $NoAccess + ) + + $currentSmbShareAccessPermissions = Get-SmbShareAccess -Name $Name + + <# + First all access must be removed for accounts that should not + have permission, or should be unblocked (those that was denied + access). After that we can add new accounts using the function + Add-SmbShareAccessPermission. + #> + foreach ($smbShareAccess in $currentSmbShareAccessPermissions) + { + switch ($smbShareAccess.AccessControlType) + { + 'Allow' + { + $shouldRevokeAccess = $false + + foreach ($accessRight in 'Change','Read','Full') + { + $accessRightVariableName = '{0}Access' -f $accessRight + $shouldRevokeAccess = $shouldRevokeAccess ` + -or ( + $smbShareAccess.AccessRight -eq $accessRight ` + -and $PSBoundParameters.ContainsKey($accessRightVariableName) ` + -and $smbShareAccess.AccountName -notin $PSBoundParameters[$accessRightVariableName] + ) + } + + if ($shouldRevokeAccess) + { + Write-Verbose -Message ($script:localizedData.RevokeAccess -f $smbShareAccess.AccountName, $Name) + + Revoke-SmbShareAccess -Name $Name -AccountName $smbShareAccess.AccountName -Force -ErrorAction 'Stop' + } + } + + 'Deny' + { + if ($smbShareAccess.AccessRight -eq 'Full') + { + if ($PSBoundParameters.ContainsKey('NoAccess') -and $smbShareAccess.AccountName -notin $NoAccess) + { + Write-Verbose -Message ($script:localizedData.UnblockAccess -f $smbShareAccess.AccountName, $Name) + + Unblock-SmbShareAccess -Name $Name -AccountName $smbShareAccess.AccountName -Force -ErrorAction 'Stop' + } + } + } + } + } +} + +<# + .SYNOPSIS + Add the access permission to the SMB share for accounts, in the + respectively access collections (FullAccess, ChangeAccess, + ReadAccess, and NoAccess), that do not yet have access. + + .PARAMETER Name + The name of the SMB share to add access permission to. + + .PARAMETER FullAccess + A string collection of account names that should have full access + permission. The accounts in this collection will be added to the + SMB share. + + .PARAMETER ChangeAccess + A string collection of account names that should have change access + permission. The accounts in this collection will be added to the + SMB share. + + .PARAMETER ReadAccess + A string collection of account names that should have read access + permission. The accounts in this collection will be added to the + SMB share. + + .PARAMETER NoAccess + A string collection of account names that should be denied access + to the SMB share. The accounts in this collection will be added to + the SMB share. +#> +function Add-SmbShareAccessPermission +{ + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $Name, + + [Parameter()] + [System.String[]] + $FullAccess, + + [Parameter()] + [System.String[]] + $ChangeAccess, + + [Parameter()] + [System.String[]] + $ReadAccess, + + [Parameter()] + [System.String[]] + $NoAccess + ) + + $currentSmbShareAccessPermissions = Get-SmbShareAccess -Name $Name + + $accessRights = @{ + ReadAccess = 'Read' + ChangeAccess = 'Change' + FullAccess = 'Full' + } + + foreach ($accessRight in $accessRights.GetEnumerator()) + { + if ($PSBoundParameters.ContainsKey($accessRight.Key)) + { + # Get already added account names. + $smbShareAccessObjects = $currentSmbShareAccessPermissions | Where-Object -FilterScript { + $_.AccessControlType -eq 'Allow' -and + $_.AccessRight -eq $accessRight.Value + } + + # Get a collection of just the account names. + $accessAccountNames = @($smbShareAccessObjects.AccountName) + + $newAccountsToHaveAccess = $PSBoundParameters[$accessRight.Key] | Where-Object -FilterScript { + $_ -notin $accessAccountNames + } + + # Add new accounts that should have permission. + $newAccountsToHaveAccess | ForEach-Object -Process { + Write-Verbose -Message ($script:localizedData.GrantAccess -f $accessRight.Value, $_, $Name) + + Grant-SmbShareAccess -Name $Name -AccountName $_ -AccessRight $accessRight.Value -Force -ErrorAction 'Stop' + } + } + } + + if ($PSBoundParameters.ContainsKey('NoAccess')) + { + # Get already added account names. + $smbShareNoAccessObjects = $currentSmbShareAccessPermissions | Where-Object -FilterScript { + $_.AccessControlType -eq 'Deny' -and + $_.AccessRight -eq 'Full' + } + + # Get a collection of just the account names. + $noAccessAccountNames = @($smbShareNoAccessObjects.AccountName) + + $newAccountsToHaveNoAccess = $NoAccess | Where-Object -FilterScript { + $_ -notin $noAccessAccountNames + } + + # Add new accounts that should be denied permission. + $newAccountsToHaveNoAccess | ForEach-Object -Process { + Write-Verbose -Message ($script:localizedData.DenyAccess -f $_, $Name) + + Block-SmbShareAccess -Name $Name -AccountName $_ -Force -ErrorAction 'Stop' + } + } +} + +<# + .SYNOPSIS + Assert that not only empty collections are passed in the + respectively access permission collections (FullAccess, + ChangeAccess, ReadAccess, and NoAccess). + + .PARAMETER Name + The name of the SMB share to add access permission to. + + .PARAMETER FullAccess + A string collection of account names that should have full access + permission. The accounts in this collection will be added to the + SMB share. + + .PARAMETER ChangeAccess + A string collection of account names that should have change access + permission. The accounts in this collection will be added to the + SMB share. + + .PARAMETER ReadAccess + A string collection of account names that should have read access + permission. The accounts in this collection will be added to the + SMB share. + + .PARAMETER NoAccess + A string collection of account names that should be denied access + to the SMB share. The accounts in this collection will be added to + the SMB share. + + .PARAMETER RemainingParameters + Container for the rest of the potentially splatted parameters from + the $PSBoundParameters object. + + .NOTES + The group 'Everyone' is automatically given read access by + the cmdlet New-SmbShare if all access permission parameters + (FullAccess, ChangeAccess, ReadAccess, NoAccess) is set to @(). + For that reason we need neither of the parameters, or at least + one to specify an account. +#> +function Assert-AccessPermissionParameters +{ + param + ( + [Parameter()] + [System.String[]] + $FullAccess, + + [Parameter()] + [System.String[]] + $ChangeAccess, + + [Parameter()] + [System.String[]] + $ReadAccess, + + [Parameter()] + [System.String[]] + $NoAccess, + + [Parameter(ValueFromRemainingArguments)] + [System.Collections.Generic.List`1[System.Object]] + $RemainingParameters + ) + + <# + First check if ReadAccess is monitored (part of the configuration). + If it is not monitored, then we don't need to worry if Everyone is + added. + #> + if ($PSBoundParameters.ContainsKey('ReadAccess') -and -not $ReadAccess) + { + $fullAccessIsEmpty = $PSBoundParameters.ContainsKey('FullAccess') -and -not $FullAccess + $changeAccessIsEmpty = $PSBoundParameters.ContainsKey('ChangeAccess') -and -not $ChangeAccess + $noAccessIsEmpty = $PSBoundParameters.ContainsKey('NoAccess') -and -not $NoAccess + + <# + If ReadAccess should have no members, then we need at least one + member in one of the other access permission collections. + #> + if ($fullAccessIsEmpty -and $changeAccessIsEmpty -and $noAccessIsEmpty) + { + New-InvalidArgumentException -Message $script:localizedData.InvalidAccessParametersCombination -ArgumentName 'FullAccess, ChangeAccess, ReadAccess, NoAccess' + } + } +} diff --git a/deployment/dsc/azshcihost/ComputerManagementDsc/8.4.0/DSCResources/DSC_SmbShare/DSC_SmbShare.schema.mof b/deployment/dsc/azshcihost/ComputerManagementDsc/8.4.0/DSCResources/DSC_SmbShare/DSC_SmbShare.schema.mof new file mode 100644 index 0000000..2c00fff --- /dev/null +++ b/deployment/dsc/azshcihost/ComputerManagementDsc/8.4.0/DSCResources/DSC_SmbShare/DSC_SmbShare.schema.mof @@ -0,0 +1,27 @@ + +[ClassVersion("1.0.0.0"), FriendlyName("SmbShare")] +class DSC_SmbShare : OMI_BaseResource +{ + [Key, Description("Specifies the name of the SMB share.")] String Name; + [Required, Description("Specifies the path of the SMB share.")] String Path; + [Write, Description("Specifies the description of the SMB share.")] String Description; + [Write, Description("Specifies which accounts will be granted modify permission to access the SMB share.")] String ChangeAccess[]; + [Write, Description("Specifies the maximum number of concurrently connected users that the new SMB share may accommodate. If this parameter is set to zero (0), then the number of users is unlimited. The default value is zero (0).")] Uint32 ConcurrentUserLimit; + [Write, Description("Indicates that the SMB share is encrypted.")] Boolean EncryptData; + [Write, Description("Specifies which files and folders in the new SMB share are visible to users."), ValueMap{"AccessBased","Unrestricted"}, Values{"AccessBased","Unrestricted"}] String FolderEnumerationMode; + [Write, Description("Specifies the caching mode of the offline files for the SMB share."), ValueMap{"None","Manual","Programs","Documents","BranchCache"}, Values{"None","Manual","Programs","Documents","BranchCache"}] String CachingMode; + [Write, Description("Specifies whether the SMB share should be continuously available.")] Boolean ContinuouslyAvailable; + [Write, Description("Specifies which accounts are granted full permission to access the SMB share.")] String FullAccess[]; + [Write, Description("Specifies which accounts are denied access to the SMB share.")] String NoAccess[]; + [Write, Description("Specifies which accounts is granted read permission to access the SMB share.")] String ReadAccess[]; + [Write, Description("Specifies if the SMB share should be added or removed."), ValueMap{"Present","Absent"}, Values{"Present","Absent"}] String Ensure; + [Write, Description("Specifies if the SMB share is allowed to be dropped and recreated (required when the path changes).")] Boolean Force; + [Write, Description("Specifies the scope in which the share should be created.")] String ScopeName; + [Read, Description("Specifies the state of the SMB share.")] String ShareState; + [Read, Description("Specifies the type of the SMB share.")] String ShareType; + [Read, Description("Specifies if this SMB share is a ShadowCopy.")] Boolean ShadowCopy; + [Read, Description("Specifies if this SMB share is a special share. E.g. an admin share, default shares, or IPC$ share.")] Boolean Special; +}; + + + diff --git a/deployment/dsc/azshcihost/ComputerManagementDsc/8.4.0/DSCResources/DSC_SmbShare/README.md b/deployment/dsc/azshcihost/ComputerManagementDsc/8.4.0/DSCResources/DSC_SmbShare/README.md new file mode 100644 index 0000000..064cc21 --- /dev/null +++ b/deployment/dsc/azshcihost/ComputerManagementDsc/8.4.0/DSCResources/DSC_SmbShare/README.md @@ -0,0 +1,40 @@ +# Description + +The resource is used to manage SMB shares, and access permissions to +SMB shares. + +## Requirements + +### Cluster Shares + +The property `ContinuouslyAvailable` can only be set to `$true` when +the SMB share is a cluster share in a failover cluster. Also in the blog +[SMB Transparent Failover – making file shares continuously available](https://blogs.technet.microsoft.com/filecab/2016/03/25/smb-transparent-failover-making-file-shares-continuously-available-2) +by [Claus Joergensen](https://github.com/clausjor) it is mentioned that +SMB Transparent Failover does not support cluster disks with 8.3 name +generation enabled. + +### Access permissions + +It is not allowed to provide empty collections in the configuration for +the access permissions parameters. The configuration below will cause an +exception to be thrown. + +```powershell +SmbShare 'Integration_Test' +{ + Name = 'TestShare' + Path = 'C:\Temp' + FullAccess = @() + ChangeAccess = @() + ReadAccess = @() + NoAccess = @() +} +``` + +The access permission parameters must either be all removed to manage +the access permission manually, or add at least one member to one of +the access permission parameters. If all the access permission parameters +are removed, then by design, the cmdlet New-SmbShare will add +the *Everyone* group with read access permission to the SMB share. +To prevent that, add a member to either access permission parameters. diff --git a/deployment/dsc/azshcihost/ComputerManagementDsc/8.4.0/DSCResources/DSC_SmbShare/en-US/DSC_SmbShare.schema.mfl b/deployment/dsc/azshcihost/ComputerManagementDsc/8.4.0/DSCResources/DSC_SmbShare/en-US/DSC_SmbShare.schema.mfl new file mode 100644 index 0000000..299a9d3 --- /dev/null +++ b/deployment/dsc/azshcihost/ComputerManagementDsc/8.4.0/DSCResources/DSC_SmbShare/en-US/DSC_SmbShare.schema.mfl @@ -0,0 +1,21 @@ +[Description("This resource is used to configure SMB shares.") : Amended,AMENDMENT, LOCALE("MS_409")] +class DSC_SmbShare : OMI_BaseResource +{ + [Key, Description("Specifies the name of the SMB share.") : Amended] String Name; + [Description("Specifies the path of the SMB share.") : Amended] String Path; + [Description("Specifies the description of the SMB share.") : Amended] String Description; + [Description("Specifies which accounts will be granted modify permission to access the SMB share.") : Amended] String ChangeAccess[]; + [Description("Specifies the maximum number of concurrently connected users that the new SMB share may accommodate. If this parameter is set to zero (0), then the number of users is unlimited. The default value is zero (0).") : Amended] Uint32 ConcurrentUserLimit; + [Description("Indicates that the SMB share is encrypted.") : Amended] Boolean EncryptData; + [Description("Specifies which files and folders in the new SMB share are visible to users.") : Amended] String FolderEnumerationMode; + [Description("Specifies the caching mode of the offline files for the SMB share.") : Amended] String CachingMode; + [Description("Specifies whether the SMB share should be continuously available.") : Amended] Boolean ContinuouslyAvailable; + [Description("Specifies which accounts are granted full permission to access the SMB share.") : Amended] String FullAccess[]; + [Description("Specifies which accounts are denied access to the SMB share.") : Amended] String NoAccess[]; + [Description("Specifies which accounts is granted read permission to access the SMB share.") : Amended] String ReadAccess[]; + [Description("Specifies if the SMB share should be added or removed.") : Amended] String Ensure; + [Description("Specifies the state of the SMB share.") : Amended] String ShareState; + [Description("Specifies the type of the SMB share.") : Amended] String ShareType; + [Description("Specifies if this SMB share is a ShadowCopy.") : Amended] String ShadowCopy; + [Description("Specifies if this SMB share is a special share. E.g. an admin share, default shares, or IPC$ share.") : Amended] String Special; +}; diff --git a/deployment/dsc/azshcihost/ComputerManagementDsc/8.4.0/DSCResources/DSC_SmbShare/en-US/DSC_SmbShare.strings.psd1 b/deployment/dsc/azshcihost/ComputerManagementDsc/8.4.0/DSCResources/DSC_SmbShare/en-US/DSC_SmbShare.strings.psd1 new file mode 100644 index 0000000..74c4ef2 --- /dev/null +++ b/deployment/dsc/azshcihost/ComputerManagementDsc/8.4.0/DSCResources/DSC_SmbShare/en-US/DSC_SmbShare.strings.psd1 @@ -0,0 +1,21 @@ +# Localized resources for WindowsOptionalFeature + +ConvertFrom-StringData @' + GetTargetResourceMessage = Getting the current state of the SMB share '{0}'. + TestTargetResourceMessage = Determining if the SMB share '{0}' is in the desired state. + ShareNotFound = Unable to find a SMB share with the name '{0}'. + IsPresent = The SMB share with the name '{0}' exist. + IsAbsent = The SMB share with the name '{0}' does not exist. + EvaluatingProperties = Evaluating the properties of the SMB share. + UpdatingProperties = Updating properties on the SMB share that are not in desired state. + RemoveShare = Removing the SMB share with the name '{0}'. + CreateShare = Creating a SMB share with the name '{0}'. + RecreateShare = Dropping and recreating share with name '{0}' + RecreateShareError = Failed to recreate share with name '{0}'. The error was: '{1}'. + NoRecreateShare = The share with name '{0}' exists on path {1}, desired state is on path {2}. Set Force = $true to allow drop and recreate of the share. + RevokeAccess = Revoking granted permission for account '{0}' on the SMB share with the name '{1}'. + UnblockAccess = Revoking denied permission for account '{0}' on the SMB share with the name '{1}'. + GrantAccess = Granting '{0}' permission for account '{1}' on the SMB share with the name '{2}'. + DenyAccess = Denying permission for account '{0}' on the SMB share with the name '{1}'. + InvalidAccessParametersCombination = Not allowed to have all access permission parameters set to empty collections. Must either remove the access permission parameters completely, or add at least one member to one of the access permission parameters. +'@ diff --git a/deployment/dsc/azshcihost/ComputerManagementDsc/8.4.0/DSCResources/DSC_SystemLocale/DSC_SystemLocale.psm1 b/deployment/dsc/azshcihost/ComputerManagementDsc/8.4.0/DSCResources/DSC_SystemLocale/DSC_SystemLocale.psm1 new file mode 100644 index 0000000..0b40865 --- /dev/null +++ b/deployment/dsc/azshcihost/ComputerManagementDsc/8.4.0/DSCResources/DSC_SystemLocale/DSC_SystemLocale.psm1 @@ -0,0 +1,191 @@ +$modulePath = Join-Path -Path (Split-Path -Path (Split-Path -Path $PSScriptRoot -Parent) -Parent) -ChildPath 'Modules' + +# Import the ComputerManagementDsc Common Modules +Import-Module -Name (Join-Path -Path $modulePath ` + -ChildPath (Join-Path -Path 'ComputerManagementDsc.Common' ` + -ChildPath 'ComputerManagementDsc.Common.psm1')) -Force + +Import-Module -Name (Join-Path -Path $modulePath -ChildPath 'DscResource.Common') + +# Import Localization Strings +$script:localizedData = Get-LocalizedData -DefaultUICulture 'en-US' + +<# + .SYNOPSIS + Returns the current System Local on the node. + + .PARAMETER IsSingleInstance + Specifies the resource is a single instance, the value must be 'Yes'. + + .PARAMETER SystemLocale + Specifies the System Locale. +#> +function Get-TargetResource +{ + [CmdletBinding()] + [OutputType([System.Collections.Hashtable])] + param + ( + [Parameter(Mandatory = $true)] + [ValidateSet('Yes')] + [System.String] + $IsSingleInstance, + + [Parameter(Mandatory = $true)] + [System.String] + $SystemLocale + ) + + Write-Verbose -Message ( @( + "$($MyInvocation.MyCommand): " + $($script:localizedData.GettingSystemLocaleMessage) + ) -join '' ) + + # Get the current System Locale + $currentSystemLocale = Get-WinSystemLocale ` + -ErrorAction Stop + + # Generate the return object. + $returnValue = @{ + IsSingleInstance = $IsSingleInstance + SystemLocale = $currentSystemLocale.Name + } + + return $returnValue +} # Get-TargetResource + +<# + .SYNOPSIS + Sets the current System Locale on the node. + + .PARAMETER IsSingleInstance + Specifies the resource is a single instance, the value must be 'Yes'. + + .PARAMETER SystemLocale + Specifies the System Locale. +#> +function Set-TargetResource +{ + # Suppressing this rule because $global:DSCMachineStatus is used to trigger a reboot when there are pending changes. + [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidGlobalVars', '')] + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [ValidateSet('Yes')] + [System.String] + $IsSingleInstance, + + [Parameter(Mandatory = $true)] + [System.String] + $SystemLocale + ) + + Write-Verbose -Message ( @( + "$($MyInvocation.MyCommand): " + $($script:localizedData.SettingSystemLocaleMessage) + ) -join '' ) + + # Get the current System Locale + $currentSystemLocale = Get-WinSystemLocale ` + -ErrorAction Stop + + if ($currentSystemLocale.Name -ne $SystemLocale) + { + Set-WinSystemLocale ` + -SystemLocale $SystemLocale ` + -ErrorAction Stop + + $global:DSCMachineStatus = 1 + + Write-Verbose -Message ( @( + "$($MyInvocation.MyCommand): " + $($script:localizedData.SystemLocaleUpdatedMessage -f $SystemLocale) + ) -join '' ) + } +} # Set-TargetResource + +<# + .SYNOPSIS + Tests if the current System Locale on the node needs to be changed. + + .PARAMETER IsSingleInstance + Specifies the resource is a single instance, the value must be 'Yes'. + + .PARAMETER SystemLocale + Specifies the System Locale. + + .OUTPUTS + Returns false if the System Locale needs to be changed or true if it is correct. +#> +function Test-TargetResource +{ + [CmdletBinding()] + [OutputType([System.Boolean])] + param + ( + [Parameter(Mandatory = $true)] + [ValidateSet('Yes')] + [System.String] + $IsSingleInstance, + + [Parameter(Mandatory = $true)] + [System.String] + $SystemLocale + ) + + Write-Verbose -Message ( @( + "$($MyInvocation.MyCommand): " + $($script:localizedData.TestingSystemLocaleMessage) + ) -join '' ) + + if (-not (Test-SystemLocaleValue -SystemLocale $SystemLocale)) + { + New-InvalidArgumentException ` + -Message ($script:localizedData.InvalidSystemLocaleError -f $SystemLocale) ` + -ArgumentName 'SystemLocale' + } # if + + # Get the current System Locale + $currentSystemLocale = Get-WinSystemLocale ` + -ErrorAction Stop + + if ($currentSystemLocale.Name -ne $SystemLocale) + { + Write-Verbose -Message ( @( + "$($MyInvocation.MyCommand): " + $($script:localizedData.SystemLocaleParameterNeedsUpdateMessage -f ` + $currentSystemLocale.Name,$SystemLocale) + ) -join '' ) + + return $false + } + return $true +} # Test-TargetResource + +<# + .SYNOPSIS + Checks the provided System Locale against the list of valid cultures. + + .PARAMETER SystemLocale + The System Locale to check the validitiy of. +#> +function Test-SystemLocaleValue +{ + [CmdletBinding()] + [OutputType([System.Boolean])] + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $SystemLocale + ) + + $validCultures = [System.Globalization.CultureInfo]::GetCultures(` + [System.Globalization.CultureTypes]::AllCultures` + ).name + + return ($SystemLocale -in $validCultures) +} + +Export-ModuleMember -Function *-TargetResource diff --git a/deployment/dsc/azshcihost/ComputerManagementDsc/8.4.0/DSCResources/DSC_SystemLocale/DSC_SystemLocale.schema.mof b/deployment/dsc/azshcihost/ComputerManagementDsc/8.4.0/DSCResources/DSC_SystemLocale/DSC_SystemLocale.schema.mof new file mode 100644 index 0000000..caaf3ee --- /dev/null +++ b/deployment/dsc/azshcihost/ComputerManagementDsc/8.4.0/DSCResources/DSC_SystemLocale/DSC_SystemLocale.schema.mof @@ -0,0 +1,6 @@ +[ClassVersion("1.0.0.0"), FriendlyName("SystemLocale")] +class DSC_SystemLocale : OMI_BaseResource +{ + [Key, Description("Specifies the resource is a single instance, the value must be 'Yes'"), ValueMap{"Yes"}, Values{"Yes"}] String IsSingleInstance; + [Required, Description("Specifies the System Locale.")] String SystemLocale; +}; diff --git a/deployment/dsc/azshcihost/ComputerManagementDsc/8.4.0/DSCResources/DSC_SystemLocale/README.md b/deployment/dsc/azshcihost/ComputerManagementDsc/8.4.0/DSCResources/DSC_SystemLocale/README.md new file mode 100644 index 0000000..e509055 --- /dev/null +++ b/deployment/dsc/azshcihost/ComputerManagementDsc/8.4.0/DSCResources/DSC_SystemLocale/README.md @@ -0,0 +1,10 @@ +# Description + +Ths resource is used set the system locale on a Windows machine. + +To get a list of valid Windows System Locales use the command: +`[System.Globalization.CultureInfo]::GetCultures([System.Globalization.CultureTypes]::AllCultures).name` + +If the System Locale is changed by this resource, it will require the node +to reboot. If the LCM is not configured to allow restarting, the configuration +will not be able to be applied until a manual restart occurs. diff --git a/deployment/dsc/azshcihost/ComputerManagementDsc/8.4.0/DSCResources/DSC_SystemLocale/en-US/DSC_SystemLocale.strings.psd1 b/deployment/dsc/azshcihost/ComputerManagementDsc/8.4.0/DSCResources/DSC_SystemLocale/en-US/DSC_SystemLocale.strings.psd1 new file mode 100644 index 0000000..3923e6d --- /dev/null +++ b/deployment/dsc/azshcihost/ComputerManagementDsc/8.4.0/DSCResources/DSC_SystemLocale/en-US/DSC_SystemLocale.strings.psd1 @@ -0,0 +1,9 @@ +# culture="en-US" +ConvertFrom-StringData -StringData @' + GettingSystemLocaleMessage = Getting Windows system locale. + SettingSystemLocaleMessage = Setting Windows system locale. + SystemLocaleUpdatedMessage = Windows system locale updated to "{0}". A system restart is required. + TestingSystemLocaleMessage = Testing Windows system locale. + SystemLocaleParameterNeedsUpdateMessage = Windows system locale is "{0}" but should be "{1}". Change required. + InvalidSystemLocaleError = The Windows system locale "{0}" is invalid. +'@ diff --git a/deployment/dsc/azshcihost/ComputerManagementDsc/8.4.0/DSCResources/DSC_TimeZone/DSC_TimeZone.psm1 b/deployment/dsc/azshcihost/ComputerManagementDsc/8.4.0/DSCResources/DSC_TimeZone/DSC_TimeZone.psm1 new file mode 100644 index 0000000..d7aee5e --- /dev/null +++ b/deployment/dsc/azshcihost/ComputerManagementDsc/8.4.0/DSCResources/DSC_TimeZone/DSC_TimeZone.psm1 @@ -0,0 +1,125 @@ +$modulePath = Join-Path -Path (Split-Path -Path (Split-Path -Path $PSScriptRoot -Parent) -Parent) -ChildPath 'Modules' + +# Import the ComputerManagementDsc Common Modules +Import-Module -Name (Join-Path -Path $modulePath ` + -ChildPath (Join-Path -Path 'ComputerManagementDsc.Common' ` + -ChildPath 'ComputerManagementDsc.Common.psm1')) + +Import-Module -Name (Join-Path -Path $modulePath -ChildPath 'DscResource.Common') + +# Import Localization Strings. +$script:localizedData = Get-LocalizedData -DefaultUICulture 'en-US' + +<# + .SYNOPSIS + Returns the current time zone of the node. + + .PARAMETER IsSingleInstance + Specifies the resource is a single instance, the value must be 'Yes'. + + .PARAMETER TimeZone + Specifies the time zone. +#> +function Get-TargetResource +{ + [CmdletBinding()] + [OutputType([Hashtable])] + param + ( + [Parameter(Mandatory = $true)] + [ValidateSet('Yes')] + [System.String] + $IsSingleInstance, + + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $TimeZone + ) + + Write-Verbose -Message ($script:localizedData.GettingTimeZoneMessage) + + # Get the current time zone Id. + $currentTimeZone = Get-TimeZoneId + + $returnValue = @{ + IsSingleInstance = 'Yes' + TimeZone = $currentTimeZone + } + + # Output the target resource. + return $returnValue +} + +<# + .SYNOPSIS + Sets the current time zone of the node. + + .PARAMETER IsSingleInstance + Specifies the resource is a single instance, the value must be 'Yes'. + + .PARAMETER TimeZone + Specifies the time zone. +#> +function Set-TargetResource +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [ValidateSet('Yes')] + [System.String] + $IsSingleInstance, + + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $TimeZone + ) + + $currentTimeZone = Get-TimeZoneId + + if ($currentTimeZone -ne $TimeZone) + { + Write-Verbose -Message ($script:localizedData.SettingTimeZoneMessage) + Set-TimeZoneId -TimeZone $TimeZone + } + else + { + Write-Verbose -Message ($script:localizedData.TimeZoneAlreadySetMessage -f $TimeZone) + } +} + +<# + .SYNOPSIS + Tests the current time zone of the node. + + .PARAMETER IsSingleInstance + Specifies the resource is a single instance, the value must be 'Yes'. + + .PARAMETER TimeZone + Specifies the time zone. +#> +function Test-TargetResource +{ + [CmdletBinding()] + [OutputType([System.Boolean])] + param + ( + [Parameter(Mandatory = $true)] + [ValidateSet('Yes')] + [System.String] + $IsSingleInstance, + + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $TimeZone + ) + + Write-Verbose -Message ($script:localizedData.TestingTimeZoneMessage) + + return Test-TimeZoneId -TimeZoneId $TimeZone +} + +Export-ModuleMember -Function *-TargetResource diff --git a/deployment/dsc/azshcihost/ComputerManagementDsc/8.4.0/DSCResources/DSC_TimeZone/DSC_TimeZone.schema.mof b/deployment/dsc/azshcihost/ComputerManagementDsc/8.4.0/DSCResources/DSC_TimeZone/DSC_TimeZone.schema.mof new file mode 100644 index 0000000..e272ca8 --- /dev/null +++ b/deployment/dsc/azshcihost/ComputerManagementDsc/8.4.0/DSCResources/DSC_TimeZone/DSC_TimeZone.schema.mof @@ -0,0 +1,6 @@ +[ClassVersion("1.0.0.0"), FriendlyName("TimeZone")] +class DSC_TimeZone : OMI_BaseResource +{ + [Key, Description("Specifies the resource is a single instance, the value must be 'Yes'."), ValueMap{"Yes"}, Values{"Yes"}] String IsSingleInstance; + [Required, Description("Specifies the TimeZone.")] String TimeZone; +}; diff --git a/deployment/dsc/azshcihost/ComputerManagementDsc/8.4.0/DSCResources/DSC_TimeZone/README.md b/deployment/dsc/azshcihost/ComputerManagementDsc/8.4.0/DSCResources/DSC_TimeZone/README.md new file mode 100644 index 0000000..871a701 --- /dev/null +++ b/deployment/dsc/azshcihost/ComputerManagementDsc/8.4.0/DSCResources/DSC_TimeZone/README.md @@ -0,0 +1,7 @@ +# Description + +The resource will use the `Get-TimeZone` cmdlet to get the current +time zone. If `Get-TimeZone` is not available them CIM will be used to retrieve +the current time zone. To update the time zone, .NET reflection will be used to +update the time zone if required. If .NET reflection is not supported on the node +(in the case of Nano Server) then tzutil.exe will be used to set the time zone. diff --git a/deployment/dsc/azshcihost/ComputerManagementDsc/8.4.0/DSCResources/DSC_TimeZone/en-US/DSC_TimeZone.strings.psd1 b/deployment/dsc/azshcihost/ComputerManagementDsc/8.4.0/DSCResources/DSC_TimeZone/en-US/DSC_TimeZone.strings.psd1 new file mode 100644 index 0000000..896ca1d --- /dev/null +++ b/deployment/dsc/azshcihost/ComputerManagementDsc/8.4.0/DSCResources/DSC_TimeZone/en-US/DSC_TimeZone.strings.psd1 @@ -0,0 +1,7 @@ +# culture="en-US" +ConvertFrom-StringData -StringData @' + GettingTimeZoneMessage = Getting the time zone. + SettingTimeZoneMessage = Setting the time zone. + TimeZoneAlreadySetMessage = Time zone already set to {0}. + TestingTimeZoneMessage = Testing the time zone. +'@ diff --git a/deployment/dsc/azshcihost/ComputerManagementDsc/8.4.0/DSCResources/DSC_UserAccountControl/DSC_UserAccountControl.psm1 b/deployment/dsc/azshcihost/ComputerManagementDsc/8.4.0/DSCResources/DSC_UserAccountControl/DSC_UserAccountControl.psm1 new file mode 100644 index 0000000..b740d7c --- /dev/null +++ b/deployment/dsc/azshcihost/ComputerManagementDsc/8.4.0/DSCResources/DSC_UserAccountControl/DSC_UserAccountControl.psm1 @@ -0,0 +1,591 @@ +$modulePath = Join-Path -Path (Split-Path -Path (Split-Path -Path $PSScriptRoot -Parent) -Parent) -ChildPath 'Modules' + +# Import the ComputerManagementDsc Common Modules +Import-Module -Name (Join-Path -Path $modulePath ` + -ChildPath (Join-Path -Path 'ComputerManagementDsc.Common' ` + -ChildPath 'ComputerManagementDsc.Common.psm1')) + +Import-Module -Name (Join-Path -Path $modulePath -ChildPath 'DscResource.Common') + +# Import Localization Strings +$script:localizedData = Get-LocalizedData -DefaultUICulture 'en-US' + +$script:registryKey = 'HKLM:\HKEY_LOCAL_MACHINE\Software\Microsoft\Windows\CurrentVersion\Policies\System' + +$script:granularUserAccountControlParameterNames = @( + 'FilterAdministratorToken' + 'ConsentPromptBehaviorAdmin' + 'ConsentPromptBehaviorUser' + 'EnableInstallerDetection' + 'ValidateAdminCodeSignatures' + 'EnableLua' + 'PromptOnSecureDesktop' + 'EnableVirtualization' +) + +<# + .SYNOPSIS + Gets the current state of the User Account Control. + + .PARAMETER IsSingleInstance + Specifies the resource is a single instance, the value must be 'Yes'. + + .PARAMETER SuppressRestart + Specifies if a restart of the node should be suppressed. By default the + node will be restarted if the value is changed. +#> +function Get-TargetResource +{ + [CmdletBinding()] + [OutputType([System.Collections.Hashtable])] + param + ( + # This is best practice when writing a single-instance DSC resource. + [Parameter(Mandatory = $true)] + [ValidateSet('Yes')] + [System.String] + $IsSingleInstance, + + [Parameter()] + [System.Boolean] + $SuppressRestart = $false + ) + + Write-Verbose -Message $script:localizedData.GettingStateMessage + + $userAccountControlValues = Get-UserAccountControl + + $returnValue = @{ + IsSingleInstance = 'Yes' + NotificationLevel = Get-NotificationLevel + FilterAdministratorToken = $userAccountControlValues.FilterAdministratorToken + ConsentPromptBehaviorAdmin = $userAccountControlValues.ConsentPromptBehaviorAdmin + ConsentPromptBehaviorUser = $userAccountControlValues.ConsentPromptBehaviorUser + EnableInstallerDetection = $userAccountControlValues.EnableInstallerDetection + ValidateAdminCodeSignatures = $userAccountControlValues.ValidateAdminCodeSignatures + EnableLua = $userAccountControlValues.EnableLua + PromptOnSecureDesktop = $userAccountControlValues.PromptOnSecureDesktop + EnableVirtualization = $userAccountControlValues.EnableVirtualization + SuppressRestart = $SuppressRestart + } + + return $returnValue +} + +<# + .SYNOPSIS + Sets the current state of the User Account Control. + + .PARAMETER IsSingleInstance + Specifies the resource is a single instance, the value must be 'Yes'. + + .PARAMETER NotificationLevel + Specifies the desired notification level for the User Account Control + setting. This parameter can not be used at the same time as any of the + granular parameters. + + .PARAMETER FilterAdministratorToken + Specifies the mode for the built-in administrator account (RID 500). + + .PARAMETER ConsentPromptBehaviorAdmin + Specifies the prompt behavior for the Consent Administrator. + + .PARAMETER ConsentPromptBehaviorUser + Specifies how the operations that requires elevation is handled for users. + + .PARAMETER EnableInstallerDetection + Specifies how package installations are handled. + + .PARAMETER ValidateAdminCodeSignatures + Specifies how cryptographic signatures on interactive applications are + handled. + + .PARAMETER EnableLua + Specifies how the 'administrator in Admin Approval Mode' user type are + handled. + + .PARAMETER PromptOnSecureDesktop + Specifies if secure desktop prompting are used. + + .PARAMETER EnableVirtualization + Specifies how redirection of legacy application File and Registry writes + are handled. + + .PARAMETER SuppressRestart + Specifies if a restart of the node should be suppressed. By default the + node will be restarted if the value is changed. +#> +function Set-TargetResource +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [ValidateSet('Yes')] + [System.String] + $IsSingleInstance, + + [Parameter()] + [ValidateSet('AlwaysNotify', 'AlwaysNotifyAndAskForCredentials', 'NotifyChanges', 'NotifyChangesWithoutDimming', 'NeverNotify', 'NeverNotifyAndDisableAll')] + [System.String] + $NotificationLevel, + + [Parameter()] + [ValidateSet(0, 1)] + [System.UInt16] + $FilterAdministratorToken, + + [Parameter()] + [ValidateSet(0, 1, 2, 3, 4, 5)] + [System.UInt16] + $ConsentPromptBehaviorAdmin, + + [Parameter()] + [ValidateSet(0, 1, 3)] + [System.UInt16] + $ConsentPromptBehaviorUser, + + [Parameter()] + [ValidateSet(0, 1)] + [System.UInt16] + $EnableInstallerDetection, + + [Parameter()] + [ValidateSet(0, 1)] + [System.UInt16] + $ValidateAdminCodeSignatures, + + [Parameter()] + [ValidateSet(0, 1)] + [System.UInt16] + $EnableLua, + + [Parameter()] + [ValidateSet(0, 1)] + [System.UInt16] + $PromptOnSecureDesktop, + + [Parameter()] + [ValidateSet(0, 1)] + [System.UInt16] + $EnableVirtualization, + + [Parameter()] + [System.Boolean] + $SuppressRestart = $false + ) + + $assertBoundParameterParameters = @{ + BoundParameterList = $PSBoundParameters + MutuallyExclusiveList1 = @( + 'NotificationLevel' + ) + MutuallyExclusiveList2 = $script:granularUserAccountControlParameterNames + } + + Assert-BoundParameter @assertBoundParameterParameters + + Write-Verbose -Message $script:localizedData.SettingStateMessage + + $needRestart = $false + + $getTargetResourceResult = Get-TargetResource -IsSingleInstance 'Yes' -SuppressRestart $SuppressRestart + + if ($PSBoundParameters.ContainsKey('NotificationLevel')) + { + if ($getTargetResourceResult.NotificationLevel -ne $NotificationLevel) + { + Write-Verbose -Message ( + $script:localizedData.SetNotificationLevel -f $NotificationLevel + ) + + Set-UserAccountControlToNotificationLevel -NotificationLevel $NotificationLevel + + $needRestart = $true + } + else + { + Write-Verbose -Message $script:localizedData.NotificationLevelInDesiredState + } + } + else + { + foreach ($parameterName in $script:granularUserAccountControlParameterNames) + { + if ($PSBoundParameters.ContainsKey($parameterName) -and $getTargetResourceResult.$parameterName -ne $PSBoundParameters.$parameterName) + { + Write-Verbose -Message ( + $script:localizedData.SetPropertyToValue ` + -f $parameterName, $PSBoundParameters.$parameterName + ) + + try + { + $setItemPropertyParameters = @{ + Path = $script:registryKey + Name = $parameterName + Value = $PSBoundParameters.$parameterName + ErrorAction = 'Stop' + } + + Set-ItemProperty @setItemPropertyParameters + } + catch + { + New-InvalidOperationException ` + -Message ($script:localizedData.FailedToSetGranularProperty -f $parameterName) ` + -ErrorRecord $_ + } + + $needRestart = $true + } + } + + if (-not $needRestart) + { + Write-Verbose -Message $script:localizedData.GranularPropertiesInDesiredState + } + } + + if ($needRestart) + { + if ($SuppressRestart) + { + Write-Warning -Message $script:localizedData.SuppressRestart + } + else + { + $global:DSCMachineStatus = 1 + } + } +} + +<# + .SYNOPSIS + Tests the current state of the User Account Control. + + .PARAMETER IsSingleInstance + Specifies the resource is a single instance, the value must be 'Yes'. + + .PARAMETER NotificationLevel + Specifies the desired notification level for the User Account Control + setting. This parameter can not be used at the same time as any of the + granular parameters. + + .PARAMETER FilterAdministratorToken + Specifies the mode for the built-in administrator account (RID 500). + + .PARAMETER ConsentPromptBehaviorAdmin + Specifies the prompt behavior for the Consent Administrator. + + .PARAMETER ConsentPromptBehaviorUser + Specifies how the operations that requires elevation is handled for users. + + .PARAMETER EnableInstallerDetection + Specifies how package installations are handled. + + .PARAMETER ValidateAdminCodeSignatures + Specifies how cryptographic signatures on interactive applications are + handled. + + .PARAMETER EnableLua + Specifies how the 'administrator in Admin Approval Mode' user type are + handled. + + .PARAMETER PromptOnSecureDesktop + Specifies if secure desktop prompting are used. + + .PARAMETER EnableVirtualization + Specifies how redirection of legacy application File and Registry writes + are handled. + + .PARAMETER SuppressRestart + Specifies if a restart of the node should be suppressed. By default the + node will be restarted if the value is changed. +#> +function Test-TargetResource +{ + [CmdletBinding()] + [OutputType([System.Boolean])] + param + ( + [Parameter(Mandatory = $true)] + [ValidateSet('Yes')] + [System.String] + $IsSingleInstance, + + [Parameter()] + [ValidateSet('AlwaysNotify', 'AlwaysNotifyAndAskForCredentials', 'NotifyChanges', 'NotifyChangesWithoutDimming', 'NeverNotify', 'NeverNotifyAndDisableAll')] + [System.String] + $NotificationLevel, + + [Parameter()] + [ValidateSet(0, 1)] + [System.UInt16] + $FilterAdministratorToken, + + [Parameter()] + [ValidateSet(0, 1, 2, 3, 4, 5)] + [System.UInt16] + $ConsentPromptBehaviorAdmin, + + [Parameter()] + [ValidateSet(0, 1, 3)] + [System.UInt16] + $ConsentPromptBehaviorUser, + + [Parameter()] + [ValidateSet(0, 1)] + [System.UInt16] + $EnableInstallerDetection, + + [Parameter()] + [ValidateSet(0, 1)] + [System.UInt16] + $ValidateAdminCodeSignatures, + + [Parameter()] + [ValidateSet(0, 1)] + [System.UInt16] + $EnableLua, + + [Parameter()] + [ValidateSet(0, 1)] + [System.UInt16] + $PromptOnSecureDesktop, + + [Parameter()] + [ValidateSet(0, 1)] + [System.UInt16] + $EnableVirtualization, + + [Parameter()] + [System.Boolean] + $SuppressRestart = $false + ) + + Write-Verbose -Message $script:localizedData.TestingStateMessage + + $assertBoundParameterParameters = @{ + BoundParameterList = $PSBoundParameters + MutuallyExclusiveList1 = @( + 'NotificationLevel' + ) + MutuallyExclusiveList2 = $script:granularUserAccountControlParameterNames + } + + Assert-BoundParameter @assertBoundParameterParameters + + $getTargetResourceResult = Get-TargetResource -IsSingleInstance 'Yes' -SuppressRestart $SuppressRestart + + if ($PSBoundParameters.ContainsKey('NotificationLevel')) + { + if ($getTargetResourceResult.NotificationLevel -ne $NotificationLevel) + { + $testTargetResourceReturnValue = $false + + Write-Verbose -Message ($script:localizedData.NotificationLevelNoInDesiredState -f $getTargetResourceResult.NotificationLevel, $NotificationLevel) + } + else + { + $testTargetResourceReturnValue = $true + + Write-Verbose -Message $script:localizedData.NotificationLevelInDesiredState + } + } + else + { + $testTargetResourceReturnValue = $true + + foreach ($parameterName in $script:granularUserAccountControlParameterNames) + { + if ($PSBoundParameters.ContainsKey($parameterName) -and $getTargetResourceResult.$parameterName -ne $PSBoundParameters.$parameterName) + { + $testTargetResourceReturnValue = $false + + Write-Verbose -Message ($script:localizedData.GranularPropertyNoInDesiredState -f $parameterName, $getTargetResourceResult.$parameterName, $PSBoundParameters.$parameterName) + } + } + + if ($testTargetResourceReturnValue) + { + Write-Verbose -Message $script:localizedData.GranularPropertiesInDesiredState + } + } + + return $testTargetResourceReturnValue +} + +<# + .SYNOPSIS + Gets the current values of the User Account Control registry entries. + + .OUTPUTS + Returns a hashtable containing the values. +#> +function Get-UserAccountControl +{ + [CmdletBinding()] + [OutputType([System.Collections.Hashtable])] + param () + + return @{ + FilterAdministratorToken = Get-RegistryPropertyValue -Path $script:registryKey -Name 'FilterAdministratorToken' + ConsentPromptBehaviorAdmin = Get-RegistryPropertyValue -Path $script:registryKey -Name 'ConsentPromptBehaviorAdmin' + ConsentPromptBehaviorUser = Get-RegistryPropertyValue -Path $script:registryKey -Name 'ConsentPromptBehaviorUser' + EnableInstallerDetection = Get-RegistryPropertyValue -Path $script:registryKey -Name 'EnableInstallerDetection' + ValidateAdminCodeSignatures = Get-RegistryPropertyValue -Path $script:registryKey -Name 'ValidateAdminCodeSignatures' + EnableLua = Get-RegistryPropertyValue -Path $script:registryKey -Name 'EnableLUA' + PromptOnSecureDesktop = Get-RegistryPropertyValue -Path $script:registryKey -Name 'PromptOnSecureDesktop' + EnableVirtualization = Get-RegistryPropertyValue -Path $script:registryKey -Name 'EnableVirtualization' + } +} + +<# + .SYNOPSIS + Gets the current notification level string value. + + .OUTPUTS + Returns the notification level string value. If the registry values does + not match a predefined notification level then $null is returned. +#> +function Get-NotificationLevel +{ + [CmdletBinding()] + [OutputType([System.String])] + param () + + $notificationLevelStringValue = $null + + $userAccountControlValues = Get-UserAccountControl + + if ($userAccountControlValues.ConsentPromptBehaviorAdmin -eq 2 ` + -and $userAccountControlValues.EnableLua -eq 1 ` + -and $userAccountControlValues.PromptOnSecureDesktop -eq 1 + ) + { + $notificationLevelStringValue = 'AlwaysNotify' + } + + if ($userAccountControlValues.ConsentPromptBehaviorAdmin -eq 1 ` + -and $userAccountControlValues.EnableLua -eq 1 ` + -and $userAccountControlValues.PromptOnSecureDesktop -eq 1 + ) + { + $notificationLevelStringValue = 'AlwaysNotifyAndAskForCredentials' + } + + if ($userAccountControlValues.ConsentPromptBehaviorAdmin -eq 5 ` + -and $userAccountControlValues.EnableLua -eq 1 ` + -and $userAccountControlValues.PromptOnSecureDesktop -eq 1 + ) + { + $notificationLevelStringValue = 'NotifyChanges' + } + + if ($userAccountControlValues.ConsentPromptBehaviorAdmin -eq 5 ` + -and $userAccountControlValues.EnableLua -eq 1 ` + -and $userAccountControlValues.PromptOnSecureDesktop -eq 0 + ) + { + $notificationLevelStringValue = 'NotifyChangesWithoutDimming' + } + + if ($userAccountControlValues.ConsentPromptBehaviorAdmin -eq 0 ` + -and $userAccountControlValues.EnableLua -eq 1 ` + -and $userAccountControlValues.PromptOnSecureDesktop -eq 0 + ) + { + $notificationLevelStringValue = 'NeverNotify' + } + + if ($userAccountControlValues.ConsentPromptBehaviorAdmin -eq 0 ` + -and $userAccountControlValues.EnableLua -eq 0 ` + -and $userAccountControlValues.PromptOnSecureDesktop -eq 0 + ) + { + $notificationLevelStringValue = 'NeverNotifyAndDisableAll' + } + + return $notificationLevelStringValue +} + +<# + .SYNOPSIS + Gets the current notification level string value. + + .OUTPUTS + Returns the notification level string value. If the registry values does + not match a predefined notification level then $null is returned. +#> +function Set-UserAccountControlToNotificationLevel +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [ValidateSet('AlwaysNotify', 'AlwaysNotifyAndAskForCredentials', 'NotifyChanges', 'NotifyChangesWithoutDimming', 'NeverNotify', 'NeverNotifyAndDisableAll')] + [System.String] + $NotificationLevel + ) + + try + { + $defaultSetItemPropertyParameters = @{ + Path = $script:registryKey + ErrorAction = 'Stop' + } + + switch ($NotificationLevel) + { + 'AlwaysNotify' + { + Set-ItemProperty @defaultSetItemPropertyParameters -Name 'ConsentPromptBehaviorAdmin' -Value 2 + Set-ItemProperty @defaultSetItemPropertyParameters -Name 'EnableLUA' -Value 1 + Set-ItemProperty @defaultSetItemPropertyParameters -Name 'PromptOnSecureDesktop' -Value 1 + } + + 'AlwaysNotifyAndAskForCredentials' + { + Set-ItemProperty @defaultSetItemPropertyParameters -Name 'ConsentPromptBehaviorAdmin' -Value 1 + Set-ItemProperty @defaultSetItemPropertyParameters -Name 'EnableLUA' -Value 1 + Set-ItemProperty @defaultSetItemPropertyParameters -Name 'PromptOnSecureDesktop' -Value 1 + } + + + 'NotifyChanges' + { + Set-ItemProperty @defaultSetItemPropertyParameters -Name 'ConsentPromptBehaviorAdmin' -Value 5 + Set-ItemProperty @defaultSetItemPropertyParameters -Name 'EnableLUA' -Value 1 + Set-ItemProperty @defaultSetItemPropertyParameters -Name 'PromptOnSecureDesktop' -Value 1 + } + + 'NotifyChangesWithoutDimming' + { + Set-ItemProperty @defaultSetItemPropertyParameters -Name 'ConsentPromptBehaviorAdmin' -Value 5 + Set-ItemProperty @defaultSetItemPropertyParameters -Name 'EnableLUA' -Value 1 + Set-ItemProperty @defaultSetItemPropertyParameters -Name 'PromptOnSecureDesktop' -Value 0 + } + + 'NeverNotify' + { + Set-ItemProperty @defaultSetItemPropertyParameters -Name 'ConsentPromptBehaviorAdmin' -Value 0 + Set-ItemProperty @defaultSetItemPropertyParameters -Name 'EnableLUA' -Value 1 + Set-ItemProperty @defaultSetItemPropertyParameters -Name 'PromptOnSecureDesktop' -Value 0 + } + + 'NeverNotifyAndDisableAll' + { + Set-ItemProperty @defaultSetItemPropertyParameters -Name 'ConsentPromptBehaviorAdmin' -Value 0 + Set-ItemProperty @defaultSetItemPropertyParameters -Name 'EnableLUA' -Value 0 + Set-ItemProperty @defaultSetItemPropertyParameters -Name 'PromptOnSecureDesktop' -Value 0 + } + } + } + catch + { + New-InvalidOperationException ` + -Message ($script:localizedData.FailedToSetNotificationLevel -f $NotificationLevel) ` + -ErrorRecord $_ + } +} diff --git a/deployment/dsc/azshcihost/ComputerManagementDsc/8.4.0/DSCResources/DSC_UserAccountControl/DSC_UserAccountControl.schema.mof b/deployment/dsc/azshcihost/ComputerManagementDsc/8.4.0/DSCResources/DSC_UserAccountControl/DSC_UserAccountControl.schema.mof new file mode 100644 index 0000000..7d41303 --- /dev/null +++ b/deployment/dsc/azshcihost/ComputerManagementDsc/8.4.0/DSCResources/DSC_UserAccountControl/DSC_UserAccountControl.schema.mof @@ -0,0 +1,15 @@ +[ClassVersion("1.0.0.0"), FriendlyName("UserAccountControl")] +class DSC_UserAccountControl : OMI_BaseResource +{ + [Key, Description("Specifies the resource is a single instance, the value must be 'Yes'."), ValueMap{"Yes"}, Values{"Yes"}] String IsSingleInstance; + [Write, Description("Specifies the desired notification level for the User Account Control setting. This parameter can not be used at the same time as any of the granular parameters."), ValueMap{"AlwaysNotify", "AlwaysNotifyAndAskForCredentials", "NotifyChanges", "NotifyChangesWithoutDimming", "NeverNotify", "NeverNotifyAndDisableAll"}, Values{"AlwaysNotify", "AlwaysNotifyAndAskForCredentials", "NotifyChanges", "NotifyChangesWithoutDimming", "NeverNotify", "NeverNotifyAndDisableAll"}] String NotificationLevel; + [Write, Description("Specifies the mode for the built-in administrator account (RID 500)."), ValueMap{"0", "1"}, Values{"0", "1"}] UInt16 FilterAdministratorToken; + [Write, Description("Specifies the prompt behavior for the Consent Administrator."), ValueMap{"0", "1", "2", "3", "4", "5"}, Values{"0", "1", "2", "3", "4", "5"}] UInt16 ConsentPromptBehaviorAdmin; + [Write, Description("Specifies how the operations that requires elevation is handled for users."), ValueMap{"0", "1", "3"}, Values{"0", "1", "3"}] UInt16 ConsentPromptBehaviorUser; + [Write, Description("Specifies how package installations are handled."), ValueMap{"0", "1"}, Values{"0", "1"}] UInt16 EnableInstallerDetection; + [Write, Description("Specifies how cryptographic signatures on interactive applications are handled."), ValueMap{"0", "1"}, Values{"0", "1"}] UInt16 ValidateAdminCodeSignatures; + [Write, Description("Specifies how the 'administrator in Admin Approval Mode' user type are handled."), ValueMap{"0", "1"}, Values{"0", "1"}] UInt16 EnableLua; + [Write, Description("Specifies if secure desktop prompting are used."), ValueMap{"0", "1"}, Values{"0", "1"}] UInt16 PromptOnSecureDesktop; + [Write, Description("Specifies how redirection of legacy application File and Registry writes are handled."), ValueMap{"0", "1"}, Values{"0", "1"}] UInt16 EnableVirtualization; + [Write, Description("Specifies if a restart of the node should be suppressed. By default the node will be restarted if the value is changed.")] Boolean SuppressRestart; +}; diff --git a/deployment/dsc/azshcihost/ComputerManagementDsc/8.4.0/DSCResources/DSC_UserAccountControl/README.md b/deployment/dsc/azshcihost/ComputerManagementDsc/8.4.0/DSCResources/DSC_UserAccountControl/README.md new file mode 100644 index 0000000..e5b2609 --- /dev/null +++ b/deployment/dsc/azshcihost/ComputerManagementDsc/8.4.0/DSCResources/DSC_UserAccountControl/README.md @@ -0,0 +1,96 @@ +# Description + +The resource allows you to configure the notification level or granularly +configure the User Account Control for the computer. + +The parameter `NotificationLevel` and any of the other granular parameters +are mutual exclusive and will throw an argument exception error. + +For the granular properties available in this resource, read more about +the possible values in the article [User Account Control Group Policy and registry key settings](https://docs.microsoft.com/en-us/windows/security/identity-protection/user-account-control/user-account-control-group-policy-and-registry-key-settings). + +The values of the parameter `NotificationLevel` reflects the +levels available through the User Account Control Settings user interface; +AlwaysNotify, NotifyChanges, NotifyChangesWithoutDimming, and NeverNotify. +The other available values have been added to easier configure different +scenarios, but they can also be configured using the granular properties +of the resource. Available values for the parameter `NotificationLevel` is: + +- **AlwaysNotify**: You will be notified before programs make changes to your + computer or to Windows settings that require the permissions of an administrator. + When you're notified, your desktop will be dimmed, and you must either approve + or deny the request in the UAC dialog box before you can do anything else on + your computer. The dimming of your desktop is referred to as the secure desktop + because other programs can't run while it's dimmed. This is the most secure + setting. When you are notified, you should carefully read the contents of each + dialog box before allowing changes to be made to your computer. + This sets the values: + - `ConsentPromptBehaviorAdmin`: 2 + - `EnableLUA`: 1 + - `PromptOnSecureDesktop`: 1 +- **AlwaysNotifyAndAskForCredentials**: This is the same as the notification level + "AlwaysNotify" with the exception that when you're notified, your desktop will + be dimmed, and you must enter valid credentials to approve the request in the + UAC dialog box. + This sets the values: + - `ConsentPromptBehaviorAdmin`: 1 + - `EnableLUA`: 1 + - `PromptOnSecureDesktop`: 1 +- **NotifyChanges**: You will be notified before programs make changes to your + computer that require the permissions of an administrator. You will not be notified + if you try to make changes to Windows settings that require the permissions of + an administrator. You will be notified if a program outside of Windows tries + to make changes to a Windows setting. It's usually safe to allow changes to be + made to Windows settings without you being notified. However, certain programs + that come with Windows can have commands or data passed to them, and malicious + software can take advantage of this by using these programs to install files + or change settings on your computer. You should always be careful about which + programs you allow to run on your computer. + This sets the values: + - `ConsentPromptBehaviorAdmin`: 5 + - `EnableLUA`: 1 + - `PromptOnSecureDesktop`: 1 +- **NotifyChangesWithoutDimming**: You will be notified before programs make + changes to your computer that require the permissions of an administrator. + You will not be notified if you try to make changes to Windows settings that + require the permissions of an administrator. You will be notified if a program + outside of Windows tries to make changes to a Windows setting. This setting is + the same as "NotifyChanges" but you are not notified on the secure desktop. + Because the UAC dialog box isn't on the secure desktop with this setting, other + programs might be able to interfere with the dialog's visual appearance. This + is a small security risk if you already have a malicious program running on + your computer. + This sets the values: + - `ConsentPromptBehaviorAdmin`: 5 + - `EnableLUA`: 1 + - `PromptOnSecureDesktop`: 0 +- **NeverNotify**: You will not be notified before any changes are made to your + computer. If you are logged on as an administrator, programs can make changes + to your computer without you knowing about it. If you are logged on as a + standard user, any changes that require the permissions of an administrator will + automatically be denied. If you select this setting, you will need to restart + the computer to complete the process of turning off UAC. Once UAC is off, people + that log on as administrator will always have the permissions of an administrator. + This is the least secure setting. When you set UAC to never notify, you open + up your computer to potential security risks. If you set UAC to never notify, + you should be careful about which programs you run, because they will have the + same access to the computer as you do. This includes reading and making changes + to protected system areas, your personal data, saved files, and anything else + stored on the computer. Programs will also be able to communicate and transfer + information to and from anything your computer connects with, including the + Internet. + This sets the values: + - `ConsentPromptBehaviorAdmin`: 0 + - `EnableLUA`: 1 + - `PromptOnSecureDesktop`: 0 +- **NeverNotifyAndDisableAll**: This is the same as the notification level + "NeverNotify", but in addition EnableLUA registry key is disabled. + EnableLUA controls the behavior of all UAC policy settings for the computer. + If you change this policy setting, you must restart your computer. We + do not recommend using this setting, but it can be selected for systems + that use programs that are not certified for Windows 8, Windows Server 2012, + Windows 7 or Windows Server 2008 R2 because they do not support UAC. + This sets the values: + - `ConsentPromptBehaviorAdmin`: 0 + - `EnableLUA`: 0 + - `PromptOnSecureDesktop`: 0 diff --git a/deployment/dsc/azshcihost/ComputerManagementDsc/8.4.0/DSCResources/DSC_UserAccountControl/en-US/DSC_UserAccountControl.strings.psd1 b/deployment/dsc/azshcihost/ComputerManagementDsc/8.4.0/DSCResources/DSC_UserAccountControl/en-US/DSC_UserAccountControl.strings.psd1 new file mode 100644 index 0000000..558a382 --- /dev/null +++ b/deployment/dsc/azshcihost/ComputerManagementDsc/8.4.0/DSCResources/DSC_UserAccountControl/en-US/DSC_UserAccountControl.strings.psd1 @@ -0,0 +1,14 @@ +ConvertFrom-StringData @' + GettingStateMessage = Getting the current notification level of the User Account Control. (UAC0001) + SettingStateMessage = Setting User Account Control state. (UAC0002) + TestingStateMessage = Testing User Account Control state. (UAC0003) + SuppressRestart = Suppressing the restart. For the change to come in affect the node must be restarted manually. (UAC0004) + NotificationLevelInDesiredState = The User Account Control notification level is in desired state. (UAC0005) + NotificationLevelNoInDesiredState = The User Account Control notification level was '{0}', but expected it to be '{1}'. (UAC0006) + GranularPropertiesInDesiredState = The User Account Control properties are in desired state. (UAC0007) + GranularPropertyNoInDesiredState = The User Account Control property '{0}' was '{1}', but expected it to be '{2}'. (UAC0008) + SetPropertyToValue = Setting the property '{0}' to the value '{1}'. (UAC0009) + SetNotificationLevel = Setting the notification level to '{0}'. (UAC0010) + FailedToSetNotificationLevel = Failed to set the notification level '{0}'. (UAC0011) + FailedToSetGranularProperty = Failed to set the property '{0}'. (UAC0012) +'@ diff --git a/deployment/dsc/azshcihost/ComputerManagementDsc/8.4.0/DSCResources/DSC_VirtualMemory/DSC_VirtualMemory.psm1 b/deployment/dsc/azshcihost/ComputerManagementDsc/8.4.0/DSCResources/DSC_VirtualMemory/DSC_VirtualMemory.psm1 new file mode 100644 index 0000000..84cb785 --- /dev/null +++ b/deployment/dsc/azshcihost/ComputerManagementDsc/8.4.0/DSCResources/DSC_VirtualMemory/DSC_VirtualMemory.psm1 @@ -0,0 +1,529 @@ +[Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidGlobalVars", "", Scope = "Function")] +param () + +$modulePath = Join-Path -Path (Split-Path -Path (Split-Path -Path $PSScriptRoot -Parent) -Parent) -ChildPath 'Modules' + +# Import the ComputerManagementDsc Common Modules +Import-Module -Name (Join-Path -Path $modulePath ` + -ChildPath (Join-Path -Path 'ComputerManagementDsc.Common' ` + -ChildPath 'ComputerManagementDsc.Common.psm1')) + +Import-Module -Name (Join-Path -Path $modulePath -ChildPath 'DscResource.Common') + +# Import Localization Strings +$script:localizedData = Get-LocalizedData -DefaultUICulture 'en-US' + +<# + .SYNOPSIS + Returns the current state of the virtual memory configuration + + .PARAMETER Drive + The drive for which the virtual memory configuration needs to be returned + + .PARAMETER Type + The type of the virtual memory configuration +#> +function Get-TargetResource +{ + [CmdletBinding()] + [OutputType([System.Collections.Hashtable])] + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $Drive, + + [Parameter(Mandatory = $true)] + [ValidateSet('AutoManagePagingFile', 'CustomSize', 'SystemManagedSize', 'NoPagingFile')] + [System.String] + $Type + ) + + Write-Verbose -Message ($script:localizedData.GettingVirtualMemoryMessage) + + $returnValue = @{ + Drive = [string]::Empty + Type = [string]::Empty + InitialSize = 0 + MaximumSize = 0 + } + + [System.Boolean] $isSystemManaged = (Get-CimInstance -ClassName 'Win32_ComputerSystem').AutomaticManagedPagefile + + if ($isSystemManaged) + { + $returnValue.Type = 'AutoManagePagingFile' + return $returnValue + } + + $driveInfo = [System.IO.DriveInfo] $Drive + + $existingPageFileSetting = Get-PageFileSetting ` + -Drive $($driveInfo.Name.Substring(0,2)) + + if (-not $existingPageFileSetting) + { + $returnValue.Type = 'NoPagingFile' + } + else + { + if ($existingPageFileSetting.InitialSize -eq 0 -and $existingPageFileSetting.MaximumSize -eq 0) + { + $returnValue.Type = 'SystemManagedSize' + } + else + { + $returnValue.Type = 'CustomSize' + } + + $returnValue.Drive = $existingPageFileSetting.Name.Substring(0, 3) + $returnValue.InitialSize = $existingPageFileSetting.InitialSize + $returnValue.MaximumSize = $existingPageFileSetting.MaximumSize + } + + return $returnValue +} + +<# + .SYNOPSIS + Sets the virtual memory settings based on the parameters supplied + + .PARAMETER Drive + The drive for which the virtual memory configuration should be set. + + .PARAMETER Type + The paging type. When set to AutoManagePagingFile, drive letters are ignored + + .PARAMETER InitialSize + The initial page file size in megabyte + + .PARAMETER MaximumSize + The maximum page file size in megabyte. May not be smaller than InitialSize +#> +function Set-TargetResource +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $Drive, + + [Parameter(Mandatory = $true)] + [ValidateSet('AutoManagePagingFile', 'CustomSize', 'SystemManagedSize', 'NoPagingFile')] + [System.String] + $Type, + + [Parameter()] + [System.Int64] + $InitialSize, + + [Parameter()] + [System.Int64] + $MaximumSize + ) + + Write-Verbose -Message ($script:localizedData.SettingVirtualMemoryMessage) + + $systemInfo = Get-CimInstance -ClassName 'Win32_ComputerSystem' + + switch ($Type) + { + 'AutoManagePagingFile' + { + Set-AutoManagePaging -State Enable + + $global:DSCMachineStatus = 1 + + break + } + + 'CustomSize' + { + if ($systemInfo.AutomaticManagedPageFile) + { + # First Disable Automatic Managed Page File + Set-AutoManagePaging -State Disable + } + + $driveInfo = Get-DriveInfo -Drive $Drive + + $existingPageFileSetting = Get-PageFileSetting ` + -Drive $($driveInfo.Name.Substring(0,2)) + + if (-not $existingPageFileSetting) + { + $pageFileName = Join-Path ` + -Path $driveInfo.Name ` + -ChildPath 'pagefile.sys' + + New-PageFile -PageFileName $pageFileName + } + + Set-PageFileSetting ` + -Drive $driveInfo.Name.Substring(0,2) ` + -InitialSize $InitialSize ` + -MaximumSize $MaximumSize + + $global:DSCMachineStatus = 1 + + Write-Verbose -Message ($script:localizedData.EnabledCustomSizeMessage -f $Drive) + + break + } + + 'SystemManagedSize' + { + if ($systemInfo.AutomaticManagedPageFile) + { + # First Disable Automatic Managed Page File + Set-AutoManagePaging -State Disable + } + + $driveInfo = Get-DriveInfo -Drive $Drive + + $existingPageFileSetting = Get-PageFileSetting ` + -Drive $($driveInfo.Name.Substring(0,2)) + + if (-not $existingPageFileSetting) + { + $pageFileName = Join-Path ` + -Path $driveInfo.Name ` + -ChildPath 'pagefile.sys' + + New-PageFile -PageFileName $pageFileName + } + + Set-PageFileSetting ` + -Drive $driveInfo.Name.Substring(0,2) + + $global:DSCMachineStatus = 1 + + Write-Verbose -Message ($script:localizedData.EnabledSystemManagedSizeMessage -f $Drive) + + break + } + + 'NoPagingFile' + { + if ($systemInfo.AutomaticManagedPageFile) + { + # First Disable Automatic Managed Page File + Set-AutoManagePaging -State Disable + } + + $driveInfo = Get-DriveInfo -Drive $Drive + + $existingPageFileSetting = Get-PageFileSetting ` + -Drive $($driveInfo.Name.Substring(0,2)) + + if ($existingPageFileSetting) + { + Write-Verbose -Message ($script:localizedData.RemovePageFileMessage -f $existingPageFileSetting.Name) + + $null = Remove-CimInstance ` + -InputObject $existingPageFileSetting + + $global:DSCMachineStatus = 1 + } + + Write-Verbose -Message ($script:localizedData.DisabledPageFileMessage -f $Drive) + + break + } + } +} + +<# + .SYNOPSIS + Tests if virtual memory settings need to be applied based on the parameters supplied + + .PARAMETER Drive + The drive letter that should be tested + + .PARAMETER Type + The type of the virtual memory configuration + + .PARAMETER InitialSize + The initial page file size in megabyte + + .PARAMETER MaximumSize + The maximum page file size in megabyte +#> +function Test-TargetResource +{ + [CmdletBinding()] + [OutputType([System.Boolean])] + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $Drive, + + [Parameter(Mandatory = $true)] + [ValidateSet('AutoManagePagingFile', 'CustomSize', 'SystemManagedSize', 'NoPagingFile')] + [System.String] + $Type, + + [Parameter()] + [System.Int64] + $InitialSize, + + [Parameter()] + [System.Int64] + $MaximumSize + ) + + Write-Verbose -Message ($script:localizedData.TestingVirtualMemoryMessage) + + $systemInfo = Get-CimInstance -ClassName 'Win32_ComputerSystem' + $inDesiredState = $false + + switch ($Type) + { + 'AutoManagePagingFile' + { + $inDesiredState = $systemInfo.AutomaticManagedPagefile + break + } + + 'CustomSize' + { + if ($systemInfo.AutomaticManagedPageFile) + { + break + } + + $driveInfo = [System.IO.DriveInfo] $Drive + + $existingPageFileSetting = Get-PageFileSetting ` + -Drive $($driveInfo.Name.Substring(0,2)) + + if (-not $existingPageFileSetting) + { + break + } + + if (-not ($existingPageFileSetting.InitialSize -eq $InitialSize -and $existingPageFileSetting.MaximumSize -eq $MaximumSize)) + { + break + } + + $inDesiredState = $true + break + } + + 'SystemManagedSize' + { + if ($systemInfo.AutomaticManagedPageFile) + { + break + } + + $driveInfo = [System.IO.DriveInfo] $Drive + + $existingPageFileSetting = Get-PageFileSetting ` + -Drive $($driveInfo.Name.Substring(0,2)) + + if (-not $existingPageFileSetting) + { + break + } + + if (-not ($existingPageFileSetting.InitialSize -eq 0 -and $existingPageFileSetting.MaximumSize -eq 0)) + { + break + } + + $inDesiredState = $true + break + } + + 'NoPagingFile' + { + if ($systemInfo.AutomaticManagedPageFile) + { + break + } + + $driveInfo = [System.IO.DriveInfo] $Drive + + $existingPageFileSetting = Get-PageFileSetting ` + -Drive $($driveInfo.Name.Substring(0,2)) + + if ($existingPageFileSetting) + { + break + } + + $inDesiredState = $true + break + } + } + + return $inDesiredState +} + +<# + .SYNOPSIS + Gets the settings for a page file assigned to a Drive. + + .PARAMETER State + The drive letter for the page file to return the settings of. +#> +function Get-PageFileSetting +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $Drive + ) + + Write-Verbose -Message ($script:localizedData.GettingPageFileSettingsMessage -f $Drive) + + # Find existing page file settings by drive letter + return Get-CimInstance ` + -ClassName 'Win32_PageFileSetting' ` + -Filter "SettingID='pagefile.sys @ $Drive'" +} + +<# + .SYNOPSIS + Sets a new page file name. + + .PARAMETER Drive + The letter of the drive containing the page file + to change the settings of. + + .PARAMETER InitialSize + The initial size to set the page file to. + + .PARAMETER MaximumSize + The maximum size to set the page file to. +#> +function Set-PageFileSetting +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $Drive, + + [Parameter()] + [System.Int64] + $InitialSize = 0, + + [Parameter()] + [System.Int64] + $MaximumSize = 0 + ) + + $setParams = @{ + Namespace = 'root\cimv2' + Query = "Select * from Win32_PageFileSetting where SettingID='pagefile.sys @ $Drive'" + Property = @{ + InitialSize = $InitialSize + MaximumSize = $MaximumSize + } + } + + Write-Verbose -Message ($script:localizedData.SettingPageFileSettingsMessage -f $Drive, $InitialSize, $MaximumSize) + + $null = Set-CimInstance @setParams +} + +<# + .SYNOPSIS + Enables or Disables Automatically Managed Paging. + + .PARAMETER State + Specifies if Automatically Managed Paging is enabled + or disabled. +#> +function Set-AutoManagePaging +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [ValidateSet('Enable', 'Disable')] + [System.String] + $State + ) + + $setParams = @{ + Namespace = 'root\cimv2' + Query = 'Select * from Win32_ComputerSystem' + Property = @{ + AutomaticManagedPageFile = ($State -eq 'Enable') + } + } + + Write-Verbose -Message ($script:localizedData.SetAutoManagePagingMessage -f $State) + + $null = Set-CimInstance @setParams +} + +<# + .SYNOPSIS + Sets a new page file name. + + .PARAMETER PageFileName + The name of the new page file. +#> +function New-PageFile +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $PageFileName + ) + + Write-Verbose -Message ($script:localizedData.NewPageFileMessage -f $State) + + $null = New-CimInstance ` + -Namespace 'root\cimv2' ` + -ClassName 'Win32_PageFileSetting' ` + -Property @{ + Name = $PageFileName + } +} + +<# + .SYNOPSIS + Gets the Drive info object for a specified + Drive. It will throw an exception if the drive + is invalid or does not exist. + + .PARAMETER Drive + The letter of the drive to get the drive info + for. +#> +function Get-DriveInfo +{ + [CmdletBinding()] + [OutputType([System.IO.DriveInfo])] + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $Drive + ) + + $driveInfo = [System.IO.DriveInfo] $Drive + + if (-not $driveInfo.IsReady) + { + New-InvalidOperationException ` + -Message ($script:localizedData.DriveNotReadyError -f $driveInfo.Name) + } + + return $driveInfo +} + +Export-ModuleMember -Function *-TargetResource diff --git a/deployment/dsc/azshcihost/ComputerManagementDsc/8.4.0/DSCResources/DSC_VirtualMemory/DSC_VirtualMemory.schema.mof b/deployment/dsc/azshcihost/ComputerManagementDsc/8.4.0/DSCResources/DSC_VirtualMemory/DSC_VirtualMemory.schema.mof new file mode 100644 index 0000000..11c4182 --- /dev/null +++ b/deployment/dsc/azshcihost/ComputerManagementDsc/8.4.0/DSCResources/DSC_VirtualMemory/DSC_VirtualMemory.schema.mof @@ -0,0 +1,10 @@ + +[ClassVersion("1.0.0.0"), FriendlyName("VirtualMemory")] +class DSC_VirtualMemory : OMI_BaseResource +{ + [Key, Description("The drive letter for which paging settings should be set. Can be letter only, letter and colon or letter with colon and trailing slash.")] String Drive; + [Key, Description("The type of the paging setting to use. If set to AutoManagePagingFile, the drive letter will be ignored. If set to SystemManagedSize, the values for InitialSize and MaximumSize will be ignored"), ValueMap{"AutoManagePagingFile","CustomSize","SystemManagedSize","NoPagingFile"}, Values{"AutoManagePagingFile","CustomSize","SystemManagedSize","NoPagingFile"}] String Type; + [Write, Description("The initial size of the page file in Megabyte")] Sint64 InitialSize; + [Write, Description("The maximum size of the page file in Megabyte")] Sint64 MaximumSize; +}; + diff --git a/deployment/dsc/azshcihost/ComputerManagementDsc/8.4.0/DSCResources/DSC_VirtualMemory/README.md b/deployment/dsc/azshcihost/ComputerManagementDsc/8.4.0/DSCResources/DSC_VirtualMemory/README.md new file mode 100644 index 0000000..0bdd7fd --- /dev/null +++ b/deployment/dsc/azshcihost/ComputerManagementDsc/8.4.0/DSCResources/DSC_VirtualMemory/README.md @@ -0,0 +1,4 @@ +# Description + +The resource allows configuration of properties of the paging file on +the local computer. diff --git a/deployment/dsc/azshcihost/ComputerManagementDsc/8.4.0/DSCResources/DSC_VirtualMemory/en-US/DSC_VirtualMemory.strings.psd1 b/deployment/dsc/azshcihost/ComputerManagementDsc/8.4.0/DSCResources/DSC_VirtualMemory/en-US/DSC_VirtualMemory.strings.psd1 new file mode 100644 index 0000000..46fad83 --- /dev/null +++ b/deployment/dsc/azshcihost/ComputerManagementDsc/8.4.0/DSCResources/DSC_VirtualMemory/en-US/DSC_VirtualMemory.strings.psd1 @@ -0,0 +1,14 @@ +ConvertFrom-StringData @' + GettingVirtualMemoryMessage = Getting Virtual Memory. + SettingVirtualMemoryMessage = Setting Virtual Memory. + SetAutoManagePagingMessage = {0} automatically managed page file. + GettingPageFileSettingsMessage = Getting page file settings for drive {0}. + SettingPageFileSettingsMessage = Setting page file settings for drive {0} with initial size of {1}MB and maximum size {2}MB. + NewPageFileMessage = Creating new page file '{0}'. + RemovePageFileMessage = Removing existing page file '{0}'. + DisabledPageFileMessage = Disabled page file for drive {0}. + EnabledSystemManagedSizeMessage = Enabled system managed page file for drive {0}. + EnabledCustomSizeMessage = Enabled custom size page file for drive {0}. + DriveNotReadyError = Drive {0} is not ready. Please ensure that the drive exists and is available. + TestingVirtualMemoryMessage = Testing Virtual Memory. +'@ diff --git a/deployment/dsc/azshcihost/ComputerManagementDsc/8.4.0/DSCResources/DSC_WindowsCapability/DSC_WindowsCapability.psm1 b/deployment/dsc/azshcihost/ComputerManagementDsc/8.4.0/DSCResources/DSC_WindowsCapability/DSC_WindowsCapability.psm1 new file mode 100644 index 0000000..64f8f48 --- /dev/null +++ b/deployment/dsc/azshcihost/ComputerManagementDsc/8.4.0/DSCResources/DSC_WindowsCapability/DSC_WindowsCapability.psm1 @@ -0,0 +1,209 @@ +$modulePath = Join-Path -Path (Split-Path -Path (Split-Path -Path $PSScriptRoot -Parent) -Parent) -ChildPath 'Modules' + +# Import the ComputerManagementDsc Common Modules +Import-Module -Name (Join-Path -Path $modulePath ` + -ChildPath (Join-Path -Path 'ComputerManagementDsc.Common' ` + -ChildPath 'ComputerManagementDsc.Common.psm1')) + +Import-Module -Name (Join-Path -Path $modulePath -ChildPath 'DscResource.Common') + +# Import Localization Strings +$script:localizedData = Get-LocalizedData -DefaultUICulture 'en-US' + +<# + .SYNOPSIS + Gets the current state of the Windows Capability. + + .PARAMETER Name + Specifies the name of the Windows Capability. +#> +function Get-TargetResource +{ + [CmdletBinding()] + [OutputType([System.Collections.Hashtable])] + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $Name + ) + + Write-Verbose -Message ($script:localizedData.GetTargetResourceStartMessage -f $Name) + $windowsCapability = Get-WindowsCapability -Online @PSBoundParameters + + if ([System.String]::IsNullOrEmpty($windowsCapability.Name)) + { + New-InvalidArgumentException ` + -Message ($script:localizedData.CapabilityNameNotFound -f $Name) ` + -ArgumentName 'Name' + } + else + { + Write-Verbose -Message ($script:localizedData.CapabilityNameFound -f $Name) + } + + if ($windowsCapability.State -eq 'Installed') + { + $Ensure = 'Present' + } + else + { + $Ensure = 'Absent' + } + + $returnValue = @{ + Name = $Name + LogLevel = $windowsCapability.LogLevel + LogPath = $windowsCapability.LogPath + Ensure = $Ensure + } + + Write-Verbose -Message ($script:localizedData.GetTargetResourceEndMessage -f $Name) + return $returnValue +} + +<# + .SYNOPSIS + Sets if the the current state of the Windows Capability is in the desired state. + + .PARAMETER Name + Specifies the name of the Windows Capability. + + .PARAMETER Ensure + Specifies whether the Windows Capability should be installed + or uninstalled. + + .PARAMETER LogLevel + Specifies the given Log Level of a Windows Capability. This is a write + only parameter that is used when updating the status of a Windows + Capability. If not specified, the default is 'WarningsInfo'. + + .PARAMETER LogPath + Specifies the full path and file name to log to. This is a write + only parameter that is used when updating the status of a Windows + Capability. If not specified, the default is '%WINDIR%\Logs\Dism\dism.log'. +#> +function Set-TargetResource +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $Name, + + [Parameter()] + [ValidateSet('Present', 'Absent')] + [System.String] + $Ensure = 'Present', + + [Parameter()] + [ValidateSet('Errors', 'Warnings', 'WarningsInfo')] + [System.String] + $LogLevel, + + [Parameter()] + [System.String] + $LogPath + ) + + Write-Verbose -Message ($script:localizedData.SetTargetResourceStartMessage -f $Name) + $null = $PSBoundParameters.Remove('Ensure') + $currentState = Get-TargetResource -Name $Name + + switch ($Ensure) + { + 'Present' + { + if ($Ensure -ne $currentState.Ensure) + { + Write-Verbose -Message ($script:localizedData.SetTargetAddMessage -f $Name) + $null = Add-WindowsCapability -Online @PSBoundParameters + } + } + + 'Absent' + { + if ($Ensure -ne $currentState.Ensure) + { + Write-Verbose -Message ($script:localizedData.SetTargetRemoveMessage -f $Name) + $null = Remove-WindowsCapability -Online @PSBoundParameters + } + } + } +} + +<# + .SYNOPSIS + Tests if the the current state of the Windows Capability is in the desired state. + + .PARAMETER Name + Specifies the name of the Windows Capability. + + .PARAMETER Ensure + Specifies whether the Windows Capability should be installed + or uninstalled. + + .PARAMETER LogLevel + Specifies the given Log Level of a Windows Capability. This is a write + only parameter that is used when updating the status of a Windows + Capability. If not specified, the default is 'WarningsInfo'. + + .PARAMETER LogPath + Specifies the full path and file name to log to. This is a write + only parameter that is used when updating the status of a Windows + Capability. If not specified, the default is '%WINDIR%\Logs\Dism\dism.log'. + + .NOTES + Get-WindowsCapability will return the LogLevel and LogPath + properties, but these values don't reflect the values set + when calling Add-WindowsCapability or Remove-WindowsCapability. + + Therefore, these values can not be used to determine if the + resource is in state. +#> +function Test-TargetResource +{ + [CmdletBinding()] + [OutputType([System.Boolean])] + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $Name, + + [Parameter()] + [ValidateSet('Present', 'Absent')] + [System.String] + $Ensure = 'Present', + + [Parameter()] + [ValidateSet('Errors', 'Warnings', 'WarningsInfo')] + [System.String] + $LogLevel, + + [Parameter()] + [System.String] + $LogPath + ) + + $inDesiredState = $true + + Write-Verbose -Message ($script:localizedData.TestTargetResourceStartMessage -f $Name) + $currentState = Get-TargetResource -Name $Name + + if ($Ensure -ne $currentState.Ensure) + { + Write-Verbose -Message ($script:localizedData.SetResourceIsNotInDesiredState -f $Name) + $inDesiredState = $false + } + else + { + Write-Verbose -Message ($script:localizedData.SetResourceIsInDesiredState -f $Name) + $inDesiredState = $true + } + + return $inDesiredState +} + +Export-ModuleMember -Function *-TargetResource diff --git a/deployment/dsc/azshcihost/ComputerManagementDsc/8.4.0/DSCResources/DSC_WindowsCapability/DSC_WindowsCapability.schema.mof b/deployment/dsc/azshcihost/ComputerManagementDsc/8.4.0/DSCResources/DSC_WindowsCapability/DSC_WindowsCapability.schema.mof new file mode 100644 index 0000000..339247a --- /dev/null +++ b/deployment/dsc/azshcihost/ComputerManagementDsc/8.4.0/DSCResources/DSC_WindowsCapability/DSC_WindowsCapability.schema.mof @@ -0,0 +1,9 @@ + +[ClassVersion("1.0.0.1"), FriendlyName("WindowsCapability")] +class DSC_WindowsCapability : OMI_BaseResource +{ + [Key, Description("Specifies the name of the Windows Capability.")] String Name; + [Write, Description("Specifies whether the Windows Capability should be installed or uninstalled. To install the Windows Capability, set this property to Present. To uninstall the Windows Capability, set the property to Absent."), ValueMap{"Present", "Absent"}, Values{"Present", "Absent"}] String Ensure; + [Write, Description("Specifies the given Log Level of a Windows Capability. This is a write only parameter that is used when updating the status of a Windows Capability. If not specified, the default is 'WarningsInfo'."), ValueMap{"Errors", "Warnings", "WarningsInfo"}, Values{"Errors", "Warnings", "WarningsInfo"}] String LogLevel; + [Write, Description("Specifies the full path and file name to log to. This is a write only parameter that is used when updating the status of a Windows Capability. If not specified, the default is '%WINDIR%\\Logs\\Dism\\dism.log'.")] String LogPath; +}; diff --git a/deployment/dsc/azshcihost/ComputerManagementDsc/8.4.0/DSCResources/DSC_WindowsCapability/README.md b/deployment/dsc/azshcihost/ComputerManagementDsc/8.4.0/DSCResources/DSC_WindowsCapability/README.md new file mode 100644 index 0000000..5be0f6d --- /dev/null +++ b/deployment/dsc/azshcihost/ComputerManagementDsc/8.4.0/DSCResources/DSC_WindowsCapability/README.md @@ -0,0 +1,10 @@ +# Description + +This resource enables installation or removal of a Windows Capability. + +The LogLevel and LogPath parameters can be passed to the resource but +are not used to determine if the resource is in the desired state. + +This is because the LogLevel and LogPath properties returned by +`Get-WindowsCapability` do not reflect the values that may have been +set with `Add-WindowsCapability` or `Remove-WindowsCapability`. diff --git a/deployment/dsc/azshcihost/ComputerManagementDsc/8.4.0/DSCResources/DSC_WindowsCapability/en-US/DSC_WindowsCapability.strings.psd1 b/deployment/dsc/azshcihost/ComputerManagementDsc/8.4.0/DSCResources/DSC_WindowsCapability/en-US/DSC_WindowsCapability.strings.psd1 new file mode 100644 index 0000000..55dc805 --- /dev/null +++ b/deployment/dsc/azshcihost/ComputerManagementDsc/8.4.0/DSCResources/DSC_WindowsCapability/en-US/DSC_WindowsCapability.strings.psd1 @@ -0,0 +1,13 @@ +# culture ="en-US" +ConvertFrom-StringData -StringData @' + SetResourceIsInDesiredState = Windows Capability '{0}' is in desired state. + SetResourceIsNotInDesiredState = Windows Capability '{0}' is not in desired state. + GetTargetResourceStartMessage = Begin executing Get functionality on Windows Capability '{0}'. + GetTargetResourceEndMessage = End executing Get functionality on Windows Capability '{0}'. + SetTargetResourceStartMessage = Begin executing Set functionality on Windows Capability '{0}'. + SetTargetRemoveMessage = Executing Remove functionality on Windows Capability '{0}'. + SetTargetAddMessage = Executing Add functionality on Windows Capability '{0}'. + TestTargetResourceStartMessage = Begin executing Test functionality on Windows Capability '{0}'. + CapabilityNameFound = Specified Windows Capability '{0}' found. + CapabilityNameNotFound = Specified Windows Capability '{0}' not found. +'@ diff --git a/deployment/dsc/azshcihost/ComputerManagementDsc/8.4.0/DSCResources/DSC_WindowsEventLog/DSC_WindowsEventLog.psm1 b/deployment/dsc/azshcihost/ComputerManagementDsc/8.4.0/DSCResources/DSC_WindowsEventLog/DSC_WindowsEventLog.psm1 new file mode 100644 index 0000000..813fd15 --- /dev/null +++ b/deployment/dsc/azshcihost/ComputerManagementDsc/8.4.0/DSCResources/DSC_WindowsEventLog/DSC_WindowsEventLog.psm1 @@ -0,0 +1,453 @@ +$modulePath = Join-Path -Path (Split-Path -Path (Split-Path -Path $PSScriptRoot -Parent) -Parent) -ChildPath 'Modules' + +# Import the ComputerManagementDsc Common Modules +Import-Module -Name (Join-Path -Path $modulePath ` + -ChildPath (Join-Path -Path 'ComputerManagementDsc.Common' ` + -ChildPath 'ComputerManagementDsc.Common.psm1')) + +Import-Module -Name (Join-Path -Path $modulePath -ChildPath 'DscResource.Common') + +# Import Localization Strings +$script:localizedData = Get-LocalizedData -DefaultUICulture 'en-US' + +<# + .SYNOPSIS + Gets the current state of the Windows Event Log. + + .PARAMETER LogName + Specifies the given name of a Windows Event Log. +#> +function Get-TargetResource +{ + [CmdletBinding()] + [OutputType([System.Collections.Hashtable])] + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $LogName, + + [Parameter()] + [System.Boolean] + $IsEnabled + ) + + $log = Get-WindowsEventLog -LogName $LogName + + $LogRetentionDays = (Get-EventLog -List | Where-Object -Property Log -eq $LogName).minimumRetentionDays + + $returnValue = @{ + LogName = [System.String] $LogName + LogFilePath = [system.String] $log.LogFilePath + MaximumSizeInBytes = [System.Int64] $log.MaximumSizeInBytes + IsEnabled = [System.Boolean] $log.IsEnabled + LogMode = [System.String] $log.LogMode + SecurityDescriptor = [System.String] $log.SecurityDescriptor + LogRetentionDays = [System.Int32] $logRetentionDays + } + + Write-Verbose -Message ($script:localizedData.GettingEventlogName -f $LogName) + return $returnValue +} + +<# + .SYNOPSIS + Sets the current state of the Windows Event Log. + + .PARAMETER LogName + Specifies the given name of a Windows Event Log. + + .PARAMETER MaximumSizeInBytes + Specifies the given maximum size in bytes for a specified Windows Event Log. + + .PARAMETER LogMode + Specifies the given LogMode for a specified Windows Event Log. + + .PARAMETER LogRetentionDays + Specifies the given LogRetentionDays for the Logmode 'AutoBackup'. + + .PARAMETER SecurityDescriptor + Specifies the given SecurityDescriptor for a specified Windows Event Log. + + .PARAMETER IsEnabled + Specifies the given state of a Windows Event Log. + + .PARAMETER LogFilePath + Specifies the given LogFile path of a Windows Event Log. +#> +function Set-TargetResource +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $LogName, + + [Parameter()] + [System.Boolean] + $IsEnabled, + + [Parameter()] + [System.Int64] + $MaximumSizeInBytes, + + [Parameter()] + [ValidateSet('AutoBackup', 'Circular', 'Retain')] + [System.String] + $LogMode, + + [Parameter()] + [System.Int32] + $LogRetentionDays, + + [Parameter()] + [System.String] + $SecurityDescriptor, + + [Parameter()] + [System.String] + $LogFilePath + ) + + $log = Get-WindowsEventLog -LogName $LogName + + if ($null -eq $log) + { + return + } + + $shouldSaveLogFile = $false + + Write-Verbose -Message ($script:localizedData.GettingEventlogName -f $LogName) + + if ($IsEnabled -eq $true) + { + if ($PSBoundParameters.ContainsKey('IsEnabled') -and $IsEnabled -ne $log.IsEnabled) + { + Write-Verbose -Message ($script:localizedData.SettingEventlogIsEnabled -f $LogName, $IsEnabled) + $log.IsEnabled = $IsEnabled + $shouldSaveLogFile = $true + } + + if ($PSBoundParameters.ContainsKey('MaximumSizeInBytes') -and $MaximumSizeInBytes -ne $log.MaximumSizeInBytes) + { + Write-Verbose -Message ($script:localizedData.SettingEventlogLogSize -f $LogName, $MaximumSizeInBytes) + $log.MaximumSizeInBytes = $MaximumSizeInBytes + $shouldSaveLogFile = $true + } + + if ($PSBoundParameters.ContainsKey('LogMode') -and $LogMode -ne $log.LogMode) + { + Write-Verbose -Message ($script:localizedData.SettingEventlogLogMode -f $LogName, $LogMode) + $log.LogMode = $LogMode + $shouldSaveLogFile = $true + } + + if ($PSBoundParameters.ContainsKey('SecurityDescriptor') -and $SecurityDescriptor -ne $log.SecurityDescriptor) + { + Write-Verbose -Message ($script:localizedData.SettingEventlogSecurityDescriptor -f $LogName, $SecurityDescriptor) + $log.SecurityDescriptor = $SecurityDescriptor + $shouldSaveLogFile = $true + } + + if ($PSBoundParameters.ContainsKey('LogFilePath') -and $LogFilePath -ne $log.LogFilePath) + { + Write-Verbose -Message ($script:localizedData.SettingEventlogLogFilePath -f $LogName, $LogFilePath) + $log.LogFilePath = $LogFilePath + $shouldSaveLogFile = $true + } + } + else + { + Write-Verbose -Message ($script:localizedData.SettingEventlogIsEnabled -f $LogName, $IsEnabled) + $log.IsEnabled = $IsEnabled + $shouldSaveLogFile = $true + } + + if ($shouldSaveLogFile -eq $true) + { + Save-LogFile -Log $log + } + + if ($PSBoundParameters.ContainsKey('LogRetentionDays')) + { + if ($LogMode -eq 'AutoBackup' -and (Get-EventLog -List | Where-Object -FilterScript {$_.Log -like $LogName})) + { + $matchingEventLog = Get-EventLog -List | Where-Object -FilterScript { + $_.Log -eq $LogName + } + + $minimumRetentionDaysForLog = $matchingEventLog.minimumRetentionDays + + if ($LogRetentionDays -ne $minimumRetentionDaysForLog) + { + Set-LogRetentionDays -LogName $LogName -LogRetentionDays $LogRetentionDays + } + } + else + { + Write-Verbose -Message ($script:localizedData.EventlogLogRetentionDaysWrongMode -f $LogName) + } + } +} + +<# + .SYNOPSIS + Tests if the the current state of the Windows Event Log is in the desired state. + + .PARAMETER LogName + Specifies the given name of a Windows Event Log. + + .PARAMETER MaximumSizeInBytes + Specifies the given maximum size in bytes for a specified Windows Event Log. + + .PARAMETER LogMode + Specifies the given LogMode for a specified evWindows Event Logentlog. + + .PARAMETER LogRetentionDays + Specifies the given LogRetentionDays for the Logmode 'AutoBackup'. + + .PARAMETER SecurityDescriptor + Specifies the given SecurityDescriptor for a specified Windows Event Log. + + .PARAMETER IsEnabled + Specifies the given state of a Windows Event Log. + + .PARAMETER LogFilePath + Specifies the given LogFile path of a Windows Event Log. +#> +function Test-TargetResource +{ + [CmdletBinding()] + [OutputType([System.Boolean])] + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $LogName, + + [Parameter()] + [System.Boolean] + $IsEnabled, + + [Parameter()] + [ValidateRange(1028kb, 18014398509481983kb)] + [System.Int64] + $MaximumSizeInBytes, + + [Parameter()] + [ValidateSet('AutoBackup', 'Circular', 'Retain')] + [System.String] + $LogMode, + + [Parameter()] + [ValidateRange(1, 365)] + [System.Int32] + $LogRetentionDays, + + [Parameter()] + [System.String] + $SecurityDescriptor, + + [Parameter()] + [System.String] + $LogFilePath + ) + + $log = Get-WindowsEventLog -LogName $LogName + + if ($null -eq $log) + { + return + } + + $desiredState = $true + + if ($IsEnabled -eq $true) + { + if ($PSBoundParameters.ContainsKey('IsEnabled') -and $log.IsEnabled -ne $IsEnabled) + { + Write-Verbose -Message ($script:localizedData.TestingEventlogIsEnabled -f $LogName, $IsEnabled) + $desiredState = $false + } + else + { + Write-Verbose -Message ($script:localizedData.SetResourceIsInDesiredState -f $LogName, 'IsEnabled') + } + + if ($PSBoundParameters.ContainsKey('MaximumSizeInBytes') -and $log.MaximumSizeInBytes -ne $MaximumSizeInBytes) + { + Write-Verbose -Message ($script:localizedData.TestingEventlogMaximumSizeInBytes -f $LogName, $MaximumSizeInBytes) + $desiredState = $false + } + else + { + Write-Verbose -Message ($script:localizedData.SetResourceIsInDesiredState -f $LogName, 'MaximumSizeInBytes') + } + + if ($PSBoundParameters.ContainsKey('LogMode') -and $log.LogMode -ne $LogMode) + { + Write-Verbose -Message ($script:localizedData.TestingEventlogLogMode -f $LogName, $LogMode) + $desiredState = $false + } + else + { + Write-Verbose -Message ($script:localizedData.SetResourceIsInDesiredState -f $LogName, 'LogMode') + } + + if ($PSBoundParameters.ContainsKey('LogRetentionDays')) + { + if ($LogMode -eq 'AutoBackup') + { + $minimumRetentionDays = Get-EventLog -List | Where-Object -FilterScript { $_.Log -eq $LogName } + + if ($LogRetentionDays -ne $minimumRetentionDays.minimumRetentionDays) + { + Write-Verbose -Message ($script:localizedData.TestingEventlogLogRetentionDays -f $LogName, $LogRetentionDays) + $desiredState = $false + } + else + { + Write-Verbose -Message ($script:localizedData.SetResourceIsInDesiredState -f $LogName, 'LogRetentionDays') + } + } + else + { + Write-Verbose -Message ($script:localizedData.EventlogLogRetentionDaysWrongMode -f $LogName) + $desiredState = $false + } + } + + if ($PSBoundParameters.ContainsKey('LogFilePath') -and $log.LogFilePath -ne $LogFilePath) + { + Write-Verbose -Message ($script:localizedData.TestingWindowsEventlogLogFilePath -f $LogName, $LogFilePath) + $desiredState = $false + } + else + { + Write-Verbose -Message ($script:localizedData.SetResourceIsInDesiredState -f $LogName, 'LogFilePath') + } + + if ($PSBoundParameters.ContainsKey('SecurityDescriptor') -and $log.SecurityDescriptor -ne $SecurityDescriptor) + { + Write-Verbose -Message ($script:localizedData.TestingWindowsEventlogSecurityDescriptor -f $LogName, $SecurityDescriptor) + $desiredState = $false + } + else + { + Write-Verbose -Message ($script:localizedData.SetResourceIsInDesiredState -f $LogName, 'SecurityDescriptor') + } + } + else + { + if ($PSBoundParameters.ContainsKey('IsEnabled') -and $log.IsEnabled -ne $IsEnabled) + { + Write-Verbose -Message ($script:localizedData.TestingEventlogIsEnabled -f $LogName, $IsEnabled) + $desiredState = $false + } + else + { + Write-Verbose -Message ($script:localizedData.SetResourceIsInDesiredState -f $LogName, 'IsEnabled') + } + } + return $desiredState +} + +<# + .SYNOPSIS + Helper function for the Windows Event Log. + + .PARAMETER Log + Gets the specified Windows Event Log properties. +#> +function Get-WindowsEventLog +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $LogName + ) + + $log = Get-WinEvent -ListLog $LogName + + if (!$log) + { + Write-Warning ` + -Message ($script:localizedData.WindowsEventLogNotFound -f $LogName) + } + else + { + Write-Verbose -Message ($script:localizedData.WindowsEventLogFound -f $LogName) + return $log + } +} + +<# + .SYNOPSIS + Save the Windows Event Log properties. + + .PARAMETER Log + Specifies the given object of a Windows Event Log. +#> +function Save-LogFile +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [System.Object] + $Log + ) + + try + { + $Log.SaveChanges() + Write-Verbose -Message ($script:localizedData.SaveWindowsEventlogSuccess) + } + catch + { + Write-Verbose -Message ($script:localizedData.SaveWindowsEventlogFailure) + } +} + +<# + .SYNOPSIS + Set the Log Retention for a Windows Event Log. + + .PARAMETER LogName + Specifies the given name of a Windows Event Log. + + .PARAMETER Retention + Specifies the given RetentionDays for LogMode Autobackup. +#> +function Set-LogRetentionDays +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $LogName, + + [Parameter(Mandatory = $true)] + [System.Int32] + $LogRetentionDays + ) + + Write-Verbose -Message ($script:localizedData.SettingEventlogLogRetentionDays -f $LogName, $LogRetentionDays) + + try + { + Limit-Eventlog -LogName $LogName -OverflowAction 'OverwriteOlder' -RetentionDays $LogRetentionDays + Write-Verbose -Message ($script:localizedData.SettingWindowsEventlogRetentionDaysSuccess -f $LogName, $LogRetentionDays) + } + catch + { + Write-Verbose -Message ($script:localizedData.SettingWindowsEventlogRetentionDaysFailed -f $LogName, $LogRetentionDays) + } +} + +Export-ModuleMember -Function *-TargetResource diff --git a/deployment/dsc/azshcihost/ComputerManagementDsc/8.4.0/DSCResources/DSC_WindowsEventLog/DSC_WindowsEventLog.schema.mof b/deployment/dsc/azshcihost/ComputerManagementDsc/8.4.0/DSCResources/DSC_WindowsEventLog/DSC_WindowsEventLog.schema.mof new file mode 100644 index 0000000..379bfd7 --- /dev/null +++ b/deployment/dsc/azshcihost/ComputerManagementDsc/8.4.0/DSCResources/DSC_WindowsEventLog/DSC_WindowsEventLog.schema.mof @@ -0,0 +1,12 @@ + +[ClassVersion("1.0.0.1"), FriendlyName("WindowsEventLog")] +class DSC_WindowsEventLog : OMI_BaseResource +{ + [Key, Description("Specifies the given name of a Windows Event Log")] String LogName; + [Write, Description("Specifies the given state of a Windows Event Log")] Boolean IsEnabled; + [Write, Description("Specifies the given maximum size in bytes for a specified Windows Event Log")] Sint64 MaximumSizeInBytes; + [Write, Description("Specifies the given LogMode for a specified Windows Event Log"), ValueMap{"AutoBackup","Circular","Retain"}, Values{"AutoBackup","Circular","Retain"}] String LogMode; + [Write, Description("Specifies the given SecurityDescriptor for a specified Windows Event Log")] String SecurityDescriptor; + [Write, Description("Specifies the given LogFile path of a Windows Event Log")] String LogFilePath; + [Write, Description("Specifies the given LogRetentionDays for the Logmode 'AutoBackup'")] Sint32 LogRetentionDays; +}; diff --git a/deployment/dsc/azshcihost/ComputerManagementDsc/8.4.0/DSCResources/DSC_WindowsEventLog/README.md b/deployment/dsc/azshcihost/ComputerManagementDsc/8.4.0/DSCResources/DSC_WindowsEventLog/README.md new file mode 100644 index 0000000..3960eec --- /dev/null +++ b/deployment/dsc/azshcihost/ComputerManagementDsc/8.4.0/DSCResources/DSC_WindowsEventLog/README.md @@ -0,0 +1,5 @@ +# Description + +This resource allows the configuration of the Logsize, Logmode, SecurityDescriptor, +RetentionDays and enabled/disabled the state of a specified Windows Event Log. +It is also possible to set the maximum size of the Windows Event Log. diff --git a/deployment/dsc/azshcihost/ComputerManagementDsc/8.4.0/DSCResources/DSC_WindowsEventLog/en-US/DSC_WindowsEventLog.strings.psd1 b/deployment/dsc/azshcihost/ComputerManagementDsc/8.4.0/DSCResources/DSC_WindowsEventLog/en-US/DSC_WindowsEventLog.strings.psd1 new file mode 100644 index 0000000..ef6fc6f --- /dev/null +++ b/deployment/dsc/azshcihost/ComputerManagementDsc/8.4.0/DSCResources/DSC_WindowsEventLog/en-US/DSC_WindowsEventLog.strings.psd1 @@ -0,0 +1,24 @@ +# culture ="en-US" +ConvertFrom-StringData -StringData @' + GettingEventlogName = Getting the Windows Event Log '{0}'. + TestingWindowsEventlogSecurityDescriptor = Setting the SecurityDescriptor for Windows Event Log '{0}' to '{1}'. + TestingWindowsEventlogLogFilePath = Setting the LogFilePath for Windows Event Log '{0}' to '{1}'. + TestingEventlogMaximumSizeInBytes = Testing the given LogSize '{1}' for Windows Event Log '{0}'. + TestingEventlogLogMode = Testing the given LogMode '{1}' for Windows Event Log '{0}'. + TestingEventlogLogRetentionDays = Testing the given Retention '{1}' days for Windows Event Log '{0}'. + TestingEventlogIsEnabled = Testing the given State '{1}' for Windows Event Log '{0}'. + SettingEventlogLogMode = Setting the LogMode for Windows Event Log '{0}' to '{1}'. + SettingEventlogLogRetentionDays = Setting the Log Retention for Windows Event Log '{0}' to '{1}' days. + SettingEventlogLogSize = Setting the LogSize for Windows Event Log '{0}' to '{1}'. + SettingEventlogLogFilePath = Setting the LogFilePath for Windows Event Log '{0}' to '{1}'. + SettingEventlogIsEnabled = Setting the IsEnabled configuration for Windows Event Log '{0}' to '{1}'. + SettingEventlogSecurityDescriptor = Setting the SecurityDescriptor configuration for Windows Event Log '{0}' to '{1}'. + SettingWindowsEventlogRetentionDaysSuccess = Updating Logfile Retention for Windows Event Log '{0}' successfully to '{1}' days. + SettingWindowsEventlogRetentionDaysFailed = Updating Logfile Retention for Windows Event Log '{0}' to '{1}' failed. + SetResourceIsInDesiredState = Windows Event Log '{0}' is in desired state for configuration '{1}'. + EventlogLogRetentionDaysWrongMode = Setting the Log Retention for Windows Event Log '{0}' failed. LogMode must be AutoBackup. + SaveWindowsEventlogSuccess = Saving Windows Event Log settings successful. + SaveWindowsEventlogFailure = Saving Windows Event Log settings failed. + WindowsEventLogNotFound = Windows Event Log '{0}' is not found. + WindowsEventLogFound = Windows Event Log '{0}' was found. +'@ diff --git a/deployment/dsc/azshcihost/ComputerManagementDsc/8.4.0/Modules/ComputerManagementDsc.Common/ComputerManagementDsc.Common.psm1 b/deployment/dsc/azshcihost/ComputerManagementDsc/8.4.0/Modules/ComputerManagementDsc.Common/ComputerManagementDsc.Common.psm1 new file mode 100644 index 0000000..9976f09 --- /dev/null +++ b/deployment/dsc/azshcihost/ComputerManagementDsc/8.4.0/Modules/ComputerManagementDsc.Common/ComputerManagementDsc.Common.psm1 @@ -0,0 +1,635 @@ +$modulePath = Join-Path -Path (Split-Path -Path (Split-Path -Path $PSScriptRoot -Parent) -Parent) -ChildPath 'Modules' +Import-Module -Name (Join-Path -Path $modulePath -ChildPath 'DscResource.Common') + +# Import Localization Strings +$script:localizedData = Get-LocalizedData -DefaultUICulture 'en-US' + +<# + .SYNOPSIS + This function tests if a cmdlet exists. + + .PARAMETER Name + The name of the cmdlet to check for. + + .PARAMETER Module + The module containing the command. +#> +function Test-Command +{ + [CmdletBinding()] + [OutputType([System.Boolean])] + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $Name, + + [Parameter(Mandatory = $true)] + [System.String] + $Module + ) + + return ($null -ne (Get-Command @PSBoundParameters -ErrorAction SilentlyContinue)) +} # function Test-Command + +<# + .SYNOPSIS + Get the of the current time zone Id. + + .NOTES + This function is also used by ScheduledTask integration tests. +#> +function Get-TimeZoneId +{ + [CmdletBinding()] + param + ( + ) + + if (Test-Command -Name 'Get-TimeZone' -Module 'Microsoft.PowerShell.Management') + { + Write-Verbose -Message ($script:localizedData.GettingTimeZoneMessage -f 'Cmdlets') + + $timeZone = (Get-TimeZone).StandardName + } + else + { + Write-Verbose -Message ($script:localizedData.GettingTimeZoneMessage -f 'CIM') + + $timeZone = (Get-CimInstance ` + -ClassName Win32_TimeZone ` + -Namespace root\cimv2).StandardName + } + + Write-Verbose -Message ($script:localizedData.CurrentTimeZoneMessage -f $timeZone) + + $timeZoneInfo = [System.TimeZoneInfo]::GetSystemTimeZones() | + Where-Object -Property StandardName -EQ $timeZone + + return $timeZoneInfo.Id +} # function Get-TimeZoneId + +<# + .SYNOPSIS + Compare a time zone Id with the current time zone Id. + + .PARAMETER TimeZoneId + The Id of the time zone to compare with the current time zone. + + .NOTES + This function is also used by ScheduledTask integration tests. +#> +function Test-TimeZoneId +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $TimeZoneId + ) + + # Test if the expected value is the same as the current value. + $currentTimeZoneId = Get-TimeZoneId + + return $TimeZoneId -eq $currentTimeZoneId +} # function Test-TimeZoneId + +<# + .SYNOPSIS + Sets the current time zone using a time zone Id. + + .PARAMETER TimeZoneId + The Id of the time zone to set. + + .NOTES + This function is also used by ScheduledTask integration tests. +#> +function Set-TimeZoneId +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $TimeZoneId + ) + + if (Test-Command -Name 'Set-TimeZone' -Module 'Microsoft.PowerShell.Management') + { + Set-TimeZone -Id $TimeZoneId + } + else + { + if (Test-Command -Name 'Add-Type' -Module 'Microsoft.Powershell.Utility') + { + # We can use reflection to modify the time zone. + Write-Verbose -Message ($script:localizedData.SettingTimeZoneMessage -f $TimeZoneId, '.NET') + + Set-TimeZoneUsingDotNet -TimeZoneId $TimeZoneId + } + else + { + # For anything else use TZUTIL.EXE. + Write-Verbose -Message ($script:localizedData.SettingTimeZoneMessage -f $TimeZoneId, 'TZUTIL.EXE') + + try + { + & tzutil.exe @('/s', $TimeZoneId) + } + catch + { + Write-Verbose -Message $_.Exception.Message + } # try + } # if + } # if + + Write-Verbose -Message ($script:localizedData.TimeZoneUpdatedMessage -f $TimeZoneId) +} # function Set-TimeZoneId + +<# + .SYNOPSIS + This function sets the time zone on the machine using .NET reflection. + It exists so that the ::Set method can be mocked by Pester. + + .PARAMETER TimeZoneId + The Id of the time zone to set using .NET. + + .NOTES + This function is also used by ScheduledTask integration tests. +#> +function Set-TimeZoneUsingDotNet +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $TimeZoneId + ) + + # Add the [TimeZoneHelper.TimeZone] type if it is not defined. + if (-not ([System.Management.Automation.PSTypeName] 'TimeZoneHelper.TimeZone').Type) + { + Write-Verbose -Message ($script:localizedData.AddingSetTimeZoneDotNetTypeMessage) + + $setTimeZoneCs = Get-Content ` + -Path (Join-Path -Path $PSScriptRoot -ChildPath 'SetTimeZone.cs') ` + -Raw + + Add-Type ` + -Language CSharp ` + -TypeDefinition $setTimeZoneCs + } # if + + [Microsoft.PowerShell.TimeZone.TimeZone]::Set($TimeZoneId) +} # function Set-TimeZoneUsingDotNet + +<# + .SYNOPSIS + This function gets a specific power plan or all available power plans. + The function returns one or more hashtable(s) containing + the friendly name and GUID of the power plan(s). + + .PARAMETER PowerPlan + Friendly name or GUID of a power plan to get. + When not specified the function will return all available power plans. + + .NOTES + This function is used by the PowerPlan resource. +#> +function Get-PowerPlan +{ + [CmdletBinding()] + [OutputType([System.Collections.Hashtable[]])] + param + ( + [Parameter()] + [ValidateNotNullOrEmpty()] + [System.String] + $PowerPlan + ) + + $ErrorActionPreference = 'Stop' + + # Get all available power plan(s) as a hashtable with friendly name and GUID + $allAvailablePowerPlans = Get-PowerPlanUsingPInvoke + + # If a specific power plan is specified filter for it otherwise return all + if ($PSBoundParameters.ContainsKey('PowerPlan')) + { + $selectedPowerPlan = $allAvailablePowerPlans | Where-Object -FilterScript { + ($_.FriendlyName -eq $PowerPlan) -or + ($_.Guid -eq $PowerPlan) + } + + return $selectedPowerPlan + } + else + { + return $allAvailablePowerPlans + } +} + +<# + .SYNOPSIS + This function gets the friendly name of a power plan specified by its GUID. + + .PARAMETER PowerPlanGuid + The GUID of a power plan. + + .NOTES + This function uses Platform Invoke (P/Invoke) mechanism to call native Windows APIs + because the Win32_PowerPlan WMI class has issues on some platforms or is unavailable at all. + e.g Server 2012 R2 core or Nano Server. + This function is used by the Get-PowerPlan function. +#> +function Get-PowerPlanFriendlyName +{ + [CmdletBinding()] + [OutputType([System.String])] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.Guid] + $PowerPlanGuid + ) + + $ErrorActionPreference = 'Stop' + + # Define C# signature of PowerReadFriendlyName function + $MethodDefinition = @' + [DllImport("powrprof.dll", CharSet = CharSet.Unicode)] + public static extern uint PowerReadFriendlyName( + IntPtr RootPowerKey, + Guid SchemeGuid, + IntPtr SubGroupOfPowerSettingGuid, + IntPtr PowerSettingGuid, + IntPtr Buffer, + ref uint BufferSize + ); +'@ + + # Create Win32PowerReadFriendlyName object with the static method PowerReadFriendlyName. + $powerprof = Add-Type ` + -MemberDefinition $MethodDefinition ` + -Name 'Win32PowerReadFriendlyName' ` + -Namespace 'Win32Functions' ` + -PassThru + + # Define variable for buffer size which whe have frist to figure out. + $bufferSize = 0 + $returnCode = 0 + + try + { + <# + Frist get needed buffer size by calling PowerReadFriendlyName + with NULL value for 'Buffer' parameter to get the required buffer size. + #> + $returnCode = $powerprof::PowerReadFriendlyName( + [System.IntPtr]::Zero, + $PowerPlanGuid, + [System.IntPtr]::Zero, + [System.IntPtr]::Zero, + [System.IntPtr]::Zero, + [ref]$bufferSize) + + if ($returnCode -eq 0) + { + try + { + # Now lets allocate the needed buffer size + $ptrName = [System.Runtime.InteropServices.Marshal]::AllocHGlobal([System.Int32]$bufferSize) + + <# + Get the actual friendly name of the powerlan by calling PowerReadFriendlyName again. + This time with the correct buffer size for the 'Buffer' parameter. + #> + $returnCode = $powerprof::PowerReadFriendlyName( + [System.IntPtr]::Zero, + $PowerPlanGuid, + [System.IntPtr]::Zero, + [System.IntPtr]::Zero, + $ptrName, + [ref]$bufferSize) + + if ($returnCode -eq 0) + { + # Create a managed String object form the unmanged memory block. + $friendlyName = [System.Runtime.InteropServices.Marshal]::PtrToStringUni($ptrName) + return $friendlyName + } + else + { + throw [ComponentModel.Win32Exception]::new([System.Int32]$returnCode) + } + } + finally + { + # Make sure allocated memory is freed up again. + [System.Runtime.InteropServices.Marshal]::FreeHGlobal($ptrName) + } + } + else + { + throw [ComponentModel.Win32Exception]::new([System.Int32]$returnCode) + } + } + catch + { + New-InvalidOperationException ` + -Message ($script:localizedData.UnableToGetPowerSchemeFriendlyName -f $PowerPlanGuid, $_.Exception.NativeErrorCode, $_.Exception.Message) + } +} + +<# + .SYNOPSIS + This function gets the GUID of the currently active power plan. + + .NOTES + This function uses Platform Invoke (P/Invoke) mechanism to call native Windows APIs + because the Win32_PowerPlan WMI class has issues on some platforms or is unavailable at all. + e.g Server 2012 R2 core or Nano Server. + This function is used by the PowerPlan resource. +#> +function Get-ActivePowerPlan +{ + [CmdletBinding()] + [OutputType([System.Guid])] + param + ( + ) + + $ErrorActionPreference = 'Stop' + + # Define C# signature of PowerGetActiveScheme function + $powerGetActiveSchemeDefinition = @' + [DllImport("powrprof.dll", CharSet = CharSet.Unicode)] + public static extern uint PowerGetActiveScheme(IntPtr UserRootPowerKey, ref IntPtr ActivePolicyGuid); +'@ + + $returnCode = 0 + + # Create Win32PowerGetActiveScheme object with the static method PowerGetActiveScheme + $powrprof = Add-Type ` + -MemberDefinition $powerGetActiveSchemeDefinition ` + -Name 'Win32PowerGetActiveScheme' ` + -Namespace 'Win32Functions' ` + -PassThru + + try + { + # Get the GUID of the active power scheme + $activeSchemeGuid = [System.IntPtr]::Zero + $returnCode = $powrprof::PowerGetActiveScheme([System.IntPtr]::Zero, [ref]$activeSchemeGuid) + + # Check for non 0 return codes / errors form the native function + if ($returnCode -ne 0) + { + # Create a Win32Exception object out of the return code + $win32Exception = ([ComponentModel.Win32Exception]::new([System.Int32]$returnCode)) + New-InvalidOperationException ` + -Message ($script:localizedData.FailedToGetActivePowerScheme -f $win32Exception.NativeErrorCode, $win32Exception.Message) + } + + # Create a managed Guid object form the unmanged memory block and return it + return [System.Runtime.InteropServices.Marshal]::PtrToStructure($activeSchemeGuid, [System.Type][System.Guid]) + } + finally + { + # Make sure allocated memory is freed up again. + [System.Runtime.InteropServices.Marshal]::FreeHGlobal($activeSchemeGuid) + } +} + +<# + .SYNOPSIS + This function enumerates all available power plans/schemes. + The function returns one or more hashtable(s) containing + the friendly name and GUID of the power plan(s). + + .NOTES + This function uses Platform Invoke (P/Invoke) mechanism to call native Windows APIs + because the Win32_PowerPlan WMI class has issues on some platforms or is unavailable at all. + e.g Server 2012 R2 core or Nano Server. + This function is used by the PowerPlan resource. +#> +function Get-PowerPlanUsingPInvoke +{ + [CmdletBinding()] + [OutputType([System.Collections.Hashtable[]])] + param + ( + ) + + $ErrorActionPreference = 'Stop' + + Write-Verbose -Message ($script:localizedData.EnumeratingPowerPlans) + + # Define C# signature of PowerEnumerate function + $powerEnumerateDefinition = @' + [DllImport("powrprof.dll", CharSet = CharSet.Unicode)] + public static extern uint PowerEnumerate( + IntPtr RootPowerKey, + IntPtr SchemeGuid, + IntPtr SubGroupOfPowerSetting, + int AccessFlags, + uint Index, + IntPtr rBuffer, + ref uint BufferSize + ); +'@ + + # Create Win32PowerEnumerate object with the static method PowerEnumerate + $powrprof = Add-Type ` + -MemberDefinition $powerEnumerateDefinition ` + -Name 'Win32PowerEnumerate' ` + -Namespace 'Win32Functions' ` + -PassThru + + $index = 0 + $returnCode = 0 + $allAvailablePowerPlans = [System.Collections.ArrayList]::new() + + # PowerEnumerate returns the GUID of the powerplan(s). Guid = 16 Bytes. + $bufferSize = 16 + + <# + The PowerEnumerate function returns only one guid at a time. + So we have to loop here until error code 259 (no more data) is returned to get all power plan GUIDs. + #> + while ($returnCode -ne 259) + { + try + { + # Allocate buffer + $readBuffer = [System.Runtime.InteropServices.Marshal]::AllocHGlobal([System.Int32]$bufferSize) + + # Get Guid of the power plan using the native PowerEnumerate function + $returnCode = $powrprof::PowerEnumerate([System.IntPtr]::Zero, [System.IntPtr]::Zero, [System.IntPtr]::Zero, 16, $index, $readBuffer, [ref]$bufferSize) + + # Return Code 259 means no more data so we stop here. + if ($returnCode -eq 259) + { + break + } + + # Check for non 0 return codes / errors form the native function. + if ($returnCode -ne 0) + { + # Create a Win32Exception object out of the return code + $win32Exception = ([ComponentModel.Win32Exception]::new([System.Int32]$returnCode)) + New-InvalidOperationException ` + -Message ($script:localizedData.UnableToEnumeratingPowerSchemes -f $win32Exception.NativeErrorCode, $win32Exception.Message) + } + + # Create a managed Guid object form the unmanaged memory block + $planGuid = [System.Runtime.InteropServices.Marshal]::PtrToStructure($readBuffer, [System.Type][System.Guid]) + + Write-Verbose -Message ($script:localizedData.PowerPlanFound -f $planGuid) + + # Now get the friendly name of to the power plan + $planFriendlyName = Get-PowerPlanFriendlyName -PowerPlanGuid $planGuid + + Write-Verbose -Message ($script:localizedData.PowerPlanFriendlyNameFound -f $planFriendlyName) + + $null = $allAvailablePowerPlans.Add( + @{ + FriendlyName = $planFriendlyName + Guid = $planGuid + } + ) + + $index++ + } + finally + { + # Free up memory + [System.Runtime.InteropServices.Marshal]::FreeHGlobal($readBuffer) + } + } + + Write-Verbose -Message ($script:localizedData.AllPowerPlansFound) + + return $allAvailablePowerPlans +} + +<# + .SYNOPSIS + This function activates a specific power plan (specified by its GUID). + + .PARAMETER Guid + GUID of a power plan to activate. + + .NOTES + This function uses Platform Invoke (P/Invoke) mechanism to call native Windows APIs + because the Win32_PowerPlan WMI class has on some platforms issues or is unavailable at all. + e.g Server 2012 R2 core or Nano Server. + This function is used by the Get-PowerPlan function respectively the PowerPlan resource. +#> +function Set-ActivePowerPlan +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [System.Guid] + $PowerPlanGuid + ) + + $ErrorActionPreference = 'Stop' + + # Define C# signature of PowerSetActiveScheme function + $powerSetActiveSchemeDefinition = @' + [DllImport("powrprof.dll", CharSet = CharSet.Auto)] + public static extern uint PowerSetActiveScheme( + IntPtr RootPowerKey, + Guid SchemeGuid + ); +'@ + + # Create Win32PowerSetActiveScheme object with the static method PowerSetActiveScheme. + $powrprof = Add-Type ` + -MemberDefinition $powerSetActiveSchemeDefinition ` + -Name 'Win32PowerSetActiveScheme' ` + -Namespace 'Win32Functions' ` + -PassThru + + try + { + # Set the active power scheme with the native function + $returnCode = $powrprof::PowerSetActiveScheme([System.IntPtr]::Zero, $PowerPlanGuid) + + # Check for non 0 return codes / errors form the native function + if ($returnCode -ne 0) + { + throw [ComponentModel.Win32Exception]::new([int]$returnCode) + } + } + catch + { + New-InvalidOperationException ` + -Message ($script:localizedData.FailedToSetActivePowerScheme -f $PowerPlanGuid, $_.Exception.NativeErrorCode, $_.Exception.Message) + } +} + +<# + .SYNOPSIS + Returns the value of the provided in the Name parameter, at the registry + location provided in the Path parameter. + + .PARAMETER Path + String containing the path in the registry to the property name. + + .PARAMETER PropertyName + String containing the name of the property for which the value is returned. +#> +function Get-RegistryPropertyValue +{ + [CmdletBinding()] + [OutputType([System.String])] + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $Path, + + [Parameter(Mandatory = $true)] + [System.String] + $Name + ) + + $getItemPropertyParameters = @{ + Path = $Path + Name = $Name + } + + <# + Using a try/catch block instead of 'SilentlyContinue' to be + able to unit test a failing registry path. + #> + try + { + $getItemPropertyResult = (Get-ItemProperty @getItemPropertyParameters -ErrorAction Stop).$Name + } + catch + { + $getItemPropertyResult = $null + } + + return $getItemPropertyResult +} + +Export-ModuleMember -Function @( + 'Test-Command' + 'Get-TimeZoneId' + 'Test-TimeZoneId' + 'Set-TimeZoneId' + 'Set-TimeZoneUsingDotNet' + 'Get-PowerPlan' + 'Get-ActivePowerPlan' + 'Set-ActivePowerPlan' + 'Get-RegistryPropertyValue' +) diff --git a/deployment/dsc/azshcihost/ComputerManagementDsc/8.4.0/Modules/ComputerManagementDsc.Common/SetTimeZone.cs b/deployment/dsc/azshcihost/ComputerManagementDsc/8.4.0/Modules/ComputerManagementDsc.Common/SetTimeZone.cs new file mode 100644 index 0000000..ab10181 --- /dev/null +++ b/deployment/dsc/azshcihost/ComputerManagementDsc/8.4.0/Modules/ComputerManagementDsc.Common/SetTimeZone.cs @@ -0,0 +1,271 @@ +// This file enables setting of the machine time zone +// using .NET framework classes. +// It can be used to set the time zone when the +// PowerShell time zone cmdlets are not available. +// +// It creates a namespace Microsoft.PowerShell.TimeZone +// containing the classes and structures required to +// set the time zone. +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Security; +using System.Runtime.InteropServices; +using Microsoft.Win32; +namespace Microsoft.PowerShell.TimeZone +{ + [StructLayoutAttribute(LayoutKind.Sequential)] + public struct SystemTime + { + [MarshalAs(UnmanagedType.U2)] + public short Year; + [MarshalAs(UnmanagedType.U2)] + public short Month; + [MarshalAs(UnmanagedType.U2)] + public short DayOfWeek; + [MarshalAs(UnmanagedType.U2)] + public short Day; + [MarshalAs(UnmanagedType.U2)] + public short Hour; + [MarshalAs(UnmanagedType.U2)] + public short Minute; + [MarshalAs(UnmanagedType.U2)] + public short Second; + [MarshalAs(UnmanagedType.U2)] + public short Milliseconds; + } + [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] + public struct TimeZoneInformation + { + [MarshalAs(UnmanagedType.I4)] + public int Bias; + [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 0x20)] + public string StandardName; + public SystemTime StandardDate; + [MarshalAs(UnmanagedType.I4)] + public int StandardBias; + [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 0x20)] + public string DaylightName; + public SystemTime DaylightDate; + [MarshalAs(UnmanagedType.I4)] + public int DaylightBias; + } + [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] + public struct DynamicTimeZoneInformation + { + public int Bias; + [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 32)] + public string StandardName; + public SystemTime StandardDate; + public int StandardBias; + [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 32)] + public string DaylightName; + public SystemTime DaylightDate; + public int DaylightBias; + [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 128)] + public string TimeZoneKeyName; + [MarshalAs(UnmanagedType.U1)] + public bool DynamicDaylightTimeDisabled; + } + [StructLayout(LayoutKind.Sequential)] + public struct RegistryTimeZoneInformation + { + [MarshalAs(UnmanagedType.I4)] + public int Bias; + [MarshalAs(UnmanagedType.I4)] + public int StandardBias; + [MarshalAs(UnmanagedType.I4)] + public int DaylightBias; + public SystemTime StandardDate; + public SystemTime DaylightDate; + public RegistryTimeZoneInformation(TimeZoneInformation tzi) + { + this.Bias = tzi.Bias; + this.StandardDate = tzi.StandardDate; + this.StandardBias = tzi.StandardBias; + this.DaylightDate = tzi.DaylightDate; + this.DaylightBias = tzi.DaylightBias; + } + public RegistryTimeZoneInformation(byte[] bytes) + { + if ((bytes == null) || (bytes.Length != 0x2c)) + { + throw new ArgumentException("Argument_InvalidREG_TZI_FORMAT"); + } + this.Bias = BitConverter.ToInt32(bytes, 0); + this.StandardBias = BitConverter.ToInt32(bytes, 4); + this.DaylightBias = BitConverter.ToInt32(bytes, 8); + this.StandardDate.Year = BitConverter.ToInt16(bytes, 12); + this.StandardDate.Month = BitConverter.ToInt16(bytes, 14); + this.StandardDate.DayOfWeek = BitConverter.ToInt16(bytes, 0x10); + this.StandardDate.Day = BitConverter.ToInt16(bytes, 0x12); + this.StandardDate.Hour = BitConverter.ToInt16(bytes, 20); + this.StandardDate.Minute = BitConverter.ToInt16(bytes, 0x16); + this.StandardDate.Second = BitConverter.ToInt16(bytes, 0x18); + this.StandardDate.Milliseconds = BitConverter.ToInt16(bytes, 0x1a); + this.DaylightDate.Year = BitConverter.ToInt16(bytes, 0x1c); + this.DaylightDate.Month = BitConverter.ToInt16(bytes, 30); + this.DaylightDate.DayOfWeek = BitConverter.ToInt16(bytes, 0x20); + this.DaylightDate.Day = BitConverter.ToInt16(bytes, 0x22); + this.DaylightDate.Hour = BitConverter.ToInt16(bytes, 0x24); + this.DaylightDate.Minute = BitConverter.ToInt16(bytes, 0x26); + this.DaylightDate.Second = BitConverter.ToInt16(bytes, 40); + this.DaylightDate.Milliseconds = BitConverter.ToInt16(bytes, 0x2a); + } + } + public class TokenPrivilegesAccess + { + [DllImport("advapi32.dll", CharSet = CharSet.Auto)] + public static extern int OpenProcessToken(int ProcessHandle, int DesiredAccess, + ref int tokenhandle); + [DllImport("kernel32.dll", CharSet = CharSet.Auto)] + public static extern int GetCurrentProcess(); + [DllImport("advapi32.dll", CharSet = CharSet.Auto)] + public static extern int LookupPrivilegeValue(string lpsystemname, string lpname, + [MarshalAs(UnmanagedType.Struct)] ref LUID lpLuid); + [DllImport("advapi32.dll", CharSet = CharSet.Auto)] + public static extern int AdjustTokenPrivileges(int tokenhandle, int disableprivs, + [MarshalAs(UnmanagedType.Struct)]ref TOKEN_PRIVILEGE Newstate, int bufferlength, + int PreivousState, int Returnlength); + public const int TOKEN_ASSIGN_PRIMARY = 0x00000001; + public const int TOKEN_DUPLICATE = 0x00000002; + public const int TOKEN_IMPERSONATE = 0x00000004; + public const int TOKEN_QUERY = 0x00000008; + public const int TOKEN_QUERY_SOURCE = 0x00000010; + public const int TOKEN_ADJUST_PRIVILEGES = 0x00000020; + public const int TOKEN_ADJUST_GROUPS = 0x00000040; + public const int TOKEN_ADJUST_DEFAULT = 0x00000080; + public const UInt32 SE_PRIVILEGE_ENABLED_BY_DEFAULT = 0x00000001; + public const UInt32 SE_PRIVILEGE_ENABLED = 0x00000002; + public const UInt32 SE_PRIVILEGE_REMOVED = 0x00000004; + public const UInt32 SE_PRIVILEGE_USED_FOR_ACCESS = 0x80000000; + public static bool EnablePrivilege(string privilege) + { + try + { + int token = 0; + int retVal = 0; + TOKEN_PRIVILEGE TP = new TOKEN_PRIVILEGE(); + LUID LD = new LUID(); + retVal = OpenProcessToken(GetCurrentProcess(), TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY, ref token); + retVal = LookupPrivilegeValue(null, privilege, ref LD); + TP.PrivilegeCount = 1; + var luidAndAtt = new LUID_AND_ATTRIBUTES(); + luidAndAtt.Attributes = SE_PRIVILEGE_ENABLED; + luidAndAtt.Luid = LD; + TP.Privilege = luidAndAtt; + retVal = AdjustTokenPrivileges(token, 0, ref TP, 1024, 0, 0); + return true; + } + catch + { + return false; + } + } + public static bool DisablePrivilege(string privilege) + { + try + { + int token = 0; + int retVal = 0; + TOKEN_PRIVILEGE TP = new TOKEN_PRIVILEGE(); + LUID LD = new LUID(); + retVal = OpenProcessToken(GetCurrentProcess(), TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY, ref token); + retVal = LookupPrivilegeValue(null, privilege, ref LD); + TP.PrivilegeCount = 1; + // TP.Attributes should be none (not set) to disable privilege + var luidAndAtt = new LUID_AND_ATTRIBUTES(); + luidAndAtt.Luid = LD; + TP.Privilege = luidAndAtt; + retVal = AdjustTokenPrivileges(token, 0, ref TP, 1024, 0, 0); + return true; + } + catch + { + return false; + } + } + } + [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] + public struct LUID + { + internal uint LowPart; + internal uint HighPart; + } + [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] + public struct LUID_AND_ATTRIBUTES + { + internal LUID Luid; + internal uint Attributes; + } + [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] + public struct TOKEN_PRIVILEGE + { + internal uint PrivilegeCount; + internal LUID_AND_ATTRIBUTES Privilege; + } + public class TimeZone + { + public const int ERROR_ACCESS_DENIED = 0x005; + public const int CORSEC_E_MISSING_STRONGNAME = -2146233317; + [DllImport("kernel32.dll", CharSet = CharSet.Auto)] + private static extern bool SetTimeZoneInformation([In] ref TimeZoneInformation lpTimeZoneInformation); + [DllImport("kernel32.dll", CharSet = CharSet.Auto)] + private static extern bool SetDynamicTimeZoneInformation([In] ref DynamicTimeZoneInformation lpTimeZoneInformation); + public static void Set(string name) + { + var regTimeZones = Registry.LocalMachine.OpenSubKey("SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\Time Zones"); + var subKey = regTimeZones.GetSubKeyNames().Where(s => s == name).First(); + string daylightName = (string)regTimeZones.OpenSubKey(subKey).GetValue("Dlt"); + string standardName = (string)regTimeZones.OpenSubKey(subKey).GetValue("Std"); + byte[] tzi = (byte[])regTimeZones.OpenSubKey(subKey).GetValue("TZI"); + var regTzi = new RegistryTimeZoneInformation(tzi); + TokenPrivilegesAccess.EnablePrivilege("SeTimeZonePrivilege"); + bool didSet; + if (Environment.OSVersion.Version.Major < 6) + { + var tz = new TimeZoneInformation(); + tz.Bias = regTzi.Bias; + tz.DaylightBias = regTzi.DaylightBias; + tz.StandardBias = regTzi.StandardBias; + tz.DaylightDate = regTzi.DaylightDate; + tz.StandardDate = regTzi.StandardDate; + tz.DaylightName = daylightName; + tz.StandardName = standardName; + didSet = TimeZone.SetTimeZoneInformation(ref tz); + } + else + { + var tz = new DynamicTimeZoneInformation(); + tz.Bias = regTzi.Bias; + tz.DaylightBias = regTzi.DaylightBias; + tz.StandardBias = regTzi.StandardBias; + tz.DaylightDate = regTzi.DaylightDate; + tz.StandardDate = regTzi.StandardDate; + tz.DaylightName = daylightName; + tz.StandardName = standardName; + tz.TimeZoneKeyName = subKey; + tz.DynamicDaylightTimeDisabled = false; + didSet = TimeZone.SetDynamicTimeZoneInformation(ref tz); + } + int lastError = Marshal.GetLastWin32Error(); + TokenPrivilegesAccess.DisablePrivilege("SeTimeZonePrivilege"); + if (! didSet) + { + if (lastError == TimeZone.ERROR_ACCESS_DENIED) + { + throw new SecurityException("Access denied changing System TimeZone."); + } + else if (lastError == TimeZone.CORSEC_E_MISSING_STRONGNAME) + { + throw new SystemException("Application is not signed."); + } + else + { + throw new SystemException("Win32Error: " + lastError + "\nHRESULT: " + Marshal.GetHRForLastWin32Error()); + } + } + } + } +} diff --git a/deployment/dsc/azshcihost/ComputerManagementDsc/8.4.0/Modules/ComputerManagementDsc.Common/en-US/ComputerManagementDsc.Common.strings.psd1 b/deployment/dsc/azshcihost/ComputerManagementDsc/8.4.0/Modules/ComputerManagementDsc.Common/en-US/ComputerManagementDsc.Common.strings.psd1 new file mode 100644 index 0000000..54298d4 --- /dev/null +++ b/deployment/dsc/azshcihost/ComputerManagementDsc/8.4.0/Modules/ComputerManagementDsc.Common/en-US/ComputerManagementDsc.Common.strings.psd1 @@ -0,0 +1,15 @@ +ConvertFrom-StringData @' + CurrentTimeZoneMessage = Current time zone is set to '{0}'. + GettingTimeZoneMessage = Getting current time zone using '{0}'. + SettingTimeZoneMessage = Setting time zone to '{0}' using '{1}'. + TimeZoneUpdatedMessage = Time zone has been updated to '{0}'. + AddingSetTimeZoneDotNetTypeMessage = Adding .NET Set time zone Type. + UnableToEnumeratingPowerSchemes = Error occurred while enumerating power schemes. Win32 error code: {0} - {1} + UnableToGetPowerSchemeFriendlyName = Error occurred while getting the friendly name of the power scheme with the GUID {0}. Win32 error code: {1} - {2} + FailedToGetActivePowerScheme = Error occurred while getting active power scheme. Win32 error code: {0} - {1} + FailedToSetActivePowerScheme = Error occurred while activating power scheme with the GUID {0}. Win32 error code: {1} - {2} + EnumeratingPowerPlans = Enumerating all available power plans/schemes on the system using native Win32 function 'PowerEnumerate'. + PowerPlanFound = Found power scheme '{0}'. Getting friendly name. + PowerPlanFriendlyNameFound = Friendly name is '{0}'. + AllPowerPlansFound = Enumerating of available power schemes done. +'@ diff --git a/deployment/dsc/azshcihost/ComputerManagementDsc/8.4.0/Modules/DscResource.Common/0.9.3/DscResource.Common.psd1 b/deployment/dsc/azshcihost/ComputerManagementDsc/8.4.0/Modules/DscResource.Common/0.9.3/DscResource.Common.psd1 new file mode 100644 index 0000000..0c18ec0 --- /dev/null +++ b/deployment/dsc/azshcihost/ComputerManagementDsc/8.4.0/Modules/DscResource.Common/0.9.3/DscResource.Common.psd1 @@ -0,0 +1,73 @@ +@{ + # Script module or binary module file associated with this manifest. + RootModule = 'DscResource.Common.psm1' + + # Version number of this module. + ModuleVersion = '0.9.3' + + # ID used to uniquely identify this module + GUID = '9c9daa5b-5c00-472d-a588-c96e8e498450' + + # Author of this module + Author = 'DSC Community' + + # Company or vendor of this module + CompanyName = 'DSC Community' + + # Copyright statement for this module + Copyright = 'Copyright the DSC Community contributors. All rights reserved.' + + # Description of the functionality provided by this module + Description = 'Common functions used in DSC Resources' + + # Minimum version of the PowerShell engine required by this module + PowerShellVersion = '4.0' + + # Functions to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no functions to export. + FunctionsToExport = @('Assert-BoundParameter','Assert-IPAddress','Assert-Module','Compare-ResourcePropertyState','ConvertTo-CimInstance','ConvertTo-HashTable','Get-LocalizedData','Get-TemporaryFolder','New-InvalidArgumentException','New-InvalidDataException','New-InvalidOperationException','New-InvalidResultException','New-NotImplementedException','New-ObjectNotFoundException','Remove-CommonParameter','Set-DscMachineRebootRequired','Set-PSModulePath','Test-DscParameterState','Test-IsNanoServer') + + # Cmdlets to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no cmdlets to export. + CmdletsToExport = @() + + # Variables to export from this module + VariablesToExport = @() + + # Aliases to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no aliases to export. + AliasesToExport = @() + + # Private data to pass to the module specified in RootModule/ModuleToProcess. This may also contain a PSData hashtable with additional module metadata used by PowerShell. + PrivateData = @{ + + PSData = @{ + # Tags applied to this module. These help with module discovery in online galleries. + Tags = @('DSC', 'Localization') + + # A URL to the license for this module. + LicenseUri = 'https://github.com/dsccommunity/DscResource.Common/blob/master/LICENSE' + + # A URL to the main website for this project. + ProjectUri = 'https://github.com/dsccommunity/DscResource.Common' + + # A URL to an icon representing this module. + IconUri = 'https://dsccommunity.org/images/DSC_Logo_300p.png' + + # ReleaseNotes of this module + ReleaseNotes = '## [0.9.3] - 2020-07-25 + +## Fixed + +- Correction to `Test-DscParameterState` returning false positive when parameter + with an empty array is passed in `DesriedValues` or `CurrentValues` - fixes + [issue #53](https://github.com/dsccommunity/DscResource.Common/issues/53). + +' + + Prerelease = '' + } # End of PSData hashtable + + } # End of PrivateData hashtable +} + + + + diff --git a/deployment/dsc/azshcihost/ComputerManagementDsc/8.4.0/Modules/DscResource.Common/0.9.3/DscResource.Common.psm1 b/deployment/dsc/azshcihost/ComputerManagementDsc/8.4.0/Modules/DscResource.Common/0.9.3/DscResource.Common.psm1 new file mode 100644 index 0000000..17d1159 --- /dev/null +++ b/deployment/dsc/azshcihost/ComputerManagementDsc/8.4.0/Modules/DscResource.Common/0.9.3/DscResource.Common.psm1 @@ -0,0 +1,2179 @@ +#Region './prefix.ps1' 0 +$script:modulesFolderPath = Split-Path -Path $PSScriptRoot -Parent +#EndRegion './prefix.ps1' 1 +#Region './Private/Test-DscObjectHasProperty.ps1' 0 +<# + .SYNOPSIS + Tests if an object has a property. + + .DESCRIPTION + Tests if the specified object has the specified property and return + $true or $false. + + .PARAMETER Object + Specifies the object to test for the specified property. + + .PARAMETER PropertyName + Specifies the property name to test for. + + .EXAMPLE + Test-DscObjectHasProperty -Object 'AnyString' -PropertyName 'Length' +#> +function Test-DscObjectHasProperty +{ + [CmdletBinding()] + [OutputType([System.Boolean])] + param + ( + [Parameter(Mandatory = $true)] + [System.Object] + $Object, + + [Parameter(Mandatory = $true)] + [System.String] + $PropertyName + ) + + if ($Object.PSObject.Properties.Name -contains $PropertyName) + { + return [System.Boolean] $Object.$PropertyName + } + + return $false +} +#EndRegion './Private/Test-DscObjectHasProperty.ps1' 39 +#Region './Private/Test-DscPropertyState.ps1' 0 +<# + .SYNOPSIS + Compares the current and the desired value of a property. + + .DESCRIPTION + This function is used to compare the current and the desired value of a + property. + + .PARAMETER Values + This is set to a hash table with the current value (the CurrentValue key) + and desired value (the DesiredValue key). + + .EXAMPLE + Test-DscPropertyState -Values @{ + CurrentValue = 'John' + DesiredValue = 'Alice' + } + + .EXAMPLE + Test-DscPropertyState -Values @{ + CurrentValue = 1 + DesiredValue = 2 + } + + .NOTES + This function is used by the cmdlet Compare-ResourcePropertyState. +#> +function Test-DscPropertyState +{ + [CmdletBinding()] + [OutputType([System.Boolean])] + param + ( + [Parameter(Mandatory = $true)] + [System.Collections.Hashtable] + $Values + ) + + if ($null -eq $Values.CurrentValue -and $null -eq $Values.DesiredValue) + { + # Both values are $null so return $true + $returnValue = $true + } + elseif ($null -eq $Values.CurrentValue -or $null -eq $Values.DesiredValue) + { + # Either CurrentValue or DesiredValue are $null so return $false + $returnValue = $false + } + elseif ( + $Values.DesiredValue -is [Microsoft.Management.Infrastructure.CimInstance[]] ` + -or $Values.DesiredValue -is [System.Array] -and $Values.DesiredValue[0] -is [Microsoft.Management.Infrastructure.CimInstance] + ) + { + if (-not $Values.ContainsKey('KeyProperties')) + { + $errorMessage = $script:localizedData.KeyPropertiesMissing + + New-InvalidOperationException -Message $errorMessage + } + + $propertyState = @() + + <# + It is a collection of CIM instances, then recursively call + Test-DscPropertyState for each CIM instance in the collection. + #> + foreach ($desiredCimInstance in $Values.DesiredValue) + { + $currentCimInstance = $Values.CurrentValue + + <# + Use the CIM instance Key properties to filter out the current + values if the exist. + #> + foreach ($keyProperty in $Values.KeyProperties) + { + $currentCimInstance = $currentCimInstance | + Where-Object -Property $keyProperty -EQ -Value $desiredCimInstance.$keyProperty + } + + if ($currentCimInstance.Count -gt 1) + { + $errorMessage = $script:localizedData.TooManyCimInstances + + New-InvalidOperationException -Message $errorMessage + } + + if ($currentCimInstance) + { + $keyCimInstanceProperties = $currentCimInstance.CimInstanceProperties | + Where-Object -FilterScript { + $_.Name -in $Values.KeyProperties + } + + <# + For each key property build a string representation of the + property name and its value. + #> + $keyPropertyValues = $keyCimInstanceProperties.ForEach({'{0}="{1}"' -f $_.Name, ($_.Value -join ',')}) + + Write-Debug -Message ( + $script:localizedData.TestingCimInstance -f @( + $currentCimInstance.CimClass.CimClassName, + ($keyPropertyValues -join ';') + ) + ) + } + else + { + $keyCimInstanceProperties = $desiredCimInstance.CimInstanceProperties | + Where-Object -FilterScript { + $_.Name -in $Values.KeyProperties + } + + <# + For each key property build a string representation of the + property name and its value. + #> + $keyPropertyValues = $keyCimInstanceProperties.ForEach({'{0}="{1}"' -f $_.Name, ($_.Value -join ',')}) + + Write-Debug -Message ( + $script:localizedData.MissingCimInstance -f @( + $desiredCimInstance.CimClass.CimClassName, + ($keyPropertyValues -join ';') + ) + ) + } + + # Recursively call Test-DscPropertyState with the CimInstance to evaluate. + $propertyState += Test-DscPropertyState -Values @{ + CurrentValue = $currentCimInstance + DesiredValue = $desiredCimInstance + } + } + + # Return $false if one property is found to not be in desired state. + $returnValue = -not ($false -in $propertyState) + } + elseif ($Values.DesiredValue -is [Microsoft.Management.Infrastructure.CimInstance]) + { + $propertyState = @() + + <# + It is a CIM instance, recursively call Test-DscPropertyState for each + CIM instance property. + #> + $desiredCimInstanceProperties = $Values.DesiredValue.CimInstanceProperties | + Select-Object -Property @('Name', 'Value') + + if ($desiredCimInstanceProperties) + { + foreach ($desiredCimInstanceProperty in $desiredCimInstanceProperties) + { + <# + Recursively call Test-DscPropertyState to evaluate each property + in the CimInstance. + #> + $propertyState += Test-DscPropertyState -Values @{ + CurrentValue = $Values.CurrentValue.($desiredCimInstanceProperty.Name) + DesiredValue = $desiredCimInstanceProperty.Value + } + } + } + else + { + if ($Values.CurrentValue.CimInstanceProperties.Count -gt 0) + { + # Current value did not have any CIM properties, but desired state has. + $propertyState += $false + } + } + + # Return $false if one property is found to not be in desired state. + $returnValue = -not ($false -in $propertyState) + } + elseif ($Values.DesiredValue -is [System.Array] -or $Values.CurrentValue -is [System.Array]) + { + $compareObjectParameters = @{ + ReferenceObject = $Values.CurrentValue + DifferenceObject = $Values.DesiredValue + } + + $arrayCompare = Compare-Object @compareObjectParameters + + if ($null -ne $arrayCompare) + { + Write-Debug -Message $script:localizedData.ArrayDoesNotMatch + + $arrayCompare | + ForEach-Object -Process { + if ($_.SideIndicator -eq '=>') + { + Write-Debug -Message ( + $script:localizedData.ArrayValueIsAbsent -f $_.InputObject + ) + } + else + { + Write-Debug -Message ( + $script:localizedData.ArrayValueIsPresent -f $_.InputObject + ) + } + } + + $returnValue = $false + } + else + { + $returnValue = $true + } + } + elseif ($Values.CurrentValue -ne $Values.DesiredValue) + { + $desiredType = $Values.DesiredValue.GetType() + + $returnValue = $false + + $supportedTypes = @( + 'String' + 'Int32' + 'UInt32' + 'Int16' + 'UInt16' + 'Single' + 'Boolean' + ) + + if ($desiredType.Name -notin $supportedTypes) + { + Write-Warning -Message ($script:localizedData.UnableToCompareType -f $desiredType.Name) + } + else + { + Write-Debug -Message ( + $script:localizedData.PropertyValueOfTypeDoesNotMatch ` + -f $desiredType.Name, $Values.CurrentValue, $Values.DesiredValue + ) + } + } + else + { + $returnValue = $true + } + + return $returnValue +} +#EndRegion './Private/Test-DscPropertyState.ps1' 246 +#Region './Public/Assert-BoundParameter.ps1' 0 +<# + .SYNOPSIS + Throws an error if there is a bound parameter that exists in both the + mutually exclusive lists. + + .DESCRIPTION + Throws an error if there is a bound parameter that exists in both the + mutually exclusive lists. + + .PARAMETER BoundParameterList + The parameters that should be evaluated against the mutually exclusive + lists MutuallyExclusiveList1 and MutuallyExclusiveList2. This parameter is + normally set to the $PSBoundParameters variable. + + .PARAMETER MutuallyExclusiveList1 + An array of parameter names that are not allowed to be bound at the + same time as those in MutuallyExclusiveList2. + + .PARAMETER MutuallyExclusiveList2 + An array of parameter names that are not allowed to be bound at the + same time as those in MutuallyExclusiveList1. + + .EXAMPLE + $assertBoundParameterParameters = @{ + BoundParameterList = $PSBoundParameters + MutuallyExclusiveList1 = @( + 'Parameter1' + ) + MutuallyExclusiveList2 = @( + 'Parameter2' + ) + } + + Assert-BoundParameter @assertBoundParameterParameters + + This example throws an exception if `$PSBoundParameters` contains both + the parameters `Parameter1` and `Parameter2`. +#> +function Assert-BoundParameter +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [AllowEmptyCollection()] + [System.Collections.Hashtable] + $BoundParameterList, + + [Parameter(Mandatory = $true)] + [System.String[]] + $MutuallyExclusiveList1, + + [Parameter(Mandatory = $true)] + [System.String[]] + $MutuallyExclusiveList2 + ) + + $itemFoundFromList1 = $BoundParameterList.Keys.Where({ $_ -in $MutuallyExclusiveList1 }) + $itemFoundFromList2 = $BoundParameterList.Keys.Where({ $_ -in $MutuallyExclusiveList2 }) + + if ($itemFoundFromList1.Count -gt 0 -and $itemFoundFromList2.Count -gt 0) + { + $errorMessage = ` + $script:localizedData.ParameterUsageWrong ` + -f ($MutuallyExclusiveList1 -join "','"), ($MutuallyExclusiveList2 -join "','") + + New-InvalidArgumentException -ArgumentName 'Parameters' -Message $errorMessage + } +} +#EndRegion './Public/Assert-BoundParameter.ps1' 69 +#Region './Public/Assert-IPAddress.ps1' 0 +<# + .SYNOPSIS + Asserts that the specified IP address is valid. + + .DESCRIPTION + Checks the IP address so that it is valid and do not conflict with address + family. If any problems are detected an exception will be thrown. + + .PARAMETER AddressFamily + IP address family that the supplied Address should be in. Valid values are + 'IPv4' or 'IPv6'. + + .PARAMETER Address + Specifies an IPv4 or IPv6 address. + + .EXAMPLE + Assert-IPAddress -Address '127.0.0.1' + + This will assert that the supplied address is a valid IPv4 address. + If it is not an exception will be thrown. + + .EXAMPLE + Assert-IPAddress -Address 'fe80:ab04:30F5:002b::1' + + This will assert that the supplied address is a valid IPv6 address. + If it is not an exception will be thrown. + + .EXAMPLE + Assert-IPAddress -Address 'fe80:ab04:30F5:002b::1' -AddressFamily 'IPv6' + + This will assert that address is valid and that it matches the + supplied address family. If the supplied address family does not match + the address an exception will be thrown. +#> +function Assert-IPAddress +{ + [CmdletBinding()] + param + ( + [Parameter()] + [ValidateSet('IPv4', 'IPv6')] + [System.String] + $AddressFamily, + + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $Address + ) + + [System.Net.IPAddress] $ipAddress = $null + + if (-not ([System.Net.IPAddress]::TryParse($Address, [ref] $ipAddress))) + { + New-InvalidArgumentException ` + -Message ($script:localizedData.AddressFormatError -f $Address) ` + -ArgumentName 'Address' + } + + if ($AddressFamily) + { + switch ($AddressFamily) + { + 'IPv4' + { + if ($ipAddress.AddressFamily -ne [System.Net.Sockets.AddressFamily]::InterNetwork.ToString()) + { + New-InvalidArgumentException ` + -Message ($script:localizedData.AddressIPv6MismatchError -f $Address, $AddressFamily) ` + -ArgumentName 'AddressFamily' + } + } + + 'IPv6' + { + if ($ipAddress.AddressFamily -ne [System.Net.Sockets.AddressFamily]::InterNetworkV6.ToString()) + { + New-InvalidArgumentException ` + -Message ($script:localizedData.AddressIPv4MismatchError -f $Address, $AddressFamily) ` + -ArgumentName 'AddressFamily' + } + } + } + } +} +#EndRegion './Public/Assert-IPAddress.ps1' 85 +#Region './Public/Assert-Module.ps1' 0 +<# + .SYNOPSIS + Assert if the specific module is available to be imported. + + .DESCRIPTION + Assert if the specific module is available to be imported. + + .PARAMETER ModuleName + Specifies the name of the module to assert. + + .PARAMETER ImportModule + Specfiies to import the module if it is asserted. + + .EXAMPLE + Assert-Module -ModuleName 'DhcpServer' + + This asserts that the module DhcpServer is available on the system. +#> +function Assert-Module +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $ModuleName, + + [Parameter()] + [System.Management.Automation.SwitchParameter] + $ImportModule + ) + + if (-not (Get-Module -Name $ModuleName -ListAvailable)) + { + $errorMessage = $script:localizedData.ModuleNotFound -f $ModuleName + New-ObjectNotFoundException -Message $errorMessage + } + + if ($ImportModule) + { + Import-Module -Name $ModuleName + } +} +#EndRegion './Public/Assert-Module.ps1' 43 +#Region './Public/Compare-ResourcePropertyState.ps1' 0 +<# + .SYNOPSIS + Compare current and desired property values for any DSC resource. + + .DESCRIPTION + This function is used to compare current and desired property values for any + DSC resource, and return a hashtable with the metadata from the comparison. + + .PARAMETER CurrentValues + The current values that should be compared to to desired values. Normally + the values returned from Get-TargetResource. + + .PARAMETER DesiredValues + The values set in the configuration and is provided in the call to the + functions *-TargetResource, and that will be compared against current + values. Normally set to $PSBoundParameters. + + .PARAMETER Properties + An array of property names, from the keys provided in DesiredValues, that + will be compared. If this parameter is left out, all the keys in the + DesiredValues will be compared. + + .PARAMETER IgnoreProperties + An array of property names, from the keys provided in DesiredValues, that + will be ignored in the comparison. If this parameter is left out, all the + keys in the DesiredValues will be compared. + + .PARAMETER CimInstanceKeyProperties + A hashtable containing a key for each property that contain a collection + of CimInstances and the value is an array of strings of the CimInstance + key properties. + @{ + Permission = @('State') + } + + .EXAMPLE + $compareTargetResourceStateParameters = @{ + CurrentValues = (Get-TargetResource $PSBoundParameters) + DesiredValues = $PSBoundParameters + } + + $propertyState = Compare-ResourcePropertyState @compareTargetResourceStateParameters + + This examples call Compare-ResourcePropertyState with the current state + and the desired state and returns a hashtable array of all the properties + that was evaluated based on the properties pass in the parameter DesiredValues. +#> +function Compare-ResourcePropertyState +{ + [CmdletBinding()] + [OutputType([System.Collections.Hashtable[]])] + param + ( + [Parameter(Mandatory = $true)] + [System.Collections.Hashtable] + $CurrentValues, + + [Parameter(Mandatory = $true)] + [System.Collections.Hashtable] + $DesiredValues, + + [Parameter()] + [ValidateNotNullOrEmpty()] + [System.String[]] + $Properties, + + [Parameter()] + [ValidateNotNullOrEmpty()] + [System.String[]] + $IgnoreProperties, + + [Parameter()] + [ValidateNotNull()] + [System.Collections.Hashtable] + $CimInstanceKeyProperties = @{} + ) + + if ($PSBoundParameters.ContainsKey('Properties')) + { + # Filter out the parameters (keys) not specified in Properties + $desiredValuesToRemove = $DesiredValues.Keys | + Where-Object -FilterScript { + $_ -notin $Properties + } + + $desiredValuesToRemove | + ForEach-Object -Process { + $DesiredValues.Remove($_) + } + } + else + { + <# + Remove any common parameters that might be part of DesiredValues, + if it $PSBoundParameters was used to pass the desired values. + #> + $commonParametersToRemove = $DesiredValues.Keys | + Where-Object -FilterScript { + $_ -in [System.Management.Automation.PSCmdlet]::CommonParameters ` + -or $_ -in [System.Management.Automation.PSCmdlet]::OptionalCommonParameters + } + + $commonParametersToRemove | + ForEach-Object -Process { + $DesiredValues.Remove($_) + } + } + + # Remove any properties that should be ignored. + if ($PSBoundParameters.ContainsKey('IgnoreProperties')) + { + $IgnoreProperties | + ForEach-Object -Process { + if ($DesiredValues.ContainsKey($_)) + { + $DesiredValues.Remove($_) + } + } + } + + $compareTargetResourceStateReturnValue = @() + + foreach ($parameterName in $DesiredValues.Keys) + { + Write-Debug -Message ($script:localizedData.EvaluatePropertyState -f $parameterName) + + $parameterState = @{ + ParameterName = $parameterName + Expected = $DesiredValues.$parameterName + Actual = $CurrentValues.$parameterName + } + + # Check if the parameter is in compliance. + $isPropertyInDesiredState = Test-DscPropertyState -Values @{ + CurrentValue = $CurrentValues.$parameterName + DesiredValue = $DesiredValues.$parameterName + KeyProperties = $CimInstanceKeyProperties.$parameterName + } + + if ($isPropertyInDesiredState) + { + Write-Verbose -Message ($script:localizedData.PropertyInDesiredState -f $parameterName) + + $parameterState['InDesiredState'] = $true + } + else + { + Write-Verbose -Message ($script:localizedData.PropertyNotInDesiredState -f $parameterName) + + $parameterState['InDesiredState'] = $false + } + + $compareTargetResourceStateReturnValue += $parameterState + } + + return $compareTargetResourceStateReturnValue +} +#EndRegion './Public/Compare-ResourcePropertyState.ps1' 157 +#Region './Public/ConvertTo-CimInstance.ps1' 0 +<# + .SYNOPSIS + Converts a hashtable into a CimInstance array. + + .DESCRIPTION + This function is used to convert a hashtable into MSFT_KeyValuePair objects. + These are stored as an CimInstance array. DSC cannot handle hashtables but + CimInstances arrays storing MSFT_KeyValuePair. + + .PARAMETER Hashtable + A hashtable with the values to convert. + + .OUTPUTS + An object array with CimInstance objects. + + .EXAMPLE + ConvertTo-CimInstance -Hashtable @{ + String = 'a string' + Bool = $true + Int = 99 + Array = 'a, b, c' + } + + This example returns an CimInstance with the provided hashtable values. +#> +function ConvertTo-CimInstance +{ + [CmdletBinding()] + [OutputType([System.Object[]])] + param + ( + [Parameter(Mandatory = $true, ValueFromPipeline = $true, ParameterSetName = 'Hashtable')] + [System.Collections.Hashtable] + $Hashtable + ) + + process + { + foreach ($item in $Hashtable.GetEnumerator()) + { + New-CimInstance -ClassName 'MSFT_KeyValuePair' -Namespace 'root/microsoft/Windows/DesiredStateConfiguration' -Property @{ + Key = $item.Key + Value = if ($item.Value -is [array]) + { + $item.Value -join ',' + } + else + { + $item.Value + } + } -ClientOnly + } + } +} +#EndRegion './Public/ConvertTo-CimInstance.ps1' 54 +#Region './Public/ConvertTo-HashTable.ps1' 0 +<# + .SYNOPSIS + Converts CimInstances into a hashtable. + + .DESCRIPTION + This function is used to convert a CimInstance array containing + MSFT_KeyValuePair objects into a hashtable. + + .PARAMETER CimInstance + An array of CimInstances or a single CimInstance object to convert. + + .OUTPUTS + Hashtable + + .EXAMPLE + $newInstanceParameters = @{ + ClassName = 'MSFT_KeyValuePair' + Namespace = 'root/microsoft/Windows/DesiredStateConfiguration' + ClientOnly = $true + } + + $cimInstance = [Microsoft.Management.Infrastructure.CimInstance[]] ( + (New-CimInstance @newInstanceParameters -Property @{ + Key = 'FirstName' + Value = 'John' + }), + + (New-CimInstance @newInstanceParameters -Property @{ + Key = 'LastName' + Value = 'Smith' + }) + ) + + ConvertTo-HashTable -CimInstance $cimInstance + + This creates a array om CimInstances of the class name MSFT_KeyValuePair + and passes it to ConvertTo-HashTable which returns a hashtable. +#> +function ConvertTo-HashTable +{ + [CmdletBinding()] + [OutputType([System.Collections.Hashtable])] + param + ( + [Parameter(Mandatory = $true, ValueFromPipeline = $true, ParameterSetName = 'CimInstance')] + [AllowEmptyCollection()] + [Microsoft.Management.Infrastructure.CimInstance[]] + $CimInstance + ) + + begin + { + $result = @{ } + } + + process + { + foreach ($ci in $CimInstance) + { + $result.Add($ci.Key, $ci.Value) + } + } + + end + { + $result + } +} +#EndRegion './Public/ConvertTo-HashTable.ps1' 68 +#Region './Public/Get-LocalizedData.ps1' 0 +<# + .SYNOPSIS + Gets language-specific data into scripts and functions based on the UI culture + that is selected for the operating system. + Similar to Import-LocalizedData, with extra parameter 'DefaultUICulture'. + + .DESCRIPTION + The Get-LocalizedData cmdlet dynamically retrieves strings from a subdirectory + whose name matches the UI language set for the current user of the operating system. + It is designed to enable scripts to display user messages in the UI language selected + by the current user. + + Get-LocalizedData imports data from .psd1 files in language-specific subdirectories + of the script directory and saves them in a local variable that is specified in the + command. The cmdlet selects the subdirectory and file based on the value of the + $PSUICulture automatic variable. When you use the local variable in the script to + display a user message, the message appears in the user's UI language. + + You can use the parameters of G-LocalizedData to specify an alternate UI culture, + path, and file name, to add supported commands, and to suppress the error message that + appears if the .psd1 files are not found. + + The G-LocalizedData cmdlet supports the script internationalization + initiative that was introduced in Windows PowerShell 2.0. This initiative + aims to better serve users worldwide by making it easy for scripts to display + user messages in the UI language of the current user. For more information + about this and about the format of the .psd1 files, see about_Script_Internationalization. + + .PARAMETER BindingVariable + Specifies the variable into which the text strings are imported. Enter a variable + name without a dollar sign ($). + + In Windows PowerShell 2.0, this parameter is required. In Windows PowerShell 3.0, + this parameter is optional. If you omit this parameter, Import-LocalizedData + returns a hash table of the text strings. The hash table is passed down the pipeline + or displayed at the command line. + + When using Import-LocalizedData to replace default text strings specified in the + DATA section of a script, assign the DATA section to a variable and enter the name + of the DATA section variable in the value of the BindingVariable parameter. Then, + when Import-LocalizedData saves the imported content in the BindingVariable, the + imported data will replace the default text strings. If you are not specifying + default text strings, you can select any variable name. + + .PARAMETER UICulture + Specifies an alternate UI culture. The default is the value of the $PsUICulture + automatic variable. Enter a UI culture in - format, such as + en-US, de-DE, or ar-SA. + + The value of the UICulture parameter determines the language-specific subdirectory + (within the base directory) from which Import-LocalizedData gets the .psd1 file + for the script. + + The cmdlet searches for a subdirectory with the same name as the value of the + UICulture parameter or the $PsUICulture automatic variable, such as de-DE or + ar-SA. If it cannot find the directory, or the directory does not contain a .psd1 + file for the script, it searches for a subdirectory with the name of the language + code, such as de or ar. If it cannot find the subdirectory or .psd1 file, the + command fails and the data is displayed in the default language specified in the + script. + + .PARAMETER BaseDirectory + Specifies the base directory where the .psd1 files are located. The default is + the directory where the script is located. Import-LocalizedData searches for + the .psd1 file for the script in a language-specific subdirectory of the base + directory. + + .PARAMETER FileName + Specifies the name of the data file (.psd1) to be imported. Enter a file name. + You can specify a file name that does not include its .psd1 file name extension, + or you can specify the file name including the .psd1 file name extension. + + The FileName parameter is required when Import-LocalizedData is not used in a + script. Otherwise, the parameter is optional and the default value is the base + name of the script. You can use this parameter to direct Import-LocalizedData + to search for a different .psd1 file. + + For example, if the FileName is omitted and the script name is FindFiles.ps1, + Import-LocalizedData searches for the FindFiles.psd1 data file. + + .PARAMETER SupportedCommand + Specifies cmdlets and functions that generate only data. + + Use this parameter to include cmdlets and functions that you have written or + tested. For more information, see about_Script_Internationalization. + + .PARAMETER DefaultUICulture + Specifies which UICulture to default to if current UI culture or its parents + culture don't have matching data file. + + For example, if you have a data file in 'en-US' but not in 'en' or 'en-GB' and + your current culture is 'en-GB', you can default back to 'en-US'. + + .NOTES + Before using Import-LocalizedData, localize your user messages. Format the messages + for each locale (UI culture) in a hash table of key/value pairs, and save the + hash table in a file with the same name as the script and a .psd1 file name extension. + Create a directory under the script directory for each supported UI culture, and + then save the .psd1 file for each UI culture in the directory with the UI + culture name. + + For example, localize your user messages for the de-DE locale and format them in + a hash table. Save the hash table in a .psd1 file. Then create a de-DE + subdirectory under the script directory, and save the de-DE .psd1 + file in the de-DE subdirectory. Repeat this method for each locale that you support. + + Import-LocalizedData performs a structured search for the localized user + messages for a script. + + Import-LocalizedData begins the search in the directory where the script file + is located (or the value of the BaseDirectory parameter). It then searches within + the base directory for a subdirectory with the same name as the value of the + $PsUICulture variable (or the value of the UICulture parameter), such as de-DE or + ar-SA. Then, it searches in that subdirectory for a .psd1 file with the same name + as the script (or the value of the FileName parameter). + + If Import-LocalizedData cannot find a subdirectory with the name of the UI culture, + or the subdirectory does not contain a .psd1 file for the script, it searches for + a .psd1 file for the script in a subdirectory with the name of the language code, + such as de or ar. If it cannot find the subdirectory or .psd1 file, the command + fails, the data is displayed in the default language in the script, and an error + message is displayed explaining that the data could not be imported. To suppress + the message and fail gracefully, use the ErrorAction common parameter with a value + of SilentlyContinue. + + If Import-LocalizedData finds the subdirectory and the .psd1 file, it imports the + hash table of user messages into the value of the BindingVariable parameter in the + command. Then, when you display a message from the hash table in the variable, the + localized message is displayed. + + For more information, see about_Script_Internationalization. + + .EXAMPLE + $script:localizedData = Get-LocalizedData -DefaultUICulture 'en-US' + + This is an example that can be used in DSC resources to import the + localized strings and if the current UI culture localized folder does + not exist the UI culture 'en-US' is returned. +#> +function Get-LocalizedData +{ + [CmdletBinding(DefaultParameterSetName = 'DefaultUICulture')] + param + ( + [Parameter(Position = 0)] + [Alias('Variable')] + [ValidateNotNullOrEmpty()] + [System.String] + $BindingVariable, + + [Parameter(Position = 1, ParameterSetName = 'TargetedUICulture')] + [System.String] + $UICulture, + + [Parameter()] + [System.String] + $BaseDirectory, + + [Parameter()] + [System.String] + $FileName, + + [Parameter()] + [System.String[]] + $SupportedCommand, + + [Parameter(Position = 1, ParameterSetName = 'DefaultUICulture')] + [System.String] + $DefaultUICulture = 'en-US' + ) + + begin + { + <# + Because Proxy Command changes the Invocation origin, we need to be explicit + when handing the pipeline back to original command. + #> + if (!$PSBoundParameters.ContainsKey('FileName')) + { + if ($myInvocation.ScriptName) + { + $file = [System.IO.FileInfo] $myInvocation.ScriptName + } + else + { + $file = [System.IO.FileInfo] $myInvocation.MyCommand.Module.Path + } + + $FileName = $file.BaseName + + $PSBoundParameters.Add('FileName', $file.Name) + } + + if ($PSBoundParameters.ContainsKey('BaseDirectory')) + { + $callingScriptRoot = $BaseDirectory + } + else + { + $callingScriptRoot = $MyInvocation.PSScriptRoot + + $PSBoundParameters.Add('BaseDirectory', $callingScriptRoot) + } + + if ($PSBoundParameters.ContainsKey('DefaultUICulture') -and !$PSBoundParameters.ContainsKey('UICulture')) + { + <# + We don't want the resolution to eventually return the ModuleManifest + so we run the same GetFilePath() logic than here: + https://github.com/PowerShell/PowerShell/blob/master/src/Microsoft.PowerShell.Commands.Utility/commands/utility/Import-LocalizedData.cs#L302-L333 + and if we see it will return the wrong thing, set the UICulture to DefaultUI culture, and return the logic to Import-LocalizedData + #> + $currentCulture = Get-UICulture + + $evaluateDefaultCulture = $true + + <# + If the LCID is 127 then use default UI culture instead. + + See more information in issue https://github.com/dsccommunity/DscResource.Common/issues/11. + #> + if ($currentCulture.LCID -eq 127) + { + $currentCulture = New-Object -TypeName 'System.Globalization.CultureInfo' -ArgumentList @($DefaultUICulture) + $PSBoundParameters['UICulture'] = $DefaultUICulture + + $evaluateDefaultCulture = $false + } + + $languageFile = $null + + $localizedFileNames = @( + $FileName + '.psd1' + $FileName + '.strings.psd1' + ) + + while ($null -ne $currentCulture -and $currentCulture.Name -and -not $languageFile) + { + foreach ($fullFileName in $localizedFileNames) + { + $filePath = [System.IO.Path]::Combine($callingScriptRoot, $CurrentCulture.Name, $fullFileName) + + if (Test-Path -Path $filePath) + { + Write-Debug -Message "Found $filePath" + + $languageFile = $filePath + + # Set the filename to the file we found. + $PSBoundParameters['FileName'] = $fullFileName + + # Exit loop if we find the first filename. + break + } + else + { + Write-Debug -Message "File $filePath not found" + } + } + + if (-not $languageFile) + { + <# + Evaluate the parent culture if there is one. + + If the parent culture is LCID 127 then move to the default culture. + See more information in issue https://github.com/dsccommunity/DscResource.Common/issues/11. + #> + if ($currentCulture.Parent -and $currentCulture.Parent.LCID -ne 127) + { + $currentCulture = $currentCulture.Parent + } + else + { + if ($evaluateDefaultCulture) + { + $evaluateDefaultCulture = $false + + <# + Could not find localized strings file for the the operating + system UI culture. Evaluating the default UI culture (which + defaults to 'en-US' if not specifically set). + #> + $currentCulture = New-Object -TypeName 'System.Globalization.CultureInfo' -ArgumentList @($DefaultUICulture) + $PSBoundParameters['UICulture'] = $DefaultUICulture + } + else + { + <# + Already evaluated everything we could, exit and let + Import-LocalizedData throw an exception. + #> + break + } + } + } + } + + <# + Removes the parameter DefaultUICulture so that isn't used when + calling Import-LocalizedData. + #> + $null = $PSBoundParameters.Remove('DefaultUICulture') + } + + try + { + $outBuffer = $null + + if ($PSBoundParameters.TryGetValue('OutBuffer', [ref] $outBuffer)) + { + $PSBoundParameters['OutBuffer'] = 1 + } + + $wrappedCmd = $ExecutionContext.InvokeCommand.GetCommand('Microsoft.PowerShell.Utility\Import-LocalizedData', [System.Management.Automation.CommandTypes]::Cmdlet) + $scriptCmd = { & $wrappedCmd @PSBoundParameters } + + $steppablePipeline = $scriptCmd.GetSteppablePipeline($myInvocation.CommandOrigin) + $steppablePipeline.Begin($PSCmdlet) + } + catch + { + throw + } + } + + process + { + try + { + $steppablePipeline.Process($_) + } + catch + { + throw + } + } + + end + { + if ($BindingVariable -and ($valueToBind = Get-Variable -Name $BindingVariable -ValueOnly -ErrorAction 'Ignore')) + { + # Bringing the variable to the parent scope + Set-Variable -Scope 1 -Name $BindingVariable -Force -ErrorAction 'SilentlyContinue' -Value $valueToBind + } + + try + { + $steppablePipeline.End() + } + catch + { + throw + } + } +} +#EndRegion './Public/Get-LocalizedData.ps1' 356 +#Region './Public/Get-TemporaryFolder.ps1' 0 +<# + .SYNOPSIS + Returns the path of the current user's temporary folder. + + .DESCRIPTION + Returns the path of the current user's temporary folder. + + .NOTES + This is the same as doing the following + - Windows: $env:TEMP + - macOS: $env:TMPDIR + - Linux: /tmp/ + + .EXAMPLE + Get-TemporaryFolder + + Returns the current user temporary folder on the current operating system. +#> +function Get-TemporaryFolder +{ + [CmdletBinding()] + [OutputType([System.String])] + param () + + return [IO.Path]::GetTempPath() +} +#EndRegion './Public/Get-TemporaryFolder.ps1' 26 +#Region './Public/New-InvalidArgumentException.ps1' 0 +<# + .SYNOPSIS + Creates and throws an invalid argument exception. + + .DESCRIPTION + Creates and throws an invalid argument exception. + + .PARAMETER Message + The message explaining why this error is being thrown. + + .PARAMETER ArgumentName + The name of the invalid argument that is causing this error to be thrown. + + .EXAMPLE + $errorMessage = $script:localizedData.ActionCannotBeUsedInThisContextMessage ` + -f $Action, $Parameter + + New-InvalidArgumentException -ArgumentName 'Action' -Message $errorMessage +#> +function New-InvalidArgumentException +{ + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '')] + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $Message, + + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $ArgumentName + ) + + $argumentException = New-Object -TypeName 'ArgumentException' ` + -ArgumentList @($Message, $ArgumentName) + + $newObjectParameters = @{ + TypeName = 'System.Management.Automation.ErrorRecord' + ArgumentList = @($argumentException, $ArgumentName, 'InvalidArgument', $null) + } + + $errorRecord = New-Object @newObjectParameters + + throw $errorRecord +} +#EndRegion './Public/New-InvalidArgumentException.ps1' 48 +#Region './Public/New-InvalidDataException.ps1' 0 +<# + .SYNOPSIS + Creates and throws an invalid data exception. + + .DESCRIPTION + Creates and throws an invalid data exception. + + .PARAMETER ErrorId + The error Id to assign to the exception. + + .PARAMETER ErrorMessage + The error message to assign to the exception. + + .EXAMPLE + if ( -not $resultOfEvaluation ) + { + $errorMessage = $script:localizedData.InvalidData -f $Action + + New-InvalidDataException -ErrorId 'InvalidDataError' -ErrorMessage $errorMessage + } +#> +function New-InvalidDataException +{ + [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '')] + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $ErrorId, + + [Parameter(Mandatory = $true)] + [System.String] + $ErrorMessage + ) + + $errorCategory = [System.Management.Automation.ErrorCategory]::InvalidData + $exception = New-Object ` + -TypeName System.InvalidOperationException ` + -ArgumentList $ErrorMessage + $errorRecord = New-Object ` + -TypeName System.Management.Automation.ErrorRecord ` + -ArgumentList $exception, $ErrorId, $errorCategory, $null + + throw $errorRecord +} +#EndRegion './Public/New-InvalidDataException.ps1' 46 +#Region './Public/New-InvalidOperationException.ps1' 0 +<# + .SYNOPSIS + Creates and throws an invalid operation exception. + + .DESCRIPTION + Creates and throws an invalid operation exception. + + .PARAMETER Message + The message explaining why this error is being thrown. + + .PARAMETER ErrorRecord + The error record containing the exception that is causing this terminating error. + + .EXAMPLE + try + { + Start-Process @startProcessArguments + } + catch + { + $errorMessage = $script:localizedData.InstallationFailedMessage -f $Path, $processId + New-InvalidOperationException -Message $errorMessage -ErrorRecord $_ + } +#> +function New-InvalidOperationException +{ + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '')] + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $Message, + + [Parameter()] + [ValidateNotNull()] + [System.Management.Automation.ErrorRecord] + $ErrorRecord + ) + + if ($null -eq $ErrorRecord) + { + $invalidOperationException = New-Object -TypeName 'InvalidOperationException' ` + -ArgumentList @($Message) + } + else + { + $invalidOperationException = New-Object -TypeName 'InvalidOperationException' ` + -ArgumentList @($Message, $ErrorRecord.Exception) + } + + $newObjectParameters = @{ + TypeName = 'System.Management.Automation.ErrorRecord' + ArgumentList = @( + $invalidOperationException.ToString(), + 'MachineStateIncorrect', + 'InvalidOperation', + $null + ) + } + + $errorRecordToThrow = New-Object @newObjectParameters + + throw $errorRecordToThrow +} +#EndRegion './Public/New-InvalidOperationException.ps1' 66 +#Region './Public/New-InvalidResultException.ps1' 0 +<# + .SYNOPSIS + Creates and throws an invalid result exception. + + .DESCRIPTION + Creates and throws an invalid result exception. + + .PARAMETER Message + The message explaining why this error is being thrown. + + .PARAMETER ErrorRecord + The error record containing the exception that is causing this terminating error. + + .EXAMPLE + try + { + $numberOfObjects = Get-ChildItem -Path $path + if ($numberOfObjects -eq 0) + { + throw 'To few files.' + } + } + catch + { + $errorMessage = $script:localizedData.TooFewFilesMessage -f $path + New-InvalidResultException -Message $errorMessage -ErrorRecord $_ + } +#> +function New-InvalidResultException +{ + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '')] + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $Message, + + [Parameter()] + [ValidateNotNull()] + [System.Management.Automation.ErrorRecord] + $ErrorRecord + ) + + if ($null -eq $ErrorRecord) + { + $exception = New-Object -TypeName 'System.Exception' ` + -ArgumentList @($Message) + } + else + { + $exception = New-Object -TypeName 'System.Exception' ` + -ArgumentList @($Message, $ErrorRecord.Exception) + } + + $newObjectParameters = @{ + TypeName = 'System.Management.Automation.ErrorRecord' + ArgumentList = @( + $exception.ToString(), + 'MachineStateIncorrect', + 'InvalidResult', + $null + ) + } + + $errorRecordToThrow = New-Object @newObjectParameters + + throw $errorRecordToThrow +} +#EndRegion './Public/New-InvalidResultException.ps1' 70 +#Region './Public/New-NotImplementedException.ps1' 0 +<# + .SYNOPSIS + Creates and throws an not implemented exception. + + .DESCRIPTION + Creates and throws an not implemented exception. + + .PARAMETER Message + The message explaining why this error is being thrown. + + .PARAMETER ErrorRecord + The error record containing the exception that is causing this terminating error. + + .EXAMPLE + if ($runFeature) + { + $errorMessage = $script:localizedData.FeatureMissing -f $path + New-NotImplementedException -Message $errorMessage -ErrorRecord $_ + } + + Throws an not implemented exception if the variable $runFeature contains + a value. +#> +function New-NotImplementedException +{ + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '')] + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $Message, + + [Parameter()] + [ValidateNotNull()] + [System.Management.Automation.ErrorRecord] + $ErrorRecord + ) + + if ($null -eq $ErrorRecord) + { + $invalidOperationException = New-Object -TypeName 'NotImplementedException' ` + -ArgumentList @($Message) + } + else + { + $invalidOperationException = New-Object -TypeName 'NotImplementedException' ` + -ArgumentList @($Message, $ErrorRecord.Exception) + } + + $newObjectParameters = @{ + TypeName = 'System.Management.Automation.ErrorRecord' + ArgumentList = @( + $invalidOperationException.ToString(), + 'MachineStateIncorrect', + 'NotImplemented', + $null + ) + } + + $errorRecordToThrow = New-Object @newObjectParameters + + throw $errorRecordToThrow +} +#EndRegion './Public/New-NotImplementedException.ps1' 65 +#Region './Public/New-ObjectNotFoundException.ps1' 0 + +<# + .SYNOPSIS + Creates and throws an object not found exception. + + .DESCRIPTION + Creates and throws an object not found exception. + + .PARAMETER Message + The message explaining why this error is being thrown. + + .PARAMETER ErrorRecord + The error record containing the exception that is causing this terminating error. + + .EXAMPLE + try + { + Get-ChildItem -Path $path + } + catch + { + $errorMessage = $script:localizedData.PathNotFoundMessage -f $path + New-ObjectNotFoundException -Message $errorMessage -ErrorRecord $_ + } +#> +function New-ObjectNotFoundException +{ + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '')] + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $Message, + + [Parameter()] + [ValidateNotNull()] + [System.Management.Automation.ErrorRecord] + $ErrorRecord + ) + + if ($null -eq $ErrorRecord) + { + $exception = New-Object -TypeName 'System.Exception' ` + -ArgumentList @($Message) + } + else + { + $exception = New-Object -TypeName 'System.Exception' ` + -ArgumentList @($Message, $ErrorRecord.Exception) + } + + $newObjectParameters = @{ + TypeName = 'System.Management.Automation.ErrorRecord' + ArgumentList = @( + $exception.ToString(), + 'MachineStateIncorrect', + 'ObjectNotFound', + $null + ) + } + + $errorRecordToThrow = New-Object @newObjectParameters + + throw $errorRecordToThrow +} +#EndRegion './Public/New-ObjectNotFoundException.ps1' 67 +#Region './Public/Remove-CommonParameter.ps1' 0 +<# + .SYNOPSIS + Removes common parameters from a hashtable. + + .DESCRIPTION + This function serves the purpose of removing common parameters and option + common parameters from a parameter hashtable. + + .PARAMETER Hashtable + The parameter hashtable that should be pruned. + + .EXAMPLE + Remove-CommonParameter -Hashtable $PSBoundParameters + + Returns a new hashtable without the common and optional common parameters. +#> +function Remove-CommonParameter +{ + [Diagnostics.CodeAnalysis.SuppressMessageAttribute( + 'PSUseShouldProcessForStateChangingFunctions', + '', + Justification = 'ShouldProcess is not supported in DSC resources.' + )] + [OutputType([System.Collections.Hashtable])] + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [System.Collections.Hashtable] + $Hashtable + ) + + $inputClone = $Hashtable.Clone() + + $commonParameters = [System.Management.Automation.PSCmdlet]::CommonParameters + $commonParameters += [System.Management.Automation.PSCmdlet]::OptionalCommonParameters + + $Hashtable.Keys | Where-Object -FilterScript { + $_ -in $commonParameters + } | ForEach-Object -Process { + $inputClone.Remove($_) + } + + return $inputClone +} +#EndRegion './Public/Remove-CommonParameter.ps1' 45 +#Region './Public/Set-DscMachineRebootRequired.ps1' 0 +<# + .SYNOPSIS + Set the DSC reboot required status variable. + + .DESCRIPTION + Sets the global DSCMachineStatus variable to a value of 1. + This function is used to set the global variable that indicates + to the LCM that a reboot of the node is required. + + .EXAMPLE + PS C:\> Set-DscMachineRebootRequired + + Sets the $global:DSCMachineStatus variable to 1. + + .NOTES + This function is implemented so that individual resource modules + do not need to use and therefore suppress Global variables + directly. It also enables mocking to increase testability of + consumers. +#> +function Set-DscMachineRebootRequired +{ + [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '')] + # Suppressing this rule because $global:DSCMachineStatus is used to trigger a reboot. + [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidGlobalVars', '')] + <# + Suppressing this rule because $global:DSCMachineStatus is only set, + never used (by design of Desired State Configuration). + #> + [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', '')] + [CmdletBinding()] + param + ( + ) + + $global:DSCMachineStatus = 1 +} +#EndRegion './Public/Set-DscMachineRebootRequired.ps1' 37 +#Region './Public/Set-PSModulePath.ps1' 0 + +<# + .SYNOPSIS + Set environment variable PSModulePath in the current session or machine + wide. + + .DESCRIPTION + This is a wrapper to set environment variable PSModulePath in current + session or machine wide. + + .PARAMETER Path + A string with all the paths separated by semi-colons. + + .PARAMETER Machine + If set the PSModulePath will be changed machine wide. If not set, only + the current session will be changed. + + .EXAMPLE + Set-PSModulePath -Path ';' + + .EXAMPLE + Set-PSModulePath -Path ';' -Machine +#> +function Set-PSModulePath +{ + [Diagnostics.CodeAnalysis.SuppressMessageAttribute( + 'PSUseShouldProcessForStateChangingFunctions', + '', + Justification = 'ShouldProcess is not supported in DSC resources.' + )] + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $Path, + + [Parameter()] + [System.Management.Automation.SwitchParameter] + $Machine + ) + + if ($Machine.IsPresent) + { + [System.Environment]::SetEnvironmentVariable('PSModulePath', $Path, [System.EnvironmentVariableTarget]::Machine) + } + else + { + $env:PSModulePath = $Path + } +} +#EndRegion './Public/Set-PSModulePath.ps1' 52 +#Region './Public/Test-DscParameterState.ps1' 0 +<# + .SYNOPSIS + This method is used to compare current and desired values for any DSC resource. + + .DESCRIPTION + This function tests the parameter status of DSC resource parameters against + the current values present on the system. + + .PARAMETER CurrentValues + A hashtable with the current values on the system, obtained by e.g. + Get-TargetResource. + + .PARAMETER DesiredValues + The hashtable of desired values. For example $PSBoundParameters with the + desired values. + + .PARAMETER Properties + This is a list of properties in the desired values list should be checked. + If this is empty then all values in DesiredValues are checked. + + .PARAMETER ExcludeProperties + This is a list of which properties in the desired values list should be checked. + If this is empty then all values in DesiredValues are checked. + + .PARAMETER TurnOffTypeChecking + Indicates that the type of the parameter should not be checked. + + .PARAMETER ReverseCheck + Indicates that a reverse check should be done. The current and desired state + are swapped for another test. + + .PARAMETER SortArrayValues + If the sorting of array values does not matter, values are sorted internally + before doing the comparison. + + .EXAMPLE + $currentState = Get-TargetResource @PSBoundParameters + + $returnValue = Test-DscParameterState -CurrentValues $currentState -DesiredValues $PSBoundParameters + + The function Get-TargetResource is called first using all bound parameters + to get the values in the current state. The result is then compared to the + desired state by calling `Test-DscParameterState`. + + .EXAMPLE + $getTargetResourceParameters = @{ + ServerName = $ServerName + InstanceName = $InstanceName + Name = $Name + } + + $returnValue = Test-DscParameterState ` + -CurrentValues (Get-TargetResource @getTargetResourceParameters) ` + -DesiredValues $PSBoundParameters ` + -ExcludeProperties @( + 'FailsafeOperator' + 'NotificationMethod' + ) + + This compares the values in the current state against the desires state. + The function Get-TargetResource is called using just the required parameters + to get the values in the current state. The parameter 'ExcludeProperties' + is used to exclude the properties 'FailsafeOperator' and + 'NotificationMethod' from the comparison. + + .EXAMPLE + $getTargetResourceParameters = @{ + ServerName = $ServerName + InstanceName = $InstanceName + Name = $Name + } + + $returnValue = Test-DscParameterState ` + -CurrentValues (Get-TargetResource @getTargetResourceParameters) ` + -DesiredValues $PSBoundParameters ` + -Properties ServerName, Name + + This compares the values in the current state against the desires state. + The function Get-TargetResource is called using just the required parameters + to get the values in the current state. The 'Properties' parameter is used + to to only compare the properties 'ServerName' and 'Name'. +#> +function Test-DscParameterState +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [System.Object] + $CurrentValues, + + [Parameter(Mandatory = $true)] + [System.Object] + $DesiredValues, + + [Parameter()] + [System.String[]] + [Alias('ValuesToCheck')] + $Properties, + + [Parameter()] + [System.String[]] + $ExcludeProperties, + + [Parameter()] + [System.Management.Automation.SwitchParameter] + $TurnOffTypeChecking, + + [Parameter()] + [System.Management.Automation.SwitchParameter] + $ReverseCheck, + + [Parameter()] + [System.Management.Automation.SwitchParameter] + $SortArrayValues + ) + + $returnValue = $true + + if ($CurrentValues -is [Microsoft.Management.Infrastructure.CimInstance] -or + $CurrentValues -is [Microsoft.Management.Infrastructure.CimInstance[]]) + { + $CurrentValues = ConvertTo-HashTable -CimInstance $CurrentValues + } + + if ($DesiredValues -is [Microsoft.Management.Infrastructure.CimInstance] -or + $DesiredValues -is [Microsoft.Management.Infrastructure.CimInstance[]]) + { + $DesiredValues = ConvertTo-HashTable -CimInstance $DesiredValues + } + + $types = 'System.Management.Automation.PSBoundParametersDictionary', 'System.Collections.Hashtable', 'Microsoft.Management.Infrastructure.CimInstance' + + if ($DesiredValues.GetType().FullName -notin $types) + { + New-InvalidArgumentException ` + -Message ($script:localizedData.InvalidDesiredValuesError -f $DesiredValues.GetType().FullName) ` + -ArgumentName 'DesiredValues' + } + + if ($CurrentValues.GetType().FullName -notin $types) + { + New-InvalidArgumentException ` + -Message ($script:localizedData.InvalidCurrentValuesError -f $CurrentValues.GetType().FullName) ` + -ArgumentName 'CurrentValues' + } + + if ($DesiredValues -is [Microsoft.Management.Infrastructure.CimInstance] -and -not $Properties) + { + New-InvalidArgumentException ` + -Message $script:localizedData.InvalidPropertiesError ` + -ArgumentName Properties + } + + $desiredValuesClean = Remove-CommonParameter -Hashtable $DesiredValues + + if (-not $Properties) + { + $keyList = $desiredValuesClean.Keys + } + else + { + $keyList = $Properties + } + if ($ExcludeProperties) + { + $keyList = $keyList | Where-Object -FilterScript { $_ -notin $ExcludeProperties } + } + + foreach ($key in $keyList) + { + $desiredValue = $desiredValuesClean.$key + $currentValue = $CurrentValues.$key + + if ($desiredValue -is [Microsoft.Management.Infrastructure.CimInstance] -or + $desiredValue -is [Microsoft.Management.Infrastructure.CimInstance[]]) + { + $desiredValue = ConvertTo-HashTable -CimInstance $desiredValue + } + if ($currentValue -is [Microsoft.Management.Infrastructure.CimInstance] -or + $currentValue -is [Microsoft.Management.Infrastructure.CimInstance[]]) + { + $currentValue = ConvertTo-HashTable -CimInstance $currentValue + } + + if ($null -ne $desiredValue) + { + $desiredType = $desiredValue.GetType() + } + else + { + $desiredType = @{ + Name = 'Unknown' + } + } + + if ($null -ne $currentValue) + { + $currentType = $currentValue.GetType() + } + else + { + $currentType = @{ + Name = 'Unknown' + } + } + + if ($currentType.Name -ne 'Unknown' -and $desiredType.Name -eq 'PSCredential') + { + # This is a credential object. Compare only the user name + if ($currentType.Name -eq 'PSCredential' -and $currentValue.UserName -eq $desiredValue.UserName) + { + Write-Verbose -Message ($script:localizedData.MatchPsCredentialUsernameMessage -f $currentValue.UserName, $desiredValue.UserName) + continue + } + else + { + Write-Verbose -Message ($script:localizedData.NoMatchPsCredentialUsernameMessage -f $currentValue.UserName, $desiredValue.UserName) + $returnValue = $false + } + + # Assume the string is our username when the matching desired value is actually a credential + if ($currentType.Name -eq 'string' -and $currentValue -eq $desiredValue.UserName) + { + Write-Verbose -Message ($script:localizedData.MatchPsCredentialUsernameMessage -f $currentValue, $desiredValue.UserName) + continue + } + else + { + Write-Verbose -Message ($script:localizedData.NoMatchPsCredentialUsernameMessage -f $currentValue, $desiredValue.UserName) + $returnValue = $false + } + } + + if (-not $TurnOffTypeChecking) + { + if (($desiredType.Name -ne 'Unknown' -and $currentType.Name -ne 'Unknown') -and + $desiredType.FullName -ne $currentType.FullName) + { + Write-Verbose -Message ($script:localizedData.NoMatchTypeMismatchMessage -f $key, $currentType.FullName, $desiredType.FullName) + $returnValue = $false + continue + } + } + + if ($currentValue -eq $desiredValue -and -not $desiredType.IsArray) + { + Write-Verbose -Message ($script:localizedData.MatchValueMessage -f $desiredType.FullName, $key, $currentValue, $desiredValue) + continue + } + + if ($desiredValuesClean.GetType().Name -in 'HashTable', 'PSBoundParametersDictionary') + { + $checkDesiredValue = $desiredValuesClean.ContainsKey($key) + } + else + { + $checkDesiredValue = Test-DscObjectHasProperty -Object $desiredValuesClean -PropertyName $key + } + + if (-not $checkDesiredValue) + { + Write-Verbose -Message ($script:localizedData.MatchValueMessage -f $desiredType.FullName, $key, $currentValue, $desiredValue) + continue + } + + if ($desiredType.IsArray) + { + Write-Verbose -Message ($script:localizedData.TestDscParameterCompareMessage -f $key, $desiredType.FullName) + + if (-not $currentValue -and -not $desiredValue) + { + Write-Verbose -Message ($script:localizedData.MatchValueMessage -f $desiredType.FullName, $key, 'empty array', 'empty array') + continue + } + elseif (-not $currentValue) + { + Write-Verbose -Message ($script:localizedData.NoMatchValueMessage -f $desiredType.FullName, $key, $currentValue, $desiredValue) + $returnValue = $false + continue + } + elseif ($currentValue.Count -ne $desiredValue.Count) + { + Write-Verbose -Message ($script:localizedData.NoMatchValueDifferentCountMessage -f $desiredType.FullName, $key, $currentValue.Count, $desiredValue.Count) + $returnValue = $false + continue + } + else + { + $desiredArrayValues = $desiredValue + $currentArrayValues = $currentValue + + if ($SortArrayValues) + { + $desiredArrayValues = @($desiredArrayValues | Sort-Object) + $currentArrayValues = @($currentArrayValues | Sort-Object) + } + + for ($i = 0; $i -lt $desiredArrayValues.Count; $i++) + { + if ($desiredArrayValues[$i]) + { + $desiredType = $desiredArrayValues[$i].GetType() + } + else + { + $desiredType = @{ + Name = 'Unknown' + } + } + + if ($currentArrayValues[$i]) + { + $currentType = $currentArrayValues[$i].GetType() + } + else + { + $currentType = @{ + Name = 'Unknown' + } + } + + if (-not $TurnOffTypeChecking) + { + if (($desiredType.Name -ne 'Unknown' -and $currentType.Name -ne 'Unknown') -and + $desiredType.FullName -ne $currentType.FullName) + { + Write-Verbose -Message ($script:localizedData.NoMatchElementTypeMismatchMessage -f $key, $i, $currentType.FullName, $desiredType.FullName) + $returnValue = $false + continue + } + } + + #Convert a scriptblock into a string as scriptblocks are not comparable + $wasCurrentArrayValuesConverted = $false + if ($currentArrayValues[$i] -is [scriptblock]) + { + $currentArrayValues[$i] = if ($desiredArrayValues[$i] -is [string]) + { + $currentArrayValues[$i] = $currentArrayValues[$i].Invoke() + } + else + { + $currentArrayValues[$i].ToString() + } + $wasCurrentArrayValuesConverted = $true + } + if ($desiredArrayValues[$i] -is [scriptblock]) + { + $desiredArrayValues[$i] = if ($currentArrayValues[$i] -is [string] -and -not $wasCurrentArrayValuesConverted) + { + $desiredArrayValues[$i].Invoke() + } + else + { + $desiredArrayValues[$i].ToString() + } + } + + if ($desiredType -eq [System.Collections.Hashtable] -and $currentType -eq [System.Collections.Hashtable]) + { + $param = $PSBoundParameters + $param.CurrentValues = $currentArrayValues[$i] + $param.DesiredValues = $desiredArrayValues[$i] + + if ($returnValue) + { + $returnValue = Test-DscParameterState @param + } + else + { + Test-DscParameterState @param | Out-Null + } + continue + } + + if ($desiredArrayValues[$i] -ne $currentArrayValues[$i]) + { + Write-Verbose -Message ($script:localizedData.NoMatchElementValueMismatchMessage -f $i, $desiredType.FullName, $key, $currentArrayValues[$i], $desiredArrayValues[$i]) + $returnValue = $false + continue + } + else + { + Write-Verbose -Message ($script:localizedData.MatchElementValueMessage -f $i, $desiredType.FullName, $key, $currentArrayValues[$i], $desiredArrayValues[$i]) + continue + } + } + + } + } + elseif ($desiredType -eq [System.Collections.Hashtable] -and $currentType -eq [System.Collections.Hashtable]) + { + $param = $PSBoundParameters + $param.CurrentValues = $currentValue + $param.DesiredValues = $desiredValue + + if ($returnValue) + { + $returnValue = Test-DscParameterState @param + } + else + { + Test-DscParameterState @param | Out-Null + } + continue + } + else + { + #Convert a scriptblock into a string as scriptblocks are not comparable + $wasCurrentValue = $false + if ($currentValue -is [scriptblock]) + { + $currentValue = if ($desiredValue -is [string]) + { + $currentValue = $currentValue.Invoke() + } + else + { + $currentValue.ToString() + } + $wasCurrentValue = $true + } + if ($desiredValue -is [scriptblock]) + { + $desiredValue = if ($currentValue -is [string] -and -not $wasCurrentValue) + { + $desiredValue.Invoke() + } + else + { + $desiredValue.ToString() + } + } + + if ($desiredValue -ne $currentValue) + { + Write-Verbose -Message ($script:localizedData.NoMatchValueMessage -f $desiredType.FullName, $key, $currentValue, $desiredValue) + $returnValue = $false + } + } + } + + if ($ReverseCheck) + { + Write-Verbose -Message $script:localizedData.StartingReverseCheck + $reverseCheckParameters = $PSBoundParameters + $reverseCheckParameters.CurrentValues = $DesiredValues + $reverseCheckParameters.DesiredValues = $CurrentValues + $null = $reverseCheckParameters.Remove('ReverseCheck') + + if ($returnValue) + { + $returnValue = Test-DscParameterState @reverseCheckParameters + } + else + { + $null = Test-DscParameterState @reverseCheckParameters + } + } + + Write-Verbose -Message ($script:localizedData.TestDscParameterResultMessage -f $returnValue) + return $returnValue +} +#EndRegion './Public/Test-DscParameterState.ps1' 464 +#Region './Public/Test-IsNanoServer.ps1' 0 +<# + .SYNOPSIS + Tests if the current OS is a Nano server. + + .DESCRIPTION + Tests if the current OS is a Nano server. + + .EXAMPLE + Test-IsNanoServer + + Returns $true if the current operating system is Nano Server, if not $false + is returned. +#> +function Test-IsNanoServer +{ + [OutputType([System.Boolean])] + [CmdletBinding()] + param () + + $productDatacenterNanoServer = 143 + $productStandardNanoServer = 144 + + $operatingSystemSKU = (Get-CimInstance -ClassName Win32_OperatingSystem).OperatingSystemSKU + + Write-Verbose -Message ($script:localizedData.TestIsNanoServerOperatingSystemSku -f $operatingSystemSKU) + + return ($operatingSystemSKU -in ($productDatacenterNanoServer, $productStandardNanoServer)) +} +#EndRegion './Public/Test-IsNanoServer.ps1' 28 +#Region './suffix.ps1' 0 +$script:localizedData = Get-LocalizedData -DefaultUICulture 'en-US' +#EndRegion './suffix.ps1' 1 diff --git a/deployment/dsc/azshcihost/ComputerManagementDsc/8.4.0/Modules/DscResource.Common/0.9.3/en-US/DscResource.Common.strings.psd1 b/deployment/dsc/azshcihost/ComputerManagementDsc/8.4.0/Modules/DscResource.Common/0.9.3/en-US/DscResource.Common.strings.psd1 new file mode 100644 index 0000000..fd8440b --- /dev/null +++ b/deployment/dsc/azshcihost/ComputerManagementDsc/8.4.0/Modules/DscResource.Common/0.9.3/en-US/DscResource.Common.strings.psd1 @@ -0,0 +1,37 @@ +# Localized English (en-US) strings. + +ConvertFrom-StringData @' + TestIsNanoServerOperatingSystemSku = OperatingSystemSKU {0} was returned by Win32_OperatingSystem when detecting if operating system is Nano Server. (DRC0008) + ModuleNotFound = Please ensure that the PowerShell module '{0}' is installed. (DRC0009) + ParameterUsageWrong = None of the parameter(s) '{0}' may be used at the same time as any of the parameter(s) '{1}'. (DRC0010) + AddressFormatError = Address '{0}' is not in the correct format. Please correct the Address parameter in the configuration and try again. (DRC0011) + AddressIPv4MismatchError = Address '{0}' is in IPv4 format, which does not match server address family {1}. Please correct either of them in the configuration and try again. (DRC0012) + AddressIPv6MismatchError = Address '{0}' is in IPv6 format, which does not match server address family {1}. Please correct either of them in the configuration and try again. (DRC0013) + InvalidDesiredValuesError = Property 'DesiredValues' in Test-DscParameterState must be either a Hashtable or CimInstance. Type detected was '{0}'. (DRC0014) + InvalidCurrentValuesError = Property 'CurrentValues' in Test-DscParameterState must be either a Hashtable, CimInstance, or CimIntance[]. Type detected was '{0}'. (DRC0015) + InvalidPropertiesError = If 'DesiredValues' is a CimInstance then property 'Properties' must contain a value. (DRC0016) + MatchPsCredentialUsernameMessage = MATCH: PSCredential username match. Current state is '{0}' and desired state is '{1}'. (DRC0017) + NoMatchPsCredentialUsernameMessage = NOTMATCH: PSCredential username mismatch. Current state is '{0}' and desired state is '{1}'. (DRC0018) + NoMatchTypeMismatchMessage = NOTMATCH: Type mismatch for property '{0}' Current state type is '{1}' and desired type is '{2}'. (DRC0019) + MatchValueMessage = MATCH: Value (type '{0}') for property '{1}' does match. Current state is '{2}' and desired state is '{3}'. (DRC0020) + NoMatchValueMessage = NOTMATCH: Value (type '{0}') for property '{1}' does not match. Current state is '{2}' and desired state is '{3}'. (DRC0021) + NoMatchValueDifferentCountMessage = NOTMATCH: Value (type '{0}') for property '{1}' does have a different count. Current state count is '{2}' and desired state count is '{3}'. (DRC0022) + NoMatchElementTypeMismatchMessage = NOTMATCH: Type mismatch for property '{0}' Current state type of element [{1}] is '{2}' and desired type is '{3}'. (DRC0023) + NoMatchElementValueMismatchMessage = NOTMATCH: Value [{0}] (type '{1}') for property '{2}' does match. Current state is '{3}' and desired state is '{4}'. (DRC0024) + MatchElementValueMessage = MATCH: Value [{0}] (type '{1}') for property '{2}' does match. Current state is '{3}' and desired state is '{4}'. (DRC0025) + TestDscParameterResultMessage = Test-DscParameter result is '{0}'. (DRC0026) + StartingReverseCheck = Starting with a reverse check. (DRC0027) + TestDscParameterCompareMessage = Comparing values in property '{0}'. (DRC0028) + TooManyCimInstances = More than one CIM instance was returned from the current state. (DRC0029) + TestingCimInstance = Testing CIM instance '{0}' with the key properties '{1}'. (DRC0030) + MissingCimInstance = The CIM instance '{0}' with the key properties '{1}' is missing. (DRC0031) + ArrayValueIsAbsent = The array value '{0}' is absent. (DRC0032) + ArrayValueIsPresent = The array value '{0}' is present. (DRC0033) + KeyPropertiesMissing = The hashtable passed to function Test-DscPropertyState is missing the key 'KeyProperties'. This must be set to the property names that makes each instance in the CIM instance collection unique. (DRC0034) + ArrayDoesNotMatch = One or more values in an array does not match the desired state. Details of the changes are below. (DRC0035) + PropertyValueOfTypeDoesNotMatch = {0} value does not match. Current value is '{1}', but expected the value '{2}'. (DRC0036) + UnableToCompareType = Unable to compare the type {0} as it is not handled by the Test-DscPropertyState cmdlet. (DRC0037) + EvaluatePropertyState = Evaluating the state of the property '{0}'. (DRC0038) + PropertyInDesiredState = The parameter '{0}' is in desired state. (DRC0039) + PropertyNotInDesiredState = The parameter '{0}' is not in desired state. (DRC0040) +'@ diff --git a/deployment/dsc/azshcihost/ComputerManagementDsc/8.4.0/Modules/DscResource.Common/0.9.3/en-US/about_DscResource.Common.help.txt b/deployment/dsc/azshcihost/ComputerManagementDsc/8.4.0/Modules/DscResource.Common/0.9.3/en-US/about_DscResource.Common.help.txt new file mode 100644 index 0000000..2a99677 --- /dev/null +++ b/deployment/dsc/azshcihost/ComputerManagementDsc/8.4.0/Modules/DscResource.Common/0.9.3/en-US/about_DscResource.Common.help.txt @@ -0,0 +1,26 @@ +TOPIC + about_DscResource.Common + +SHORT DESCRIPTION + Common functions used in DSC tesources. + +LONG DESCRIPTION + This module contains common functions that are used in DSC resources. + +EXAMPLES + PS C:\> Get-Command -Module DscResource.Common + +NOTE: + Thank you to the DSC Community contributors who contributed to this module by + writing code, sharing opinions, and provided feedback. + +TROUBLESHOOTING NOTE: + Go to the Github repository for read about issues, submit a new issue, and read + about new releases. https://github.com/dsccommunity/DscResource.Common + +SEE ALSO + - https://github.com/dsccommunity/DscResource.Common + +KEYWORDS + DSC, Localization + diff --git a/deployment/dsc/azshcihost/ComputerManagementDsc/8.4.0/en-US/about_ComputerManagementDsc.help.txt b/deployment/dsc/azshcihost/ComputerManagementDsc/8.4.0/en-US/about_ComputerManagementDsc.help.txt new file mode 100644 index 0000000..71a4fcb --- /dev/null +++ b/deployment/dsc/azshcihost/ComputerManagementDsc/8.4.0/en-US/about_ComputerManagementDsc.help.txt @@ -0,0 +1,34 @@ +TOPIC + about_ComputerManagementDsc + +SHORT DESCRIPTION + DSC resources for configuration of a Windows computer. These DSC resources + allow you to perform computer management tasks, such as renaming the computer, + joining a domain and scheduling tasks as well as configuring items such as + virtual memory, event logs, time zones and power settings. + +LONG DESCRIPTION + This module contains DSC resources for configuration of a Windows computer. + These DSC resources allow you to perform computer management tasks, such as + renaming the computer, joining a domain and scheduling tasks as well as + configuring items such as virtual memory, event logs, time zones and power + settings. + +EXAMPLES + PS C:\> Get-DscResource -Module ComputerManagementDsc + +NOTE: + Thank you to the DSC Community contributors who contributed to this module by + writing code, sharing opinions, and provided feedback. + +TROUBLESHOOTING NOTE: + Go to the Github repository for read about issues, submit a new issue, and read + about new releases. https://github.com/dsccommunity/ComputerManagementDsc + +SEE ALSO + - https://github.com/dsccommunity/ComputerManagementDsc + +KEYWORDS + DSC, DscResource, Computer, OfflineDomainJoin, PendingReboot, PowerPlan, + PowerShellExecutionPolicy, RemoteDesktopAdmin, ScheduledTask, SmbServerConfiguration + SmbShare, SystemLocale, TimeZone, VirtualMemory, WindowsEventLog, WindowsCapability diff --git a/deployment/dsc/azshcihost/DSCR_Shortcut/2.1.1/DSCR_Shortcut.psd1 b/deployment/dsc/azshcihost/DSCR_Shortcut/2.1.1/DSCR_Shortcut.psd1 new file mode 100644 index 0000000..b270a53 --- /dev/null +++ b/deployment/dsc/azshcihost/DSCR_Shortcut/2.1.1/DSCR_Shortcut.psd1 @@ -0,0 +1,83 @@ +# +# モジュール 'DSCR_Shortcut' のモジュール マニフェスト +# +# 生成者: mkht +# +# 生成日: 2016/06/18 +# + +@{ + + # このマニフェストに関連付けられているスクリプト モジュール ファイルまたはバイナリ モジュール ファイル。 + # RootModule = '' + + # このモジュールのバージョン番号です。 + ModuleVersion = '2.1.1' + + # このモジュールを一意に識別するために使用される ID + GUID = 'dc24c0c9-ad6b-4fce-9ce4-2410f9ce4f7f' + + # このモジュールの作成者 + Author = 'mkht' + + # このモジュールの会社またはベンダー + CompanyName = '' + + # このモジュールの著作権情報 + Copyright = '(c) 2020 mkht. All rights reserved.' + + # このモジュールの機能の説明 + Description = 'PowerShell DSC Resource to create shortcut file.' + + # このモジュールに必要な Windows PowerShell エンジンの最小バージョン + PowerShellVersion = '5.0' + + # Minimum version of the common language runtime (CLR) required by this module. This prerequisite is valid for the PowerShell Desktop edition only. + # CLRVersion = '' + + # RootModule/ModuleToProcess に指定されているモジュールの入れ子になったモジュールとしてインポートするモジュール + # NestedModules = @() + + # このモジュールからエクスポートする関数です。最適なパフォーマンスを得るには、ワイルドカードを使用せず、エクスポートする関数がない場合は、エントリを削除しないで空の配列を使用してください。 + FunctionsToExport = @() + + # このモジュールからエクスポートするコマンドレットです。最適なパフォーマンスを得るには、ワイルドカードを使用せず、エクスポートするコマンドレットがない場合は、エントリを削除しないで空の配列を使用してください。 + CmdletsToExport = @() + + # このモジュールからエクスポートする変数 + VariablesToExport = '*' + + # このモジュールからエクスポートするエイリアスです。最適なパフォーマンスを得るには、ワイルドカードを使用せず、エクスポートするエイリアスがない場合は、エントリを削除しないで空の配列を使用してください。 + AliasesToExport = @() + + # このモジュールからエクスポートする DSC リソース + DscResourcesToExport = @('cShortcut') + + # このモジュールからエクスポートされたコマンドの既定のプレフィックス。既定のプレフィックスをオーバーライドする場合は、Import-Module -Prefix を使用します。 + # DefaultCommandPrefix = '' + + # RootModule/ModuleToProcess に指定されているモジュールに渡すプライベート データ。これには、PowerShell で使用される追加のモジュール メタデータを含む PSData ハッシュテーブルが含まれる場合もあります。 + PrivateData = @{ + + PSData = @{ + + # このモジュールに適用されているタグ。オンライン ギャラリーでモジュールを検出する際に役立ちます。 + Tags = ('DesiredStateConfiguration', 'DSC', 'DSCResource', 'Shortcut') + + # A URL to the license for this module. + LicenseUri = 'https://github.com/mkht/DSCR_Shortcut/blob/master/LICENSE' + + # このプロジェクトのメイン Web サイトの URL。 + ProjectUri = 'https://github.com/mkht/DSCR_Shortcut' + + # このモジュールを表すアイコンの URL。 + # IconUri = '' + + # このモジュールの ReleaseNotes + # ReleaseNotes = '' + + } # PSData ハッシュテーブル終了 + + } # PrivateData ハッシュテーブル終了 + +} diff --git a/deployment/dsc/azshcihost/DSCR_Shortcut/2.1.1/DSCResources/cShortcut/cShortcut.psm1 b/deployment/dsc/azshcihost/DSCR_Shortcut/2.1.1/DSCResources/cShortcut/cShortcut.psm1 new file mode 100644 index 0000000..57026d8 --- /dev/null +++ b/deployment/dsc/azshcihost/DSCR_Shortcut/2.1.1/DSCResources/cShortcut/cShortcut.psm1 @@ -0,0 +1,861 @@ +# Import ShellLink class +$ShellLinkPath = Join-Path $PSScriptRoot '..\..\Libs\ShellLink\ShellLink.cs' +if (Test-Path -LiteralPath $ShellLinkPath -PathType Leaf) { + Add-Type -TypeDefinition (Get-Content -LiteralPath $ShellLinkPath -Raw -Encoding UTF8) -Language 'CSharp' -ErrorAction Stop +} + +# Import VKeyUtil class +$VKeyUtilPath = Join-Path $PSScriptRoot '..\..\Libs\VKeyUtil\VKeyUtil.cs' +if (Test-Path -LiteralPath $VKeyUtilPath -PathType Leaf) { + Add-Type -TypeDefinition (Get-Content -LiteralPath $VKeyUtilPath -Raw -Encoding UTF8) -Language 'CSharp' -ErrorAction Stop -ReferencedAssemblies System.Windows.Forms +} + +Enum Ensure { + Absent + Present +} + +Enum WindowStyle { + undefined = 0 + normal = 1 + maximized = 3 + minimized = 7 +} + +function Get-TargetResource { + [CmdletBinding()] + [OutputType([Hashtable])] + param + ( + [Parameter()] + [ValidateSet('Present', 'Absent')] + [string] + $Ensure = [Ensure]::Present, + + [Parameter(Mandatory)] + [string] + $Path, + + [Parameter(Mandatory)] + [string] + $Target, + + [Parameter()] + [string] + $WorkingDirectory, + + [Parameter()] + [string] + $Arguments, + + [Parameter()] + [string] + $Description, + + [Parameter()] + [string] + $Icon, + + [Parameter()] + [string] + $HotKey, + + [Parameter()] + [uint16] + $HotKeyCode = 0x0000, + + [ValidateSet('normal', 'maximized', 'minimized')] + [string] + $WindowStyle = [WindowStyle]::normal, + + [Parameter()] + [string]$AppUserModelID + ) + + if (-not $Path.EndsWith('.lnk')) { + Write-Verbose ("File extension is not 'lnk'. Automatically add extension") + $Path = $Path + '.lnk' + } + + $Ensure = [Ensure]::Present + + try { + # check file exists + if (-not (Test-Path -LiteralPath $Path -PathType Leaf)) { + Write-Verbose 'File not found.' + $Ensure = [Ensure]::Absent + } + else { + $Shortcut = Get-Shortcut -Path $Path -ReadOnly -ErrorAction Continue + } + $returnValue = @{ + Ensure = $Ensure + Path = $Path + Target = $Shortcut.TargetPath + WorkingDirectory = $Shortcut.WorkingDirectory + Arguments = $Shortcut.Arguments + Description = $Shortcut.Description + Icon = $Shortcut.IconLocation + HotKey = ConvertTo-HotKeyString -HotKeyCode $Shortcut.Hotkey + HotKeyCode = $Shortcut.Hotkey + WindowStyle = [WindowStyle]::undefined + AppUserModelID = $Shortcut.AppUserModelID + } + + if ($Shortcut.WindowStyle -as [WindowStyle]) { + $returnValue.WindowStyle = [WindowStyle]$Shortcut.WindowStyle + } + + $returnValue + } + finally { + if ($Shortcut -is [IDisposable]) { + $Shortcut.Dispose() + $Shortcut = $null + } + } +} # end of Get-TargetResource + + +function Set-TargetResource { + [CmdletBinding()] + param + ( + [Parameter()] + [ValidateSet('Present', 'Absent')] + [string] + $Ensure = [Ensure]::Present, + + [Parameter(Mandatory)] + [string] + $Path, + + [Parameter(Mandatory)] + [string] + $Target, + + [Parameter()] + [string] + $WorkingDirectory, + + [Parameter()] + [string] + $Arguments, + + [Parameter()] + [string] + $Description, + + [Parameter()] + [string] + $Icon, + + [Parameter()] + [string] + $HotKey, + + [Parameter()] + [uint16] + $HotKeyCode = 0x0000, + + [ValidateSet('normal', 'maximized', 'minimized')] + [string] + $WindowStyle = [WindowStyle]::normal, + + [Parameter()] + [string]$AppUserModelID + ) + + $arg = [HashTable]$PSBoundParameters + + if (-not $Path.EndsWith('.lnk')) { + Write-Verbose ("File extension is not 'lnk'. Automatically add extension") + $arg.Path = $Path + '.lnk' + } + + if ($Icon -and ($Icon -notmatch ',\d+$')) { + $arg.Icon = $Icon + ',0' + } + + # Ensure = "Absent" + if ($Ensure -eq [Ensure]::Absent) { + Write-Verbose ('Remove shortcut file "{0}"' -f $arg.Path) + Remove-Item -LiteralPath $arg.Path -Force + } + else { + # Ensure = "Present" + $null = $arg.Remove('Ensure') + Update-Shortcut @arg -Force + } + +} # end of Set-TargetResource + + +function Test-TargetResource { + [CmdletBinding()] + [OutputType([bool])] + param + ( + [Parameter()] + [ValidateSet('Present', 'Absent')] + [string] + $Ensure = [Ensure]::Present, + + [Parameter(Mandatory)] + [string] + $Path, + + [Parameter(Mandatory)] + [string] + $Target, + + [Parameter()] + [string] + $WorkingDirectory, + + [Parameter()] + [string] + $Arguments, + + [Parameter()] + [string] + $Description, + + [Parameter()] + [string] + $Icon = ',0', + + [Parameter()] + [string] + $HotKey, + + [Parameter()] + [uint16] + $HotKeyCode = 0x0000, + + [ValidateSet('normal', 'maximized', 'minimized')] + [string] + $WindowStyle = [WindowStyle]::normal, + + [Parameter()] + [string]$AppUserModelID + ) + + <# 想定される状態パターンと返却するべき値 + 1. ショートカットがあるべき(Present) + 1-A. ショートカットなし => 更新の必要あり($false) + 1-B. ショートカットはあるがプロパティが正しくない => 更新の必要あり($false) + 1-C. ショートカットあり、プロパティ一致 => 何もする必要なし($true) + 2. ショートカットはあるべきではない(Absent) + 2-A. ショートカットなし => 何もする必要なし($true) + 2-B. ショートカットあり => 削除の必要あり($false) + #> + + # 拡張子つける + if (-not $Path.EndsWith('.lnk')) { + Write-Verbose ("File extension is not 'lnk'. Automatically add extension") + $Path = $Path + '.lnk' + } + + if ($Icon -and ($Icon -notmatch ',\d+$')) { + $Icon = $Icon + ',0' + } + + # HotKey文字列からHotKeyCode(数値表現)を取得 + if ($HotKey) { + # $HotKeyStr = Format-HotKeyString $HotKey + $HotKeyCode = ConvertFrom-HotKeyString -HotKey $HotKey + } + else { + # $HotKeyStr = [string]::Empty + $HotKeyCode = 0x0000 + } + + $ReturnValue = $false + switch ($Ensure) { + 'Absent' { + # ファイルがなければ$true あれば$false + $ReturnValue = (-not (Test-Path -LiteralPath $Path -PathType Leaf)) + } + 'Present' { + $Info = Get-TargetResource -Ensure $Ensure -Path $Path -Target $Target + if ($Info.Ensure -eq [Ensure]::Absent) { + $ReturnValue = $false + } + else { + # Tests whether the shortcut property is the same as the specified parameter. + $NotMatched = @() + if ($Info.Target -ne $Target) { + $NotMatched += 'Target' + } + + if ($PSBoundParameters.ContainsKey('WorkingDirectory') -and ($Info.WorkingDirectory -ne $WorkingDirectory)) { + $NotMatched += 'WorkingDirectory' + } + + if ($PSBoundParameters.ContainsKey('Arguments') -and ($Info.Arguments -ne $Arguments)) { + $NotMatched += 'Arguments' + } + + if ($PSBoundParameters.ContainsKey('Description') -and ($Info.Description -ne $Description)) { + $NotMatched += 'Description' + } + + if ($PSBoundParameters.ContainsKey('Icon') -and ($Info.Icon -ne $Icon)) { + $NotMatched += 'Icon' + } + + if ($PSBoundParameters.ContainsKey('HotKey') -and ($Info.HotKeyCode -ne $HotKeyCode)) { + $NotMatched += 'HotKey' + } + + if ($PSBoundParameters.ContainsKey('WindowStyle') -and ($Info.WindowStyle -ne $WindowStyle)) { + $NotMatched += 'WindowStyle' + } + + if ($PSBoundParameters.ContainsKey('AppUserModelID') -and ($Info.AppUserModelID -ne $AppUserModelID)) { + $NotMatched += 'AppUserModelID' + } + + $ReturnValue = ($NotMatched.Count -eq 0) + if (-not $ReturnValue) { + $NotMatched | ForEach-Object { + Write-Verbose ('{0} property does not match!' -f $_) + } + } + } + } + } + Write-Verbose "Test returns $ReturnValue" + return $ReturnValue +} # end of Test-TargetResource + + +function Get-Shortcut { + [CmdletBinding()] + [OutputType([ShellLink])] + param + ( + # Path of shortcut files + [Parameter(Position = 0, Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName)] + [Alias('FullName')] + [ValidateScript( { Test-Path -LiteralPath $_ -PathType Leaf } )] + [string]$Path, + + [switch]$ReadOnly + ) + + Begin { + if ($ReadOnly) { + [int]$flag = 0x00000000 #STGM_READ + } + else { + [int]$flag = 0x00000002 #STGM_READWRITE + } + } + + Process { + try { + $Shortcut = New-Object -TypeName ShellLink + $Shortcut.Load($Path, $flag) + return $Shortcut + } + catch { + if ($Shortcut -is [IDisposable]) { + $Shortcut.Dispose() + $Shortcut = $null + } + + Write-Error -Exception $_.Exception + return $null + } + } +} + + +function New-Shortcut { + [CmdletBinding()] + [OutputType([System.IO.FileSystemInfo])] + param + ( + # set file path to create shortcut. If the path not ends with '.lnk', extension will be add automatically. + [Parameter( + Position = 0, + Mandatory, + ValueFromPipelineByPropertyName)] + [Alias('FilePath')] + [string]$Path, + + # Set Target full path to create shortcut + [Parameter( + Position = 1, + Mandatory, + ValueFromPipelineByPropertyName)] + [Alias('Target')] + [string]$TargetPath, + + # Set Description for shortcut. + [Parameter(ValueFromPipelineByPropertyName)] + [Alias('Comment')] + [string]$Description, + + # Set Arguments for shortcut. + [Parameter(ValueFromPipelineByPropertyName)] + [string]$Arguments, + + # Set WorkingDirectory for shortcut. + [Parameter(ValueFromPipelineByPropertyName)] + [string]$WorkingDirectory, + + # Set IconLocation for shortcut. + [Parameter(ValueFromPipelineByPropertyName)] + [string]$Icon, + + [Parameter(ValueFromPipelineByPropertyName)] + [string]$HotKey, + + # Set WindowStyle for shortcut. + [Parameter(ValueFromPipelineByPropertyName)] + [ValidateSet('normal', 'maximized', 'minimized')] + [string]$WindowStyle = [WindowStyle]::normal, + + [Parameter(ValueFromPipelineByPropertyName)] + [string]$AppUserModelID, + + # set if you want to show create shortcut result + [switch]$PassThru, + + [switch]$Force + ) + + begin { + $extension = '.lnk' + } + + process { + # Set Path of a Shortcut + if (-not $Path.EndsWith($extension)) { + $Path = $Path + $extension + } + + if ($HotKey) { + $local:HotKeyCode = ConvertFrom-HotKeyString -HotKey $HotKey -ErrorAction Stop + } + else { + $local:HotKeyCode = 0x0000 + } + + if (-not (Test-Path -LiteralPath (Split-Path $Path -Parent))) { + Write-Verbose 'Create a parent folder' + $null = New-Item -Path (Split-Path $Path -Parent) -ItemType Directory -Force -ErrorAction Stop + } + + $fileName = Split-Path $Path -Leaf # Filename of shortcut + $Directory = Resolve-Path -Path (Split-Path $Path -Parent) # Directory of shortcut + $Path = Join-Path $Directory $fileName # Fullpath of shortcut + + #Remove existing shortcut (when the Force switch is specified) + if (Test-Path -LiteralPath $Path -PathType Leaf) { + if ($Force) { + Write-Verbose 'Remove existing shortcut file' + Remove-Item $Path -Force -ErrorAction SilentlyContinue + } + else { + Write-Error -Exception ([System.IO.IOException]::new("The file '$Path' is already exists.")) + return + } + } + + # Call IShellLink to create Shortcut + Write-Verbose ("Trying to create Shortcut to '{0}'" -f $Path) + try { + $Shortcut = New-Object -TypeName ShellLink + $Shortcut.TargetPath = $TargetPath + $Shortcut.Description = $Description + $Shortcut.WindowStyle = [int][WindowStyle]$WindowStyle + $Shortcut.Arguments = $Arguments + $Shortcut.WorkingDirectory = $WorkingDirectory + if ($PSBoundParameters.ContainsKey('Icon')) { + $Shortcut.IconLocation = $Icon + } + if ($PSBoundParameters.ContainsKey('AppUserModelID')) { + $Shortcut.AppUserModelID = $AppUserModelID + } + if ($PSBoundParameters.ContainsKey('Hotkey')) { + $Shortcut.Hotkey = $local:HotKeyCode + } + $Shortcut.Save($Path) + Write-Verbose 'Shortcut file created successfully.' + } + catch { + Write-Error -Exception $_.Exception + return + } + finally { + if ($Shortcut -is [System.IDisposable]) { + $Shortcut.Dispose() + $Shortcut = $null + } + } + + if ($PassThru) { + Get-Item -LiteralPath $Path + } + } + + end {} +} + +function Update-Shortcut { + [CmdletBinding(DefaultParameterSetName = 'ShellLink')] + [OutputType([System.IO.FileSystemInfo])] + param + ( + # Set file path to update shortcut. If the path not ends with '.lnk', extension will be add automatically. + [Parameter( + Position = 0, + Mandatory, + ValueFromPipeline, + ValueFromPipelineByPropertyName, + ParameterSetName = 'FilePath')] + [Alias('FilePath')] + [string]$Path, + + [Parameter( + Position = 0, + Mandatory, + ValueFromPipeline, + ParameterSetName = 'ShellLink')] + [ShellLink]$InputObject, + + # Set Target full path for shortcut + [Parameter(ParameterSetName = 'FilePath', ValueFromPipelineByPropertyName)] + [Parameter(ParameterSetName = 'ShellLink')] + [Alias('Target')] + [string]$TargetPath, + + # Set Description for shortcut. + [Parameter(ParameterSetName = 'FilePath', ValueFromPipelineByPropertyName)] + [Parameter(ParameterSetName = 'ShellLink')] + [Alias('Comment')] + [string]$Description, + + # Set Arguments for shortcut. + [Parameter(ParameterSetName = 'FilePath', ValueFromPipelineByPropertyName)] + [Parameter(ParameterSetName = 'ShellLink')] + [string]$Arguments, + + # Set WorkingDirectory for shortcut. + [Parameter(ParameterSetName = 'FilePath', ValueFromPipelineByPropertyName)] + [Parameter(ParameterSetName = 'ShellLink')] + [string]$WorkingDirectory, + + # Set IconLocation for shortcut. + [Parameter(ParameterSetName = 'FilePath', ValueFromPipelineByPropertyName)] + [Parameter(ParameterSetName = 'ShellLink')] + [string]$Icon, + + [Parameter(ParameterSetName = 'FilePath', ValueFromPipelineByPropertyName)] + [Parameter(ParameterSetName = 'ShellLink')] + [string]$HotKey, + + # Set WindowStyle for shortcut. + [Parameter(ParameterSetName = 'FilePath', ValueFromPipelineByPropertyName)] + [Parameter(ParameterSetName = 'ShellLink')] + [ValidateSet('normal', 'maximized', 'minimized')] + [string]$WindowStyle = [WindowStyle]::normal, + + [Parameter(ParameterSetName = 'FilePath', ValueFromPipelineByPropertyName)] + [Parameter(ParameterSetName = 'ShellLink')] + [string]$AppUserModelID, + + # set if you want to show create shortcut result + [switch]$PassThru, + + [switch]$Force + ) + + begin { + $extension = '.lnk' + } + + process { + if ($PSCmdlet.ParameterSetName -eq 'FilePath') { + if (-not $Path.EndsWith($extension)) { + $Path = $Path + $extension + } + + if (-not (Test-Path -LiteralPath $Path -PathType Leaf)) { + if ($Force -and $TargetPath) { + New-Shortcut @PSBoundParameters + return + } + else { + Write-Error -Exception ([System.IO.FileNotFoundException]::new("The file '$Path' does not exists.")) + return + } + } + } + elseif ($PSCmdlet.ParameterSetName -eq 'ShellLink') { + if (-not ($InputObject.FilePath)) { + Write-Error -Exception ([System.ArgumentException]::new("The InputObject does not valid.")) + return + } + } + + if ($HotKey) { + $local:HotKeyCode = ConvertFrom-HotKeyString -HotKey $HotKey -ErrorAction Stop + } + else { + $local:HotKeyCode = 0x0000 + } + + # Call IShellLink to update Shortcut + Write-Verbose ("Updating Shortcut for '{0}'" -f $Path) + try { + if ($PSCmdlet.ParameterSetName -eq 'FilePath') { + $InputObject = New-Object -TypeName ShellLink + $InputObject.Load($Path) + } + elseif ($PSCmdlet.ParameterSetName -eq 'ShellLink') { + $Path = $InputObject.FilePath + } + + $Shortcut = $InputObject + if ($PSBoundParameters.ContainsKey('TargetPath')) { + $Shortcut.TargetPath = $TargetPath + } + if ($PSBoundParameters.ContainsKey('Description')) { + $Shortcut.Description = $Description + } + if ($PSBoundParameters.ContainsKey('WindowStyle')) { + $Shortcut.WindowStyle = [int][WindowStyle]$WindowStyle + } + if ($PSBoundParameters.ContainsKey('Arguments')) { + $Shortcut.Arguments = $Arguments + } + if ($PSBoundParameters.ContainsKey('WorkingDirectory')) { + $Shortcut.WorkingDirectory = $WorkingDirectory + } + if ($PSBoundParameters.ContainsKey('Icon')) { + $Shortcut.IconLocation = $Icon + } + if ($PSBoundParameters.ContainsKey('AppUserModelID')) { + $Shortcut.AppUserModelID = $AppUserModelID + } + if ($PSBoundParameters.ContainsKey('Hotkey')) { + $Shortcut.Hotkey = $local:HotKeyCode + } + + $Shortcut.Save($Path) + Write-Verbose 'Shortcut file updated successfully.' + } + catch { + Write-Error -Exception $_.Exception + return + } + finally { + if ($Shortcut -is [System.IDisposable]) { + $Shortcut.Dispose() + $Shortcut = $null + } + } + + if ($PassThru) { + Get-Item -LiteralPath $Path + } + } + + end {} +} + + +function Format-HotKeyString { + [CmdletBinding()] + [OutputType([string])] + Param( + [Parameter(Mandatory, Position = 0)] + [AllowEmptyString()] + [string]$HotKey + ) + + if ([string]::IsNullOrWhiteSpace($HotKey)) { + return [string]::Empty + } + + [string[]]$local:HotKeyArray = $HotKey.split('+').Trim() + + if ($local:HotKeyArray.Count -eq 1 -and $local:HotKeyArray[0] -match '^F([1-9]|1[0-9]|2[0-4])$') { + # F1~F24は修飾キーを伴わず単体でもOK + } + elseif ($local:HotKeyArray.Count -notin (2..4)) { + #最短で修飾+キーの2要素、最長でAlt+Ctrl+Shift+キーの4要素 + Write-Error 'HotKey is not valid format.' + return [string]::Empty + } + elseif ($local:HotKeyArray[0] -notmatch '^(Ctrl|Alt|Shift)$') { + #修飾キーから始まっていないとダメ + Write-Error 'HotKey is not valid format.' + return [string]::Empty + } + + #優先順位付きソート + $local:sort = $local:HotKeyArray | ForEach-Object { + switch ($_) { + 'Ctrl' { 1 } + 'Shift' { 2 } + 'Alt' { 3 } + Default { 4 } + } + } + [Array]::Sort($local:sort, $local:HotKeyArray) + $local:HotKeyArray -join '+' +} + + +function ConvertFrom-HotKeyString { + [CmdletBinding()] + [OutputType([uint16])] + Param( + [Parameter(Mandatory, Position = 0, ValueFromPipeline)] + [AllowEmptyString()] + [string]$HotKey + ) + + begin { + [uint16]$HOTKEYF_SHIFT = 0x0100 + [uint16]$HOTKEYF_CONTROL = 0x0200 + [uint16]$HOTKEYF_ALT = 0x0400 + # [uint16]$HOTKEYF_EXT = 0x0800 #? + + Add-Type -AssemblyName System.Windows.Forms + $KeysConverter = New-Object -TypeName 'System.Windows.Forms.KeysConverter' + } + + Process { + if ([string]::IsNullOrWhiteSpace($HotKey)) { + return 0x0000 + } + + [uint16]$local:HotKeyCode = 0x0000 + $HotKey = Format-HotKeyString -HotKey $HotKey + [string[]]$local:HotKeyArray = $HotKey.split('+').Trim() + + switch ($local:HotKeyArray) { + 'Shift' { + $local:HotKeyCode = $local:HotKeyCode -bor $HOTKEYF_SHIFT + continue + } + + 'Ctrl' { + $local:HotKeyCode = $local:HotKeyCode -bor $HOTKEYF_CONTROL + continue + } + + 'Alt' { + $local:HotKeyCode = $local:HotKeyCode -bor $HOTKEYF_ALT + continue + } + + Default { + $local:KeyString = $_ + $local:KeyCode = $null + try { + $local:KeyCode = $KeysConverter.ConvertFromString($local:KeyString.ToUpper()) + } + catch [ArgumentException] { + try { + $local:KeyCode = [VKeyUtil]::GetKeyCodeFromChar($local:KeyString) -band 0x00ff + } + catch { + Write-Error 'HotKey is not valid format.' + return + } + } + catch { + Write-Error -Exception $_.Exception + return + } + + if ($null -ne $local:KeyCode) { + $local:HotKeyCode = $local:HotKeyCode -bor $local:KeyCode + } + } + } + + $local:HotKeyCode + } + + End { + $KeysConverter = $null + } +} + + +function ConvertTo-HotKeyString { + [CmdletBinding()] + [OutputType([string])] + Param( + [Parameter(Mandatory, Position = 0, ValueFromPipeline)] + [uint16]$HotKeyCode + ) + + begin { + [uint16]$HOTKEYF_SHIFT = 0x0100 + [uint16]$HOTKEYF_CONTROL = 0x0200 + [uint16]$HOTKEYF_ALT = 0x0400 + # [uint16]$HOTKEYF_EXT = 0x0800 #? + + Add-Type -AssemblyName System.Windows.Forms + $KeysConverter = New-Object -TypeName 'System.Windows.Forms.KeysConverter' + } + + Process { + if ($HotKeyCode -eq 0x0000) { + return [string]::Empty + } + + [string[]]$local:HotKeyArray = @() + + # Modifier Keys + if ($HotKeyCode -band $HOTKEYF_SHIFT) { + $local:HotKeyArray += 'Shift' + } + if ($HotKeyCode -band $HOTKEYF_CONTROL) { + $local:HotKeyArray += 'Ctrl' + } + if ($HotKeyCode -band $HOTKEYF_ALT) { + $local:HotKeyArray += 'Alt' + } + + # Key + [string]$local:Key = $null + try { $local:Key = [VKeyUtil]::GetCharsFromKeys($HotKeyCode -band 0x00ff) } catch {} + + if ([string]::IsNullOrWhiteSpace($local:Key)) { + try { + $local:Key = $KeysConverter.ConvertToString($HotKeyCode -band 0x00ff) + } + catch { + Write-Error -Exception $_.Exception + return + } + } + + if (-not [string]::IsNullOrWhiteSpace($local:Key)) { + $local:HotKeyArray += $local:Key.ToUpper() + # return formatted string + Format-HotKeyString -HotKey ([string]::Join('+', $local:HotKeyArray)) + } + else { + [string]::Empty + } + } + + End { + $KeysConverter = $null + } +} + + +Export-ModuleMember -Function *-TargetResource diff --git a/deployment/dsc/azshcihost/DSCR_Shortcut/2.1.1/DSCResources/cShortcut/cShortcut.schema.mof b/deployment/dsc/azshcihost/DSCR_Shortcut/2.1.1/DSCResources/cShortcut/cShortcut.schema.mof new file mode 100644 index 0000000..bf47f8c --- /dev/null +++ b/deployment/dsc/azshcihost/DSCR_Shortcut/2.1.1/DSCResources/cShortcut/cShortcut.schema.mof @@ -0,0 +1,16 @@ + +[ClassVersion("1.0.0.0"), FriendlyName("cShortcut")] +class cShortcut : OMI_BaseResource +{ + [Write, ValueMap{"Present","Absent"}, Values{"Present","Absent"}] String Ensure; + [Required] String Target; + [Key] String Path; + [Write] String Arguments; + [Write] String WorkingDirectory; + [Write] String Description; + [Write] String Icon; + [Write] String HotKey; + [read] UInt16 HotKeyCode; + [Write, ValueMap{"normal","maximized","minimized"}, Values{"normal","maximized","minimized"}] String WindowStyle; + [Write] String AppUserModelID; +}; diff --git a/deployment/dsc/azshcihost/DSCR_Shortcut/2.1.1/LICENSE b/deployment/dsc/azshcihost/DSCR_Shortcut/2.1.1/LICENSE new file mode 100644 index 0000000..056a509 --- /dev/null +++ b/deployment/dsc/azshcihost/DSCR_Shortcut/2.1.1/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2020 mkht + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/deployment/dsc/azshcihost/DSCR_Shortcut/2.1.1/Libs/ShellLink/ShellLink.cs b/deployment/dsc/azshcihost/DSCR_Shortcut/2.1.1/Libs/ShellLink/ShellLink.cs new file mode 100644 index 0000000..0bb6a10 --- /dev/null +++ b/deployment/dsc/azshcihost/DSCR_Shortcut/2.1.1/Libs/ShellLink/ShellLink.cs @@ -0,0 +1,612 @@ +using System; +using System.IO; +using System.Runtime.InteropServices; +using System.Runtime.InteropServices.ComTypes; +using System.Text; +using ComTypes = System.Runtime.InteropServices.ComTypes; + +// Original code is https://emoacht.wordpress.com/2012/11/14/csharp-appusermodelid/ +public class ShellLink : IDisposable +{ + #region Win32 and COM + + // IShellLink Interface + [ComImport] + [Guid("000214F9-0000-0000-C000-000000000046")] + [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] + private interface IShellLinkW + { + uint GetPath([Out, MarshalAs(UnmanagedType.LPWStr)] StringBuilder pszFile, int cch, ref WIN32_FIND_DATAW pfd, uint fFlags); + uint GetIDList(out IntPtr ppidl); + uint SetIDList(IntPtr pidl); + uint GetDescription([Out, MarshalAs(UnmanagedType.LPWStr)] StringBuilder pszName, int cch); + uint SetDescription([MarshalAs(UnmanagedType.LPWStr)] string pszName); + uint GetWorkingDirectory([Out, MarshalAs(UnmanagedType.LPWStr)] StringBuilder pszDir, int cch); + uint SetWorkingDirectory([MarshalAs(UnmanagedType.LPWStr)] string pszDir); + uint GetArguments([Out, MarshalAs(UnmanagedType.LPWStr)] StringBuilder pszArgs, int cch); + uint SetArguments([MarshalAs(UnmanagedType.LPWStr)] string pszArgs); + uint GetHotKey(out ushort pwHotkey); + uint SetHotKey(ushort wHotKey); + uint GetShowCmd(out int piShowCmd); + uint SetShowCmd(int iShowCmd); + uint GetIconLocation([Out, MarshalAs(UnmanagedType.LPWStr)] StringBuilder pszIconPath, int cch, out int piIcon); + uint SetIconLocation([MarshalAs(UnmanagedType.LPWStr)] string pszIconPath, int iIcon); + uint SetRelativePath([MarshalAs(UnmanagedType.LPWStr)] string pszPathRel, uint dwReserved); + uint Resolve(IntPtr hwnd, uint fFlags); + uint SetPath([MarshalAs(UnmanagedType.LPWStr)] string pszFile); + } + + // ShellLink CoClass (ShellLink object) + [ComImport] + [ClassInterface(ClassInterfaceType.None)] + [Guid("00021401-0000-0000-C000-000000000046")] + private class CShellLink { } + + // WIN32_FIND_DATAW Structure + [StructLayout(LayoutKind.Sequential, Pack = 4, CharSet = CharSet.Unicode)] + private struct WIN32_FIND_DATAW + { + public uint dwFileAttributes; + public ComTypes.FILETIME ftCreationTime; + public ComTypes.FILETIME ftLastAccessTime; + public ComTypes.FILETIME ftLastWriteTime; + public uint nFileSizeHigh; + public uint nFileSizeLow; + public uint dwReserved0; + public uint dwReserved1; + [MarshalAs(UnmanagedType.ByValTStr, SizeConst = MAX_PATH)] + public string cFileName; + [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 14)] + public string cAlternateFileName; + } + + // IPropertyStore Interface + [ComImport] + [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] + [Guid("886D8EEB-8CF2-4446-8D02-CDBA1DBDCF99")] + private interface IPropertyStore + { + uint GetCount([Out] out uint cProps); + uint GetAt([In] uint iProp, out PropertyKey pkey); + uint GetValue([In] ref PropertyKey key, [Out] PropVariant pv); + uint SetValue([In] ref PropertyKey key, [In] PropVariant pv); + uint Commit(); + } + + // PropertyKey Structure + // Narrowed down from PropertyKey.cs of Windows API Code Pack 1.1 + [StructLayout(LayoutKind.Sequential, Pack = 4)] + private struct PropertyKey + { + #region Fields + + private Guid formatId; // Unique GUID for property + private Int32 propertyId; // Property identifier (PID) + + #endregion + + #region Public Properties + + public Guid FormatId + { + get { return formatId; } + } + + public Int32 PropertyId + { + get { return propertyId; } + } + + #endregion + + #region Constructor + + public PropertyKey(Guid formatId, Int32 propertyId) + { + this.formatId = formatId; + this.propertyId = propertyId; + } + + public PropertyKey(string formatId, Int32 propertyId) + { + this.formatId = new Guid(formatId); + this.propertyId = propertyId; + } + + #endregion + } + + // PropVariant Class (only for string value) + // Narrowed down from PropVariant.cs of Windows API Code Pack 1.1 + // Originally from http://blogs.msdn.com/b/adamroot/archive/2008/04/11 + // /interop-with-propvariants-in-net.aspx + [StructLayout(LayoutKind.Explicit)] + private sealed class PropVariant : IDisposable + { + #region Fields + + [FieldOffset(0)] + ushort valueType; // Value type + + // [FieldOffset(2)] + // ushort wReserved1; // Reserved field + // [FieldOffset(4)] + // ushort wReserved2; // Reserved field + // [FieldOffset(6)] + // ushort wReserved3; // Reserved field + + [FieldOffset(8)] + IntPtr ptr; // Value + + #endregion + + #region Public Properties + + // Value type (System.Runtime.InteropServices.VarEnum) + public VarEnum VarType + { + get { return (VarEnum)valueType; } + set { valueType = (ushort)value; } + } + + // Whether value is empty or null + public bool IsNullOrEmpty + { + get { return (valueType == (ushort)VarEnum.VT_EMPTY || valueType == (ushort)VarEnum.VT_NULL); } + } + + // Value (only for string value) + public string Value + { + get { return Marshal.PtrToStringUni(ptr); } + } + + #endregion + + #region Constructor + + public PropVariant() { } + + // Construct with string value + public PropVariant(string value) + { + if (value == null) + throw new ArgumentException("Failed to set value."); + + valueType = (ushort)VarEnum.VT_LPWSTR; + ptr = Marshal.StringToCoTaskMemUni(value); + } + + #endregion + + #region Destructor + + ~PropVariant() + { + Dispose(); + } + + public void Dispose() + { + PropVariantClear(this); + GC.SuppressFinalize(this); + } + + #endregion + } + + [DllImport("Ole32.dll", PreserveSig = false)] + private static extern void PropVariantClear([In, Out] PropVariant pvar); + + [DllImport("Shell32.dll", CharSet = CharSet.Unicode, PreserveSig = false)] + private static extern void SHGetNameFromIDList(IntPtr pidl, uint sigdnName, [Out, MarshalAs(UnmanagedType.LPTStr)] out string ppszName); + + [DllImport("Shell32.dll")] + private static extern void ILFree(IntPtr pidl); + + #endregion + + #region Private + + private IShellLinkW shellLinkW = null; + + private bool readOnly = false; + + private readonly PropertyKey AppUserModelIDKey = + new PropertyKey("{9F4C2855-9F79-4B39-A8D0-E1D42DE1D5F3}", 5); + + private const int MAX_PATH = 260; + private const int INFOTIPSIZE = 1024; + + private const int SW_SHOWNORMAL = 1; + private const int SW_SHOWMINIMIZED = 2; + private const int SW_SHOWMAXIMIZED = 3; + private const int SW_SHOWMINNOACTIVE = 7; + + private const int STGM_READ = 0x00000000; + private const int STGM_READWRITE = 0x00000002; + private const uint SLGP_UNCPRIORITY = 0x0002; + private const uint SLGP_RAWPATH = 0x0004; + + private const uint SIGDN_DESKTOPABSOLUTEPARSING = 0x80028000; + + + private IPersistFile PersistFile + { + get + { + IPersistFile PersistFile = (IPersistFile)shellLinkW; + if (PersistFile == null) + throw new COMException("Failed to create IPersistFile."); + else + return PersistFile; + } + } + + private IPropertyStore PropertyStore + { + get + { + IPropertyStore PropertyStore = (IPropertyStore)shellLinkW; + if (PropertyStore == null) + throw new COMException("Failed to create IPropertyStore."); + else + return PropertyStore; + } + } + + #endregion + + #region Public + + // Path of loaded shortcut file + public string FilePath + { + get + { + string shortcutFile; + PersistFile.GetCurFile(out shortcutFile); + return shortcutFile; + } + } + + // Path of target file + public string TargetPath + { + get + { + StringBuilder targetPath = new StringBuilder(MAX_PATH); + WIN32_FIND_DATAW data = new WIN32_FIND_DATAW(); + + VerifySucceeded(shellLinkW.GetPath(targetPath, targetPath.Capacity, ref data, SLGP_RAWPATH)); + return targetPath.ToString(); + } + set + { + VerifyReadOnly(); + VerifySucceeded(shellLinkW.SetPath(value)); + } + } + + // Shell item id list + public string IDList + { + get + { + string idList; + System.IntPtr pidl = IntPtr.Zero; + + try + { + VerifySucceeded(shellLinkW.GetIDList(out pidl)); + SHGetNameFromIDList(pidl, SIGDN_DESKTOPABSOLUTEPARSING, out idList); + return idList; + } + finally + { + ILFree(pidl); + } + } + } + + // Description + public string Description + { + get + { + StringBuilder description = new StringBuilder(INFOTIPSIZE); + + VerifySucceeded(shellLinkW.GetDescription(description, description.Capacity)); + return description.ToString(); + } + set + { + VerifyReadOnly(); + VerifySucceeded(shellLinkW.SetDescription(value)); + } + } + + // Arguments + public string Arguments + { + get + { + StringBuilder arguments = new StringBuilder(INFOTIPSIZE); + + VerifySucceeded(shellLinkW.GetArguments(arguments, arguments.Capacity)); + return arguments.ToString(); + } + set + { + VerifyReadOnly(); + VerifySucceeded(shellLinkW.SetArguments(value)); + } + } + + // WorkingDirectory + public string WorkingDirectory + { + get + { + StringBuilder workingDirectory = new StringBuilder(MAX_PATH); + + VerifySucceeded(shellLinkW.GetWorkingDirectory(workingDirectory, workingDirectory.Capacity)); + return workingDirectory.ToString(); + } + set + { + VerifyReadOnly(); + VerifySucceeded(shellLinkW.SetWorkingDirectory(value)); + } + } + + // IconLocation + public string IconLocation + { + get + { + StringBuilder iconLocation = new StringBuilder(MAX_PATH); + int iconIdx; + VerifySucceeded(shellLinkW.GetIconLocation(iconLocation, iconLocation.Capacity, out iconIdx)); + iconLocation.Append(","); + iconLocation.Append(iconIdx.ToString()); + return iconLocation.ToString(); + } + set + { + VerifyReadOnly(); + + int idx = value.LastIndexOf(","); + string iconLocation; + string strIdx; + int iconIdx; + if (idx >= 0) + { + strIdx = value.Substring(idx + 1); + if (Int32.TryParse(strIdx, out iconIdx)) + { + iconLocation = value.Substring(0, idx); + } + else + { + iconLocation = value; + iconIdx = 0; + } + } + else + { + iconLocation = value; + iconIdx = 0; + } + VerifySucceeded(shellLinkW.SetIconLocation(iconLocation, iconIdx)); + } + } + + // WindowStyle + public int WindowStyle + { + get + { + int windowStyle; + + VerifySucceeded(shellLinkW.GetShowCmd(out windowStyle)); + switch (windowStyle) + { + case SW_SHOWMINIMIZED: + case SW_SHOWMINNOACTIVE: + return SW_SHOWMINNOACTIVE; + + case SW_SHOWMAXIMIZED: + return SW_SHOWMAXIMIZED; + + case SW_SHOWNORMAL: + return SW_SHOWNORMAL; + + default: + return 0; + } + } + set + { + VerifyReadOnly(); + + int windowStyle; + switch (value) + { + case 0: + case 1: + windowStyle = SW_SHOWNORMAL; + break; + + case 3: + windowStyle = SW_SHOWMAXIMIZED; + break; + + case 7: + windowStyle = SW_SHOWMINNOACTIVE; + break; + + default: + throw new ArgumentException("Unsupported value."); + } + + VerifySucceeded(shellLinkW.SetShowCmd(windowStyle)); + } + } + + // Hotkey + public ushort Hotkey + { + get + { + ushort hotKey; + VerifySucceeded(shellLinkW.GetHotKey(out hotKey)); + return hotKey; + } + set + { + VerifyReadOnly(); + VerifySucceeded(shellLinkW.SetHotKey(value)); + } + } + + // AppUserModelID + // https://docs.microsoft.com/en-us/windows/win32/shell/appids + public string AppUserModelID + { + get + { + using (PropVariant pv = new PropVariant()) + { + VerifySucceeded(PropertyStore.GetValue(AppUserModelIDKey, pv)); + + if (pv.Value == null) + return string.Empty; + else + return pv.Value; + } + } + set + { + VerifyReadOnly(); + using (PropVariant pv = new PropVariant(value)) + { + VerifySucceeded(PropertyStore.SetValue(AppUserModelIDKey, pv)); + VerifySucceeded(PropertyStore.Commit()); + } + } + } + + #endregion + + #region Constructor + + public ShellLink() : this(null) { } + + // Construct with loading shortcut file. + public ShellLink(string file) + { + try + { + shellLinkW = (IShellLinkW)new CShellLink(); + } + catch + { + throw new COMException("Failed to create ShellLink object."); + } + + if (file != null) + Load(file); + } + + #endregion + + #region Destructor + + ~ShellLink() + { + Dispose(false); + } + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + protected virtual void Dispose(bool disposing) + { + if (shellLinkW != null) + { + // Release all references. + Marshal.FinalReleaseComObject(shellLinkW); + shellLinkW = null; + } + } + + #endregion + + #region Methods + + // Save shortcut file. + public void Save() + { + VerifyReadOnly(); + + string file = FilePath; + if (file == null) + throw new InvalidOperationException("File name is not given."); + else + Save(file); + } + + public void Save(string file) + { + if (file == null) + { + throw new ArgumentNullException("File name is required."); + } + else + { + VerifyReadOnly(); + PersistFile.Save(file, true); + } + } + + // Load shortcut file. + public void Load(string file) + { + Load(file, STGM_READWRITE); + } + + public void Load(string file, int flags) + { + if (!File.Exists(file)) + { + throw new FileNotFoundException("File is not found.", file); + } + else + { + PersistFile.Load(file, flags); + if ((flags & 0x0000000f) == 0) + readOnly = true; + } + } + + // Verify if operation succeeded. + private static void VerifySucceeded(uint hresult) + { + if (hresult > 1) + throw new InvalidOperationException("Failed with HRESULT: " + + hresult.ToString("X")); + } + + // Verify if operation as read only. + private void VerifyReadOnly() + { + if (readOnly) + throw new UnauthorizedAccessException("This object is read-only."); + } + + #endregion +} diff --git a/deployment/dsc/azshcihost/DSCR_Shortcut/2.1.1/Libs/VKeyUtil/VKeyUtil.cs b/deployment/dsc/azshcihost/DSCR_Shortcut/2.1.1/Libs/VKeyUtil/VKeyUtil.cs new file mode 100644 index 0000000..fa31b8f --- /dev/null +++ b/deployment/dsc/azshcihost/DSCR_Shortcut/2.1.1/Libs/VKeyUtil/VKeyUtil.cs @@ -0,0 +1,27 @@ +using System; +using System.Text; +using System.Windows.Forms; +using System.Runtime.InteropServices; + +public static class VKeyUtil +{ + [DllImport("user32.dll", CharSet = CharSet.Unicode)] + private static extern short VkKeyScanW(char ch); + + [DllImport("user32.dll")] + private static extern int ToUnicode(uint wVirtKey, uint wScanCode, byte[] lpKeyState, [Out, MarshalAs(UnmanagedType.LPWStr)] StringBuilder pwszBuff, int cchBuff, uint wFlags); + + public static short GetKeyCodeFromChar(char ch) + { + return VkKeyScanW(ch); + } + + public static string GetCharsFromKeys(Keys keys) + { + StringBuilder buf = new StringBuilder(10); + byte[] keyboardState = new byte[256]; + + ToUnicode((uint)keys, 0, keyboardState, buf, buf.Capacity, 0); + return buf.ToString(); + } +} diff --git a/deployment/dsc/azshcihost/DSCR_Shortcut/2.1.1/README.md b/deployment/dsc/azshcihost/DSCR_Shortcut/2.1.1/README.md new file mode 100644 index 0000000..5646f85 --- /dev/null +++ b/deployment/dsc/azshcihost/DSCR_Shortcut/2.1.1/README.md @@ -0,0 +1,139 @@ +DSCR_Shortcut +==== + +PowerShell DSC Resource to create shortcut file (LNK file). + +## Install +You can install Resource through [PowerShell Gallery](https://www.powershellgallery.com/packages/DSCR_Shortcut/). +```PowerShell +Install-Module -Name DSCR_Shortcut +``` + +## Resources +* **cShortcut** +PowerShell DSC Resource to create shortcut file. + +## Properties +### cShortcut ++ [string] **Ensure** (Write): + + Specify whether or not a shortcut file exists + + The default value is `Present`. { Present | Absent } + ++ [string] **Path** (Key): + + The path of the shortcut file. + + If the path ends with something other than `.lnk`, The extension will be added automatically to the end of the path + ++ [string] **Target** (Required): + + The target path of the shortcut. + ++ [string] **Arguments** (Write): + + The arguments of the shortcut. + ++ [string] **WorkingDirectory** (Write): + + The working directory of the shortcut. + ++ [string] **WindowStyle** (Write): + + You can select window style. { normal | maximized | minimized } + + The default value is `normal` + ++ [string] **Description** (Write): + + The description of the shortcut. + ++ [string] **Icon** (Write): + + The path of the icon resource. + ++ [string] **HotKey** (Write): + + HotKey (Shortcut Key) of the shortcut + + HotKey works only for shortcuts on the desktop or in the Start menu. + + The syntax is: `"{KeyModifier} + {KeyName}"` ( e.g. `"Alt+Ctrl+Q"`, `"Shift+F9"` ) + + If the hotkey not working after configuration, try to reboot. + ++ [string] **AppUserModelID** (Write): + + Specifies AppUserModelID of the shortcut + + About AppUserModelID, See Microsoft Docs. + https://docs.microsoft.com/en-us/windows/win32/shell/appids + + +## Examples ++ **Example 1**: Create a shortcut to the Internet Explore InPrivate mode to the Administrator's desktop +```PowerShell +Configuration Example1 +{ + Import-DscResource -ModuleName DSCR_Shortcut + cShortcut IE_Desktop + { + Path = 'C:\Users\Administrator\Desktop\PrivateIE.lnk' + Target = "C:\Program Files\Internet Explorer\iexplore.exe" + Arguments = '-private' + } +} +``` + ++ **Example 2**: Specifies All Properties +```PowerShell +Configuration Example2 +{ + Import-DscResource -ModuleName DSCR_Shortcut + cShortcut IE_Desktop + { + Path = 'C:\Users\Administrator\Desktop\PrivateIE.lnk' + Target = 'C:\Program Files\Internet Explorer\iexplore.exe' + Arguments = '-private' + WindowStyle = 'maximized' + WorkingDirectory = 'C:\work' + Description = 'This is a shortcut to the IE' + Icon = 'shell32.dll,277' + HotKey = 'Ctrl+Shift+U' + AppUserModelID = 'Microsoft.InternetExplorer.Default' + } +} +``` + +## ChangeLog +### v2.1.1 + #### Improvements :zap: + - [Regression] Fixed an issue where environment variables in a shortcut file would be unintentionally expanded. + +### v2.1.0 + #### Improvements :zap: + - [Regression] Fixed an issue where environment variables in a shortcut file would be unintentionally expanded. + - Fixed an issue where `HotKey` would not be determined correctly between multiple different keyboard layouts. + - You can now specify the Fn-key for `HotKey` by itself. (In previous versions, it had to be combined with modifier keys.) + - Add Unit & Integration tests. + +### v2.0.0 + #### BREAKING CHANGES :boom: + - v1 of the module initializes properties not specified in the configuration when updating an existing shortcut file, but v2 preserves them. + + #### New Features :sparkles: + - Add `AppUserModelID` property. + You can use this to control the grouping of the taskbar. See Microsoft Docs for more information. + https://docs.microsoft.com/en-us/windows/win32/shell/appids + + #### Improvements :zap: + - For better performance and future scalability, The internal interface has been changed from `WshShortcut` to `IShellLink`. + - Avoid positional parameters. + - Fix minor issues. + +### v1.3.8 + + Changed not to test for properties not explicitly specified. + +### v1.3.7 + + Fix PSSA issues. + + Remove unnecessary files in the published data. + +### v1.3.6 ++ Fixed issue that the Test-TargetResource always fails when the Target contains environment variables. #9 ++ Fixed issue that the Test-TargetResource may fails when the Icon is specified. + +### v1.3.4 ++ Fixed issue that the Test-TargetResource always fails when the HotKey is not specified. #8 ++ Improved verbose messages. + +### v1.3.1 ++ Change type of `HotKey` to `[string]` + +### v1.3.0 ++ Add `Description` property #1 ++ Add `HotKey` property #2 ++ Add `Icon` property #3 diff --git a/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/DSCResources/DSC_DnsServerADZone/DSC_DnsServerADZone.psm1 b/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/DSCResources/DSC_DnsServerADZone/DSC_DnsServerADZone.psm1 new file mode 100644 index 0000000..66294ef --- /dev/null +++ b/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/DSCResources/DSC_DnsServerADZone/DSC_DnsServerADZone.psm1 @@ -0,0 +1,340 @@ +$script:dscResourceCommonPath = Join-Path -Path $PSScriptRoot -ChildPath '..\..\Modules\DscResource.Common' +$script:dnsServerDscCommonPath = Join-Path -Path $PSScriptRoot -ChildPath '..\..\Modules\DnsServerDsc.Common' + +Import-Module -Name $script:dscResourceCommonPath +Import-Module -Name $script:dnsServerDscCommonPath + +$script:localizedData = Get-LocalizedData -DefaultUICulture 'en-US' + +function Get-TargetResource +{ + [CmdletBinding()] + [OutputType([System.Collections.Hashtable])] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $Name, + + [Parameter()] + [ValidateSet('None','NonSecureAndSecure','Secure')] + [System.String] + $DynamicUpdate = 'Secure', + + [Parameter(Mandatory = $true)] + [ValidateSet('Custom','Domain','Forest','Legacy')] + [System.String] + $ReplicationScope, + + [Parameter()] + [System.String] + $DirectoryPartitionName, + + [Parameter()] + [System.String] + $ComputerName, + + [Parameter()] + [System.Management.Automation.PSCredential] + $Credential, + + [Parameter()] + [ValidateSet('Present','Absent')] + [System.String] + $Ensure = 'Present' + ) + Assert-Module -ModuleName 'DNSServer' + Write-Verbose ($script:localizedData.CheckingZoneMessage -f $Name, $Ensure) + + if (!$PSBoundParameters.ContainsKey('ComputerName') -and $PSBoundParameters.ContainsKey('Credential')) + { + throw $script:localizedData.CredentialRequiresComputerNameMessage + } + + $getParams = @{ + Name = $Name + ErrorAction = 'SilentlyContinue' + } + + if ($PSBoundParameters.ContainsKey('ComputerName')) + { + $cimSessionParams = @{ + ErrorAction = 'SilentlyContinue' + ComputerName = $ComputerName + } + if ($PSBoundParameters.ContainsKey('Credential')) + { + $cimSessionParams += @{ + Credential = $Credential + } + } + $getParams += @{ + CimSession = (New-CimSession @cimSessionParams) + } + } + + $dnsServerZone = Get-DnsServerZone @getParams + if ($getParams.CimSession) + { + Remove-CimSession -CimSession $getParams.CimSession + } + $targetResource = @{ + Name = $dnsServerZone.ZoneName + DynamicUpdate = $dnsServerZone.DynamicUpdate + ReplicationScope = $dnsServerZone.ReplicationScope + DirectoryPartitionName = $dnsServerZone.DirectoryPartitionName + Ensure = if ($null -eq $dnsServerZone) { 'Absent' } else { 'Present' } + } + return $targetResource +} #end function Get-TargetResource + +function Test-TargetResource +{ + [CmdletBinding()] + [OutputType([System.Boolean])] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $Name, + + [Parameter()] + [ValidateSet('None','NonSecureAndSecure','Secure')] + [System.String] + $DynamicUpdate = 'Secure', + + [Parameter(Mandatory = $true)] + [ValidateSet('Custom','Domain','Forest','Legacy')] + [System.String] + $ReplicationScope, + + [Parameter()] + [System.String] + $DirectoryPartitionName, + + [Parameter()] + [System.String] + $ComputerName, + + [Parameter()] + [System.Management.Automation.PSCredential] + $Credential, + + [Parameter()] + [ValidateSet('Present','Absent')] + [System.String] + $Ensure = 'Present' + ) + + $targetResource = Get-TargetResource @PSBoundParameters + + $targetResourceInCompliance = $true + + if ($Ensure -eq 'Present') + { + if ($targetResource.Ensure -eq 'Present') + { + if ($targetResource.DynamicUpdate -ne $DynamicUpdate) + { + Write-Verbose ($script:localizedData.NotDesiredPropertyMessage -f ` + 'DynamicUpdate', $DynamicUpdate, $targetResource.DynamicUpdate) + + $targetResourceInCompliance = $false + } + + if ($targetResource.ReplicationScope -ne $ReplicationScope) + { + Write-Verbose ($script:localizedData.NotDesiredPropertyMessage -f ` + 'ReplicationScope', $ReplicationScope, $targetResource.ReplicationScope) + + $targetResourceInCompliance = $false + } + + if ($DirectoryPartitionName -and $targetResource.DirectoryPartitionName -ne $DirectoryPartitionName) + { + Write-Verbose ($script:localizedData.NotDesiredPropertyMessage -f ` + 'DirectoryPartitionName', $DirectoryPartitionName, $targetResource.DirectoryPartitionName) + + $targetResourceInCompliance = $false + } + } + else + { + # Dns zone is present and needs removing + Write-Verbose ($script:localizedData.NotDesiredPropertyMessage -f 'Ensure', 'Present', 'Absent') + + $targetResourceInCompliance = $false + } + } + else + { + if ($targetResource.Ensure -eq 'Present') + { + ## Dns zone is absent and should be present + Write-Verbose ($script:localizedData.NotDesiredPropertyMessage -f 'Ensure', 'Absent', 'Present') + + $targetResourceInCompliance = $false + } + } + + return $targetResourceInCompliance +} #end function Test-TargetResource + +function Set-TargetResource +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $Name, + + [Parameter()] + [ValidateSet('None','NonSecureAndSecure','Secure')] + [System.String] + $DynamicUpdate = 'Secure', + + [Parameter(Mandatory = $true)] + [ValidateSet('Custom','Domain','Forest','Legacy')] + [System.String] + $ReplicationScope, + + [Parameter()] + [System.String] + $DirectoryPartitionName, + + [Parameter()] + [System.String] + $ComputerName, + + [Parameter()] + [System.Management.Automation.PSCredential] + $Credential, + + [Parameter()] + [ValidateSet('Present','Absent')] + [System.String] + $Ensure = 'Present' + ) + + Assert-Module -ModuleName 'DNSServer' + + $targetResource = Get-TargetResource @PSBoundParameters + + $params = @{ + Name = $Name + } + + if ($PSBoundParameters.ContainsKey('ComputerName')) + { + $cimSessionParams = @{ + ErrorAction = 'SilentlyContinue' + ComputerName = $ComputerName + } + + if ($PSBoundParameters.ContainsKey('Credential')) + { + $cimSessionParams += @{ + Credential = $Credential + } + } + + $params += @{ + CimSession = (New-CimSession @cimSessionParams) + } + } + + if ($Ensure -eq 'Present') + { + if ($targetResource.Ensure -eq 'Present') + { + ## Update the existing zone + if ($targetResource.DynamicUpdate -ne $DynamicUpdate) + { + $params += @{ + DynamicUpdate = $DynamicUpdate + } + + Write-Verbose ($script:localizedData.SetPropertyMessage -f 'DynamicUpdate') + } + + if ($targetResource.ReplicationScope -ne $ReplicationScope) + { + $params += @{ + ReplicationScope = $ReplicationScope + } + + Write-Verbose ($LocalizedData.SetPropertyMessage -f 'ReplicationScope') + } + + if ($DirectoryPartitionName -and $targetResource.DirectoryPartitionName -ne $DirectoryPartitionName) + { + if ($ReplicationScope -ne 'Custom') + { + # ReplicationScope must be 'Custom' if a DirectoryPartitionName is specified + $errorMessage = $script:localizedData.DirectoryPartitionReplicationScopeError + + New-InvalidArgumentException -ArgumentName 'ReplicationScope' -Message $errorMessage + } + + # ReplicationScope is a required parameter if DirectoryPartitionName is specified + if ($params.keys -notcontains 'ReplicationScope') + { + $params += @{ + ReplicationScope = $ReplicationScope + } + } + + $params += @{ + DirectoryPartitionName = $DirectoryPartitionName + } + + Write-Verbose ($script:localizedData.SetPropertyMessage -f 'DirectoryPartitionName') + } + + Set-DnsServerPrimaryZone @params + } + elseif ($targetResource.Ensure -eq 'Absent') + { + # Create the zone + Write-Verbose ($script:localizedData.AddingZoneMessage -f $targetResource.Name) + + $params += @{ + DynamicUpdate = $DynamicUpdate + ReplicationScope = $ReplicationScope + } + + if ($DirectoryPartitionName) + { + if ($ReplicationScope -ne 'Custom') + { + # ReplicationScope must be 'Custom' if a DirectoryPartitionName is specified + $errorMessage = $script:localizedData.DirectoryPartitionReplicationScopeError + + New-InvalidArgumentException -ArgumentName 'ReplicationScope' -Message $errorMessage + } + + $params += @{ + DirectoryPartitionName = $DirectoryPartitionName + } + } + + Add-DnsServerPrimaryZone @params + } + } + elseif ($Ensure -eq 'Absent') + { + # Remove the DNS Server zone + Write-Verbose ($script:localizedData.RemovingZoneMessage -f $targetResource.Name) + + Remove-DnsServerZone @params -Force + } + + if ($params.CimSession) + { + Remove-CimSession -CimSession $params.CimSession + } +} #end function Set-TargetResource diff --git a/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/DSCResources/DSC_DnsServerADZone/DSC_DnsServerADZone.schema.mof b/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/DSCResources/DSC_DnsServerADZone/DSC_DnsServerADZone.schema.mof new file mode 100644 index 0000000..c80ae9e --- /dev/null +++ b/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/DSCResources/DSC_DnsServerADZone/DSC_DnsServerADZone.schema.mof @@ -0,0 +1,11 @@ +[ClassVersion("1.0.0.0"), FriendlyName("DnsServerADZone")] +class DSC_DnsServerADZone : OMI_BaseResource +{ + [Key, Description("Name of the AD DNS zone")] String Name; + [Write, Description("AD zone dynamic DNS update option. Defaults to `'Secure'`."), ValueMap{"None","NonSecureAndSecure","Secure"}, Values{"None","NonSecureAndSecure","Secure"}] String DynamicUpdate; + [Required, Description("AD zone replication scope option."), ValueMap{"Custom","Domain","Forest","Legacy"}, Values{"Custom","Domain","Forest","Legacy"}] String ReplicationScope; + [Write, Description("Name of the directory partition on which to store the zone. Use this parameter when the ReplicationScope parameter has a value of Custom.")] String DirectoryPartitionName; + [Write, Description("Specifies a DNS server. If you do not specify this parameter, the command runs on the local system.")] String ComputerName; + [Write, Description("Specifies the credential to use to create the AD zone on a remote computer. This parameter can only be used when you also are passing a value for the `ComputerName` parameter."), EmbeddedInstance("MSFT_Credential")] String Credential; + [Write, Description("Whether the DNS zone should be available or removed"), ValueMap{"Present","Absent"}, Values{"Present","Absent"}] String Ensure; +}; diff --git a/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/DSCResources/DSC_DnsServerADZone/README.md b/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/DSCResources/DSC_DnsServerADZone/README.md new file mode 100644 index 0000000..ce06877 --- /dev/null +++ b/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/DSCResources/DSC_DnsServerADZone/README.md @@ -0,0 +1,3 @@ +# Description + +The DnsServerADZone DSC resource manages an AD integrated zone on a Domain Name System (DNS) server. diff --git a/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/DSCResources/DSC_DnsServerADZone/en-US/DSC_DnsServerADZone.strings.psd1 b/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/DSCResources/DSC_DnsServerADZone/en-US/DSC_DnsServerADZone.strings.psd1 new file mode 100644 index 0000000..0bdcaf2 --- /dev/null +++ b/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/DSCResources/DSC_DnsServerADZone/en-US/DSC_DnsServerADZone.strings.psd1 @@ -0,0 +1,10 @@ +# culture="en-US" +ConvertFrom-StringData @' + CheckingZoneMessage = Checking DNS server zone with name '{0}' is '{1}'... + AddingZoneMessage = Adding DNS server zone '{0}' ... + RemovingZoneMessage = Removing DNS server zone '{0}' ... + NotDesiredPropertyMessage = DNS server zone property '{0}' is not correct. Expected '{1}', actual '{2}' + SetPropertyMessage = DNS server zone property '{0}' is set + CredentialRequiresComputerNameMessage = The Credentials Parameter can only be used when ComputerName is also specified. + DirectoryPartitionReplicationScopeError = A Directory Partition can only be specified when the Replication Scope is set to 'Custom' +'@ diff --git a/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/DSCResources/DSC_DnsServerADZone/en-US/about_DnsServerADZone.help.txt b/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/DSCResources/DSC_DnsServerADZone/en-US/about_DnsServerADZone.help.txt new file mode 100644 index 0000000..d438bad --- /dev/null +++ b/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/DSCResources/DSC_DnsServerADZone/en-US/about_DnsServerADZone.help.txt @@ -0,0 +1,86 @@ +.NAME + DnsServerADZone + +.DESCRIPTION + The DnsServerADZone DSC resource manages an AD integrated zone on a Domain Name System (DNS) server. + +.PARAMETER Name + Key - String + Name of the AD DNS zone + +.PARAMETER DynamicUpdate + Write - String + Allowed values: None, NonSecureAndSecure, Secure + AD zone dynamic DNS update option. Defaults to 'Secure'. + +.PARAMETER ReplicationScope + Required - String + Allowed values: Custom, Domain, Forest, Legacy + AD zone replication scope option. + +.PARAMETER DirectoryPartitionName + Write - String + Name of the directory partition on which to store the zone. Use this parameter when the ReplicationScope parameter has a value of Custom. + +.PARAMETER ComputerName + Write - String + Specifies a DNS server. If you do not specify this parameter, the command runs on the local system. + +.PARAMETER Credential + Write - Instance + Specifies the credential to use to create the AD zone on a remote computer. This parameter can only be used when you also are passing a value for the ComputerName parameter. + +.PARAMETER Ensure + Write - String + Allowed values: Present, Absent + Whether the DNS zone should be available or removed + +.EXAMPLE 1 + +This configuration will manage an AD integrated DNS forward lookup zone + +Configuration DnsServerADZone_forward_config +{ + param + ( + [System.Management.Automation.PSCredential] + $Credential + ) + + Import-DscResource -ModuleName 'DnsServerDsc' + + Node localhost + { + + DnsServerADZone 'AddForwardADZone' + { + Name = 'MyDomainName.com' + DynamicUpdate = 'Secure' + ReplicationScope = 'Forest' + ComputerName = 'MyDnsServer.MyDomain.com' + Credential = $Credential + Ensure = 'Present' + } + } +} + +.EXAMPLE 2 + +This configuration will manage an AD integrated DNS reverse lookup zone + +Configuration DnsServerADZone_reverse_config +{ + Import-DscResource -ModuleName 'DnsServerDsc' + + Node localhost + { + DnsServerADZone 'addReverseADZone' + { + Name = '1.168.192.in-addr.arpa' + DynamicUpdate = 'Secure' + ReplicationScope = 'Forest' + Ensure = 'Present' + } + } +} + diff --git a/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/DSCResources/DSC_DnsServerClientSubnet/DSC_DnsServerClientSubnet.psm1 b/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/DSCResources/DSC_DnsServerClientSubnet/DSC_DnsServerClientSubnet.psm1 new file mode 100644 index 0000000..96cc5dd --- /dev/null +++ b/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/DSCResources/DSC_DnsServerClientSubnet/DSC_DnsServerClientSubnet.psm1 @@ -0,0 +1,238 @@ +$script:dscResourceCommonPath = Join-Path -Path $PSScriptRoot -ChildPath '..\..\Modules\DscResource.Common' +$script:dnsServerDscCommonPath = Join-Path -Path $PSScriptRoot -ChildPath '..\..\Modules\DnsServerDsc.Common' + +Import-Module -Name $script:dscResourceCommonPath +Import-Module -Name $script:dnsServerDscCommonPath + +$script:localizedData = Get-LocalizedData -DefaultUICulture 'en-US' + +<# + .SYNOPSIS + This will return the current state of the resource. + + .PARAMETER Name + Specifies the name of the client subnet. + + .PARAMETER IPv4Subnet + Specify an array (1 or more values) of IPv4 Subnet addresses in CIDR Notation. + + .PARAMETER IPv6Subnet + Specify an array (1 or more values) of IPv6 Subnet addresses in CIDR Notation. +#> +function Get-TargetResource +{ + [CmdletBinding()] + [OutputType([System.Collections.Hashtable])] + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $Name + + ) + + Write-Verbose -Message ($script:localizedData.GettingDnsServerClientSubnetMessage -f $Name) + $record = Get-DnsServerClientSubnet -Name $Name -ErrorAction SilentlyContinue + + if ($null -eq $record) + { + return @{ + Name = $Name + IPv4Subnet = $null + IPv6Subnet = $null + Ensure = 'Absent' + } + } + + return @{ + Name = $record.Name + IPv4Subnet = $record.IPv4Subnet + IPv6Subnet = $record.IPv6Subnet + Ensure = 'Present' + } +} #end function Get-TargetResource + +<# + .SYNOPSIS + This will configure the resource. + + .PARAMETER Name + Specifies the name of the client subnet. + + .PARAMETER IPv4Subnet + Specify an array (1 or more values) of IPv4 Subnet addresses in CIDR Notation. + + .PARAMETER IPv6Subnet + Specify an array (1 or more values) of IPv6 Subnet addresses in CIDR Notation. +#> +function Set-TargetResource +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $Name, + + [Parameter()] + [System.String[]] + $IPv4Subnet, + + [Parameter()] + [System.String[]] + $IPv6Subnet, + + [Parameter()] + [ValidateSet('Present','Absent')] + [System.String] + $Ensure = 'Present' + ) + + $dnsServerClientSubnetParameters = @{ + Name = $Name + } + $clientSubnet = Get-DnsServerClientSubnet -Name $Name -ErrorAction SilentlyContinue + if ($Ensure -eq 'Present') + { + if ($IPv4Subnet) + { + $dnsServerClientSubnetParameters.Add('IPv4Subnet',$IPv4Subnet) + } + if ($IPv6Subnet) + { + $dnsServerClientSubnetParameters.Add('IPv6Subnet',$IPv6Subnet) + } + + if ($clientSubnet) + { + $dnsServerClientSubnetParameters.Add('Action', "REPLACE") + Write-Verbose -Message ($script:localizedData.UpdatingDnsServerClientSubnetMessage -f ` + $Name, "$IPv4Subnet", "$IPv6Subnet") + Set-DnsServerClientSubnet @dnsServerClientSubnetParameters + } + else + { + Write-Verbose -Message ($script:localizedData.CreatingDnsServerClientSubnetMessage -f ` + $Name, "$IPv4Subnet", "$IPv6Subnet") + Add-DnsServerClientSubnet @dnsServerClientSubnetParameters + } + } + elseif ($Ensure -eq 'Absent') + { + Write-Verbose -Message ($script:localizedData.RemovingDnsServerClientSubnetMessage -f $Name) + Remove-DnsServerClientSubnet -Name $Name + } +} #end function Set-TargetResource + +<# + .SYNOPSIS + This will return whether the resource is in desired state. + + .PARAMETER Name + Specifies the name of the client subnet. + + .PARAMETER IPv4Subnet + Specify an array (1 or more values) of IPv4 Subnet addresses in CIDR Notation. + + .PARAMETER IPv6Subnet + Specify an array (1 or more values) of IPv6 Subnet addresses in CIDR Notation. +#> +function Test-TargetResource +{ + [CmdletBinding()] + [OutputType([System.Boolean])] + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $Name, + + [Parameter()] + [System.String[]] + $IPv4Subnet, + + [Parameter()] + [System.String[]] + $IPv6Subnet, + + [Parameter()] + [ValidateSet('Present','Absent')] + [System.String] + $Ensure = 'Present' + ) + + $result = Get-TargetResource -Name $Name + + if ($Ensure -ne $result.Ensure) + { + Write-Verbose -Message ($script:localizedData.NotDesiredPropertyMessage -f ` + 'Ensure', $Ensure, $result.Ensure) + Write-Verbose -Message ($script:localizedData.NotInDesiredStateMessage -f $Name) + return $false + } + elseif ($Ensure -eq 'Present') + { + $IPv4SubnetResult = $result.IPv4Subnet + $IPv6SubnetResult = $result.IPv6Subnet + + if (($null -eq $IPv4Subnet) -and ($null -ne $IPv4SubnetResult)) + { + Write-Verbose -Message ($script:localizedData.NotDesiredPropertyMessage -f ` + 'IPv4Subnet', "$IPv4Subnet", "$IPv4SubnetResult") + Write-Verbose -Message ($script:localizedData.NotInDesiredStateMessage -f $Name) + return $false + } + + if (($null -eq $IPv4SubnetResult) -and ($null -ne $IPv4Subnet)) + { + Write-Verbose -Message ($script:localizedData.NotDesiredPropertyMessage -f ` + 'IPv4Subnet', "$IPv4Subnet", "$IPv4SubnetResult") + Write-Verbose -Message ($script:localizedData.NotInDesiredStateMessage -f $Name) + return $false + } + + if ($IPv4Subnet) + { + $IPv4Difference = Compare-Object -ReferenceObject $IPv4Subnet -DifferenceObject $IPv4SubnetResult + if ($IPv4Difference) + { + Write-Verbose -Message ($script:localizedData.NotDesiredPropertyMessage -f ` + 'IPv4Subnet', "$IPv4Subnet", "$IPv4SubnetResult") + Write-Verbose -Message ($script:localizedData.NotInDesiredStateMessage -f $Name) + return $false + } + } + + if (($null -eq $IPv6Subnet) -and ($null -ne $IPv6SubnetResult)) + { + Write-Verbose -Message ($script:localizedData.NotDesiredPropertyMessage -f ` + 'IPv6Subnet', "$IPv6Subnet", "$IPv6SubnetResult") + Write-Verbose -Message ($script:localizedData.NotInDesiredStateMessage -f $Name) + return $false + } + + if (($null -eq $IPv6SubnetResult) -and ($null -ne $IPv6Subnet)) + { + Write-Verbose -Message ($script:localizedData.NotDesiredPropertyMessage -f ` + 'IPv6Subnet', "$IPv6Subnet", "$IPv6SubnetResult") + Write-Verbose -Message ($script:localizedData.NotInDesiredStateMessage -f $Name) + return $false + } + + if ($IPv6Subnet) + { + $IPv6Difference = Compare-Object -ReferenceObject $IPv6Subnet -DifferenceObject $IPv6SubnetResult + if ($IPv6Difference) + { + Write-Verbose -Message ($script:localizedData.NotDesiredPropertyMessage -f ` + 'IPv6Subnet', "$IPv6Subnet", "$IPv6SubnetResult") + Write-Verbose -Message ($script:localizedData.NotInDesiredStateMessage -f $Name) + return $false + } + } + } + Write-Verbose -Message ($script:localizedData.InDesiredStateMessage -f $Name) + return $true +} #end function Test-TargetResource + +Export-ModuleMember -Function *-TargetResource diff --git a/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/DSCResources/DSC_DnsServerClientSubnet/DSC_DnsServerClientSubnet.schema.mof b/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/DSCResources/DSC_DnsServerClientSubnet/DSC_DnsServerClientSubnet.schema.mof new file mode 100644 index 0000000..ef4f9d1 --- /dev/null +++ b/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/DSCResources/DSC_DnsServerClientSubnet/DSC_DnsServerClientSubnet.schema.mof @@ -0,0 +1,8 @@ +[ClassVersion("1.0.0.0"), FriendlyName("DnsServerClientSubnet")] +class DSC_DnsServerClientSubnet : OMI_BaseResource +{ + [Key, Description("Specifies the name of the client subnet.")] string Name; + [Write, Description("Specify an array (1 or more values) of IPv4 Subnet addresses in CIDR Notation.")] string IPv4Subnet[]; + [Write, Description("Specify an array (1 or more values) of IPv6 Subnet addresses in CIDR Notation.")] string IPv6Subnet[]; + [Write, Description("Should this DNS server client subnet be present or absent"), ValueMap{"Present","Absent"}, Values{"Present","Absent"}] String Ensure; +}; diff --git a/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/DSCResources/DSC_DnsServerClientSubnet/README.md b/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/DSCResources/DSC_DnsServerClientSubnet/README.md new file mode 100644 index 0000000..57aa375 --- /dev/null +++ b/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/DSCResources/DSC_DnsServerClientSubnet/README.md @@ -0,0 +1,7 @@ +# Description + +The DnsServerClientSubnet DSC resource manages DNS Client Subnets on a Domain Name System (DNS) server. A client subnet is a group of IP subnets that represent a logical group, for example, a geographical area, a datacenter, or a trusted resolver fleet. You can use client subnets in criteria in DNS policies. Multiple DNS policies can refer to the same client subnet. + +## Requirements + +- Target machine must be running Windows Server 2016 or later. diff --git a/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/DSCResources/DSC_DnsServerClientSubnet/en-US/DSC_DnsServerClientSubnet.strings.psd1 b/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/DSCResources/DSC_DnsServerClientSubnet/en-US/DSC_DnsServerClientSubnet.strings.psd1 new file mode 100644 index 0000000..6da3c6d --- /dev/null +++ b/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/DSCResources/DSC_DnsServerClientSubnet/en-US/DSC_DnsServerClientSubnet.strings.psd1 @@ -0,0 +1,10 @@ +# culture="en-US" +ConvertFrom-StringData @' + GettingDnsServerClientSubnetMessage = Getting DNS Server Client Subnet '{0}'. + CreatingDnsServerClientSubnetMessage = Creating DNS Server Client Subnet '{0}' IPv4 '{1}' and/or IPv6 '{2}'. + UpdatingDnsServerClientSubnetMessage = Updating DNS Server Client Subnet '{0}' IPv4 '{1}' and/or IPv6 '{2}'. + RemovingDnsServerClientSubnetMessage = Removing DNS Server Client Subnet '{0}'. + NotDesiredPropertyMessage = DNS Server Client Subnet property '{0}' is not correct. Expected '{1}', actual '{2}' + InDesiredStateMessage = DNS Server Client Subnet '{0}' is in the desired state. + NotInDesiredStateMessage = DNS Server Client Subnet '{0}' is NOT in the desired state. +'@ diff --git a/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/DSCResources/DSC_DnsServerClientSubnet/en-US/about_DnsServerClientSubnet.help.txt b/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/DSCResources/DSC_DnsServerClientSubnet/en-US/about_DnsServerClientSubnet.help.txt new file mode 100644 index 0000000..cf2f829 --- /dev/null +++ b/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/DSCResources/DSC_DnsServerClientSubnet/en-US/about_DnsServerClientSubnet.help.txt @@ -0,0 +1,46 @@ +.NAME + DnsServerClientSubnet + +.DESCRIPTION + The DnsServerClientSubnet DSC resource manages DNS Client Subnets on a Domain Name System (DNS) server. A client subnet is a group of IP subnets that represent a logical group, for example, a geographical area, a datacenter, or a trusted resolver fleet. You can use client subnets in criteria in DNS policies. Multiple DNS policies can refer to the same client subnet. + + ## Requirements + + - Target machine must be running Windows Server 2016 or later. + +.PARAMETER Name + Key - String + Specifies the name of the client subnet. + +.PARAMETER IPv4Subnet + Write - StringArray + Specify an array (1 or more values) of IPv4 Subnet addresses in CIDR Notation. + +.PARAMETER IPv6Subnet + Write - StringArray + Specify an array (1 or more values) of IPv6 Subnet addresses in CIDR Notation. + +.PARAMETER Ensure + Write - String + Allowed values: Present, Absent + Should this DNS server client subnet be present or absent + +.EXAMPLE 1 + +This configuration will manage a DNS client subnet + +Configuration DnsServerClientSubnet_config +{ + Import-DscResource -ModuleName 'DnsServerDsc' + + Node localhost + { + DnsServerClientSubnet 'ClientSubnet1' + { + Name = 'London' + IPv4Subnet = @('10.1.0.0/16', '10.8.0.0/16') + Ensure = 'Present' + } + } +} + diff --git a/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/DSCResources/DSC_DnsServerConditionalForwarder/DSC_DnsServerConditionalForwarder.psm1 b/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/DSCResources/DSC_DnsServerConditionalForwarder/DSC_DnsServerConditionalForwarder.psm1 new file mode 100644 index 0000000..a01a01e --- /dev/null +++ b/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/DSCResources/DSC_DnsServerConditionalForwarder/DSC_DnsServerConditionalForwarder.psm1 @@ -0,0 +1,430 @@ +$script:dscResourceCommonPath = Join-Path -Path $PSScriptRoot -ChildPath '..\..\Modules\DscResource.Common' +$script:dnsServerDscCommonPath = Join-Path -Path $PSScriptRoot -ChildPath '..\..\Modules\DnsServerDsc.Common' + +Import-Module -Name $script:dscResourceCommonPath +Import-Module -Name $script:dnsServerDscCommonPath + +$script:localizedData = Get-LocalizedData -DefaultUICulture 'en-US' + +<# + .SYNOPSIS + Get the state of a conditional forwarder. + + .DESCRIPTION + DnsServerConditionalForwarder can be used to manage the state of a single conditional forwarder. + + .PARAMETER Ensure + Ensure whether the zone is absent or present. + + .PARAMETER Name + The name of the zone to manage. + + .PARAMETER MasterServers + The IP addresses the forwarder should use. Mandatory if Ensure is present. + + .PARAMETER ReplicationScope + Whether the conditional forwarder should be replicated in AD, and the scope of that replication. + + Valid values are: + + * None: (file based / not replicated) + * Custom: A user defined directory partition. DirectoryPartitionName is mandatory if Custom is set. + * Domain: DomainDnsZones + * Forest: ForestDnsZones + * Legacy: The domain partition (defaultNamingContext). + + .PARAMETER DirectoryPartitionName + The name of the directory partition to use when the ReplicationScope is Custom. This value is ignored for all other replication scopes. + +#> +function Get-TargetResource +{ + [CmdletBinding()] + [OutputType([Hashtable])] + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $Name + ) + + $targetResource = @{ + Ensure = 'Absent' + Name = $Name + MasterServers = $null + ReplicationScope = $null + DirectoryPartitionName = $null + ZoneType = $null + } + + $zone = Get-DnsServerZone -Name $Name -ErrorAction SilentlyContinue + if ($zone) + { + Write-Verbose ($script:localizedData.FoundZone -f @( + $zone.ZoneType + $Name + )) + + $targetResource.ZoneType = $zone.ZoneType + } + if ($zone -and $zone.ZoneType -eq 'Forwarder') + { + $targetResource.Ensure = 'Present' + $targetResource.MasterServers = $zone.MasterServers + + if ($zone.IsDsIntegrated) + { + $targetResource.ReplicationScope = $zone.ReplicationScope + $targetResource.DirectoryPartitionName = $zone.DirectoryPartitionName + } + else + { + $targetResource.ReplicationScope = 'None' + } + } + else + { + Write-Verbose ($script:localizedData.CouldNotFindZone -f $Name) + } + + $targetResource +} + +<# + .SYNOPSIS + Set the state of a conditional forwarder. + + .DESCRIPTION + DnsServerConditionalForwarder can be used to manage the state of a single conditional forwarder. + + .PARAMETER Ensure + Ensure whether the zone is absent or present. + + .PARAMETER Name + The name of the zone to manage. + + .PARAMETER MasterServers + The IP addresses the forwarder should use. Mandatory if Ensure is present. + + .PARAMETER ReplicationScope + Whether the conditional forwarder should be replicated in AD, and the scope of that replication. + + Valid values are: + + * None: (file based / not replicated) + * Custom: A user defined directory partition. DirectoryPartitionName is mandatory if Custom is set. + * Domain: DomainDnsZones + * Forest: ForestDnsZones + * Legacy: The domain partition (defaultNamingContext). + + .PARAMETER DirectoryPartitionName + The name of the directory partition to use when the ReplicationScope is Custom. This value is ignored for all other replication scopes. + +#> +function Set-TargetResource +{ + [CmdletBinding()] + param + ( + [Parameter()] + [ValidateSet('Absent', 'Present')] + [System.String] + $Ensure = 'Present', + + [Parameter(Mandatory = $true)] + [System.String] + $Name, + + [Parameter()] + [String[]] + $MasterServers, + + [Parameter()] + [ValidateSet('None', 'Custom', 'Domain', 'Forest', 'Legacy')] + [System.String] + $ReplicationScope = 'None', + + [Parameter()] + [System.String] + $DirectoryPartitionName + ) + + Test-DscDnsServerConditionalForwarderParameter + + $zone = Get-DnsServerZone -Name $Name -ErrorAction SilentlyContinue + if ($Ensure -eq 'Present') + { + $params = @{ + Name = $Name + MasterServers = $MasterServers + } + + if ($zone) + { + # File <--> DsIntegrated requires create and destroy + if ($zone.ZoneType -ne 'Forwarder' -or + ($zone.IsDsIntegrated -and $ReplicationScope -eq 'None') -or + (-not $zone.IsDsIntegrated -and $ReplicationScope -ne 'None')) + { + Remove-DnsServerZone -Name $Name + + Write-Verbose ($script:localizedData.RecreateZone -f @( + $zone.ZoneType + $Name + )) + + $zone = $null + } + else + { + if ("$($zone.MasterServers)" -ne "$MasterServers") + { + Write-Verbose ($script:localizedData.UpdatingMasterServers -f @( + $Name + ($MasterServers -join ', ') + )) + + $null = Set-DnsServerConditionalForwarderZone @params + } + } + } + + $params = @{ + Name = $Name + } + if ($ReplicationScope -ne 'None') + { + $params.ReplicationScope = $ReplicationScope + } + if ($ReplicationScope -eq 'Custom' -and + $DirectoryPartitionName -and + $zone.DirectoryPartitionName -ne $DirectoryPartitionName) + { + $params.ReplicationScope = 'Custom' + $params.DirectoryPartitionName = $DirectoryPartitionName + } + + if ($zone) + { + if (($params.ReplicationScope -and $params.ReplicationScope -ne $zone.ReplicationScope) -or $params.DirectoryPartitionName) + { + Write-Verbose ($script:localizedData.MoveADZone -f @( + $Name + $ReplicationScope + )) + + $null = Set-DnsServerConditionalForwarderZone @params + } + } + else + { + Write-Verbose ($script:localizedData.NewZone -f $Name) + + $params.MasterServers = $MasterServers + $null = Add-DnsServerConditionalForwarderZone @params + } + } + elseif ($Ensure -eq 'Absent') + { + if ($zone -and $zone.ZoneType -eq 'Forwarder') + { + Write-Verbose ($script:localizedData.RemoveZone -f $Name) + + Remove-DnsServerZone -Name $Name + } + } +} + +<# + .SYNOPSIS + Test the state of a conditional forwarder. + + .DESCRIPTION + DnsServerConditionalForwarder can be used to manage the state of a single conditional forwarder. + + .PARAMETER Ensure + Ensure whether the zone is absent or present. + + .PARAMETER Name + The name of the zone to manage. + + .PARAMETER MasterServers + The IP addresses the forwarder should use. Mandatory if Ensure is present. + + .PARAMETER ReplicationScope + Whether the conditional forwarder should be replicated in AD, and the scope of that replication. + + Valid values are: + + * None: (file based / not replicated) + * Custom: A user defined directory partition. DirectoryPartitionName is mandatory if Custom is set. + * Domain: DomainDnsZones + * Forest: ForestDnsZones + * Legacy: The domain partition (defaultNamingContext). + + .PARAMETER DirectoryPartitionName + The name of the directory partition to use when the ReplicationScope is Custom. This value is ignored for all other replication scopes. + +#> +function Test-TargetResource +{ + [CmdletBinding()] + [OutputType([Boolean])] + param + ( + [Parameter()] + [ValidateSet('Absent', 'Present')] + [System.String] + $Ensure = 'Present', + + [Parameter(Mandatory = $true)] + [System.String] + $Name, + + [Parameter()] + [String[]] + $MasterServers, + + [Parameter()] + [ValidateSet('None', 'Custom', 'Domain', 'Forest', 'Legacy')] + [System.String] + $ReplicationScope = 'None', + + [Parameter()] + [System.String] + $DirectoryPartitionName + ) + + Test-DscDnsServerConditionalForwarderParameter + + $zone = Get-DnsServerZone -Name $Name -ErrorAction SilentlyContinue + if ($Ensure -eq 'Present') + { + if (-not $zone) + { + Write-Verbose ($script:localizedData.ZoneDoesNotExist -f $Name) + + return $false + } + + if ($zone.ZoneType -ne 'Forwarder') + { + Write-Verbose ($script:localizedData.IncorrectZoneType -f @( + $Name + $zone.ZoneType + )) + + return $false + } + + if ($zone.IsDsIntegrated -and $ReplicationScope -eq 'None') + { + Write-Verbose ($script:localizedData.ZoneIsDsIntegrated -f $Name) + + return $false + } + + if (-not $zone.IsDsIntegrated -and $ReplicationScope -ne 'None') + { + Write-Verbose ($script:localizedData.ZoneIsFileBased -f $Name) + + return $false + } + + if ($ReplicationScope -ne 'None' -and $zone.ReplicationScope -ne $ReplicationScope) + { + Write-Verbose ($script:localizedData.ReplicationScopeDoesNotMatch -f @( + $Name + $zone.ReplicationScope + $ReplicationScope + )) + + return $false + } + + if ($ReplicationScope -eq 'Custom' -and $zone.DirectoryPartitionName -ne $DirectoryPartitionName) + { + Write-Verbose ($script:localizedData.DirectoryPartitionDoesNotMatch -f @( + $Name + $DirectoryPartitionName + )) + + return $false + } + + <# + Compares two joined arrays. Arrays are joined using the output field separator. + + If the elements are not in the same order the configuration is considered to be different. + + Equivalent to Compare-Object -SyncWindow 0 without the need for null checking of reference or difference. + + Element order is considered important as it affects name resolution. + + https://support.microsoft.com/en-us/help/2834250/net-dns-forwarders-and-conditional-forwarders-resolution-timeouts + #> + if ("$($zone.MasterServers)" -ne "$MasterServers") + { + Write-Verbose ($script:localizedData.MasterServersDoNotMatch -f @( + $Name + ($MasterServers -join ', ') + ($zone.MasterServers -join ', ') + )) + + return $false + } + } + elseif ($Ensure -eq 'Absent') + { + if ($zone -and $zone.ZoneType -eq 'Forwarder') + { + Write-Verbose ($script:localizedData.ZoneExists -f $Name) + + return $false + } + } + + return $true +} + +function Test-DscDnsServerConditionalForwarderParameter +{ + <# + .SYNOPSIS + Tests the parameter combinations required by this resource. + .DESCRIPTION + Tests the parameter combinations required by this resource. + #> + + [CmdletBinding()] + param () + + $invocationInfo = Get-Variable MyInvocation -Scope 1 -ValueOnly + + if (-not $invocationInfo.BoundParameters.ContainsKey('Ensure') -or $invocationInfo.BoundParameters['Ensure'] -eq 'Present') + { + if ($null -eq $invocationInfo.BoundParameters['MasterServers'] -or $invocationInfo.BoundParameters['MasterServers'].Count -eq 0) + { + $pscmdlet.ThrowTerminatingError(( + New-Object System.Management.Automation.ErrorRecord( + (New-Object System.ArgumentException($script:localizedData.MasterServersIsMandatory)), + 'MasterServersIsMandatory', + 'InvalidArgument', + $null + ) + )) + } + + if ($invocationInfo.BoundParameters['ReplicationScope'] -eq 'Custom' -and -not $invocationInfo.BoundParameters['DirectoryPartitionName']) + { + $pscmdlet.ThrowTerminatingError(( + New-Object System.Management.Automation.ErrorRecord( + (New-Object System.ArgumentException($script:localizedData.DirectoryPartitionNameIsMandatory)), + 'DirectoryPartitionNameIsMandatory', + 'InvalidArgument', + $null + ) + )) + } + } +} diff --git a/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/DSCResources/DSC_DnsServerConditionalForwarder/DSC_DnsServerConditionalForwarder.schema.mof b/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/DSCResources/DSC_DnsServerConditionalForwarder/DSC_DnsServerConditionalForwarder.schema.mof new file mode 100644 index 0000000..73bd3d4 --- /dev/null +++ b/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/DSCResources/DSC_DnsServerConditionalForwarder/DSC_DnsServerConditionalForwarder.schema.mof @@ -0,0 +1,10 @@ +[ClassVersion("1.0.0.0"), FriendlyName("DnsServerConditionalForwarder")] +class DSC_DnsServerConditionalForwarder : OMI_BaseResource +{ + [Write, Description("Ensure whether the zone is absent or present."), ValueMap{"Present", "Absent"}, Values{"Present", "Absent"}] String Ensure; + [Key, Description("The name of the zone to manage.")] String Name; + [Write, Description("The IP addresses the forwarder should use. Mandatory if Ensure is present.")] String MasterServers[]; + [Write, Description("Whether the conditional forwarder should be replicated in AD, and the scope of that replication. Default is `None`."), ValueMap{"None", "Custom", "Domain", "Forest", "Legacy"}, Values{"None", "Custom", "Domain", "Forest", "Legacy"}] String ReplicationScope; + [Write, Description("The name of the directory partition to use when the ReplicationScope is `Custom`. This value is ignored for all other replication scopes.")] String DirectoryPartitionName; + [Read, Description("The zone type")] String ZoneType; +}; diff --git a/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/DSCResources/DSC_DnsServerConditionalForwarder/README.md b/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/DSCResources/DSC_DnsServerConditionalForwarder/README.md new file mode 100644 index 0000000..7b861b0 --- /dev/null +++ b/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/DSCResources/DSC_DnsServerConditionalForwarder/README.md @@ -0,0 +1,5 @@ +# Description + +The DnsServerConditionalForwarder DSC resource manages a conditional forwarder on a Domain Name System (DNS) server. + +You can manage the master servers, forwarder time-out, recursion, recursion scope, and directory partition name for a conditional forwarder zone. diff --git a/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/DSCResources/DSC_DnsServerConditionalForwarder/en-US/DSC_DnsServerConditionalForwarder.strings.psd1 b/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/DSCResources/DSC_DnsServerConditionalForwarder/en-US/DSC_DnsServerConditionalForwarder.strings.psd1 new file mode 100644 index 0000000..e5e76b0 --- /dev/null +++ b/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/DSCResources/DSC_DnsServerConditionalForwarder/en-US/DSC_DnsServerConditionalForwarder.strings.psd1 @@ -0,0 +1,20 @@ +# culture="en-US" +ConvertFrom-StringData @' + FoundZone = Found a {0} zone named {1}. + CouldNotFindZone = Unable to find a zone named {0}. + RecreateZone = The {0} zone {1} was removed pending recreation. The existing zone type or replication scope cannot be converted. + UpdatingMasterServers = The list of servers for the conditional forwarder, {0}, was updated to {1}. + MoveADZone = The conditional forwarder, {0}, was moved to {1} replication scope. + NewZone = The conditional forwarder, {0}, was created. + RemoveZone = The conditional forwarder, {0}, was removed. + ZoneDoesNotExist = The zone, {0}, does not exist. + IncorrectZoneType = The zone {0} is {1}. Expected forwarder. + ZoneIsDsIntegrated = The zone {0} is AD Integrated. Expected file. + ZoneIsFileBased = The zone {0} is file based. Expected AD Integrated. + ReplicationScopeDoesNotMatch = The zone {0} has replication scope {1}. Expected replication scope {2}. + DirectoryPartitionDoesNotMatch = The zone {0} is not stored in the partition {1}. + MasterServersDoNotMatch = Expected master servers for the zone {0} to be {1}. Found {2}. + ZoneExists = The zone, {0}, exists. + MasterServersIsMandatory = The MasterServers parameter is mandatory when ensuring a zone is present. + DirectoryPartitionNameIsMandatory = A DirectoryPartitionName is mandatory when the replication scope is custom. +'@ diff --git a/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/DSCResources/DSC_DnsServerConditionalForwarder/en-US/about_DnsServerConditionalForwarder.help.txt b/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/DSCResources/DSC_DnsServerConditionalForwarder/en-US/about_DnsServerConditionalForwarder.help.txt new file mode 100644 index 0000000..95548db --- /dev/null +++ b/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/DSCResources/DSC_DnsServerConditionalForwarder/en-US/about_DnsServerConditionalForwarder.help.txt @@ -0,0 +1,54 @@ +.NAME + DnsServerConditionalForwarder + +.DESCRIPTION + The DnsServerConditionalForwarder DSC resource manages a conditional forwarder on a Domain Name System (DNS) server. + + You can manage the master servers, forwarder time-out, recursion, recursion scope, and directory partition name for a conditional forwarder zone. + +.PARAMETER Ensure + Write - String + Allowed values: Present, Absent + Ensure whether the zone is absent or present. + +.PARAMETER Name + Key - String + The name of the zone to manage. + +.PARAMETER MasterServers + Write - StringArray + The IP addresses the forwarder should use. Mandatory if Ensure is present. + +.PARAMETER ReplicationScope + Write - String + Allowed values: None, Custom, Domain, Forest, Legacy + Whether the conditional forwarder should be replicated in AD, and the scope of that replication. Default is None. + +.PARAMETER DirectoryPartitionName + Write - String + The name of the directory partition to use when the ReplicationScope is Custom. This value is ignored for all other replication scopes. + +.PARAMETER ZoneType + Read - String + The zone type + +.EXAMPLE 1 + +This configuration will manage a DNS server conditional forwarder + +Configuration DnsServerConditionalForwarder_config +{ + Import-DscResource -ModuleName 'DnsServerDsc' + + Node localhost + { + DnsServerConditionalForwarder 'Forwarder1' + { + Name = 'London' + MasterServers = @('10.0.1.10', '10.0.2.10') + ReplicationScope = 'Forest' + Ensure = 'Present' + } + } +} + diff --git a/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/DSCResources/DSC_DnsServerDiagnostics/DSC_DnsServerDiagnostics.psm1 b/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/DSCResources/DSC_DnsServerDiagnostics/DSC_DnsServerDiagnostics.psm1 new file mode 100644 index 0000000..e7da493 --- /dev/null +++ b/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/DSCResources/DSC_DnsServerDiagnostics/DSC_DnsServerDiagnostics.psm1 @@ -0,0 +1,533 @@ +$script:dscResourceCommonPath = Join-Path -Path $PSScriptRoot -ChildPath '..\..\Modules\DscResource.Common' +$script:dnsServerDscCommonPath = Join-Path -Path $PSScriptRoot -ChildPath '..\..\Modules\DnsServerDsc.Common' + +Import-Module -Name $script:dscResourceCommonPath +Import-Module -Name $script:dnsServerDscCommonPath + +$script:localizedData = Get-LocalizedData -DefaultUICulture 'en-US' + +<# + .SYNOPSIS + This will return a hashtable of results about DNS Diagnostics + + .PARAMETER DnsServer + Specifies the DNS server to connect to, or use 'localhost' for the current + node. +#> +function Get-TargetResource +{ + [CmdletBinding()] + [OutputType([System.Collections.Hashtable])] + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $DnsServer + ) + + Assert-Module -ModuleName 'DnsServer' + + Write-Verbose -Message $script:localizedData.GettingDnsServerDiagnosticsMessage + + $getDnsServerDiagnosticsParameters = @{ + ErrorAction = 'Stop' + } + + if ($DnsServer -ne 'localhost') + { + $getDnsServerDiagnosticsParameters['ComputerName'] = $DnsServer + } + + $dnsServerDiagnostics = Get-DnsServerDiagnostics @getDnsServerDiagnosticsParameters + + $returnValue = @{ + DnsServer = $DnsServer + Answers = $dnsServerDiagnostics.Answers + EnableLogFileRollover = $dnsServerDiagnostics.EnableLogFileRollover + EnableLoggingForLocalLookupEvent = $dnsServerDiagnostics.EnableLoggingForLocalLookupEvent + EnableLoggingForPluginDllEvent = $dnsServerDiagnostics.EnableLoggingForPluginDllEvent + EnableLoggingForRecursiveLookupEvent = $dnsServerDiagnostics.EnableLoggingForRecursiveLookupEvent + EnableLoggingForRemoteServerEvent = $dnsServerDiagnostics.EnableLoggingForRemoteServerEvent + EnableLoggingForServerStartStopEvent = $dnsServerDiagnostics.EnableLoggingForServerStartStopEvent + EnableLoggingForTombstoneEvent = $dnsServerDiagnostics.EnableLoggingForTombstoneEvent + EnableLoggingForZoneDataWriteEvent = $dnsServerDiagnostics.EnableLoggingForZoneDataWriteEvent + EnableLoggingForZoneLoadingEvent = $dnsServerDiagnostics.EnableLoggingForZoneLoadingEvent + EnableLoggingToFile = $dnsServerDiagnostics.EnableLoggingToFile + EventLogLevel = $dnsServerDiagnostics.EventLogLevel + FilterIPAddressList = $dnsServerDiagnostics.FilterIPAddressList + FullPackets = $dnsServerDiagnostics.FullPackets + LogFilePath = $dnsServerDiagnostics.LogFilePath + MaxMBFileSize = $dnsServerDiagnostics.MaxMBFileSize + Notifications = $dnsServerDiagnostics.Notifications + Queries = $dnsServerDiagnostics.Queries + QuestionTransactions = $dnsServerDiagnostics.QuestionTransactions + ReceivePackets = $dnsServerDiagnostics.ReceivePackets + SaveLogsToPersistentStorage = $dnsServerDiagnostics.SaveLogsToPersistentStorage + SendPackets = $dnsServerDiagnostics.SendPackets + TcpPackets = $dnsServerDiagnostics.TcpPackets + UdpPackets = $dnsServerDiagnostics.UdpPackets + UnmatchedResponse = $dnsServerDiagnostics.UnmatchedResponse + Update = $dnsServerDiagnostics.Update + UseSystemEventLog = $dnsServerDiagnostics.UseSystemEventLog + WriteThrough = $dnsServerDiagnostics.WriteThrough + } + + return $returnValue +} + +<# + .SYNOPSIS + This will set the desired state + + .PARAMETER DnsServer + Specifies the DNS server to connect to, or use 'localhost' for the current + node. + + .PARAMETER Answers + Specifies whether to enable the logging of DNS responses. + + .PARAMETER EnableLogFileRollover + Specifies whether to enable log file rollover. + + .PARAMETER EnableLoggingForLocalLookupEvent + Specifies whether the DNS server logs local lookup events. + + .PARAMETER EnableLoggingForPluginDllEvent + Specifies whether the DNS server logs dynamic link library (DLL) plug-in events. + + .PARAMETER EnableLoggingForRecursiveLookupEvent + Specifies whether the DNS server logs recursive lookup events. + + .PARAMETER EnableLoggingForRemoteServerEvent + Specifies whether the DNS server logs remote server events. + + .PARAMETER EnableLoggingForServerStartStopEvent + Specifies whether the DNS server logs server start and stop events. + + .PARAMETER EnableLoggingForTombstoneEvent + Specifies whether the DNS server logs tombstone events. + + .PARAMETER EnableLoggingForZoneDataWriteEvent + Specifies Controls whether the DNS server logs zone data write events. + + .PARAMETER EnableLoggingForZoneLoadingEvent + Specifies whether the DNS server logs zone load events. + + .PARAMETER EnableLoggingToFile + Specifies whether the DNS server logs logging-to-file. + + .PARAMETER EventLogLevel + Specifies an event log level. Valid values are Warning, Error, and None. + + .PARAMETER FilterIPAddressList + Specifies an array of IP addresses to filter. When you enable logging, traffic to and from these IP addresses is logged. If you do not specify any IP addresses, traffic to and from all IP addresses is logged. + + .PARAMETER FullPackets + Specifies whether the DNS server logs full packets. + + .PARAMETER LogFilePath + Specifies a log file path. + + .PARAMETER MaxMBFileSize + Specifies the maximum size of the log file. This parameter is relevant if you set EnableLogFileRollover and EnableLoggingToFile to $True. + + .PARAMETER Notifications + Specifies whether the DNS server logs notifications. + + .PARAMETER Queries + Specifies whether the DNS server allows query packet exchanges to pass through the content filter, such as the IPFilterList parameter. + + .PARAMETER QuestionTransactions + Specifies whether the DNS server logs queries. + + .PARAMETER ReceivePackets + Specifies whether the DNS server logs receive packets. + + .PARAMETER SaveLogsToPersistentStorage + Specifies whether the DNS server saves logs to persistent storage. + + .PARAMETER SendPackets + Specifies whether the DNS server logs send packets. + + .PARAMETER TcpPackets + Specifies whether the DNS server logs TCP packets. + + .PARAMETER UdpPackets + Specifies whether the DNS server logs UDP packets. + + .PARAMETER UnmatchedResponse + Specifies whether the DNS server logs unmatched responses. + + .PARAMETER Update + Specifies whether the DNS server logs updates. + + .PARAMETER UseSystemEventLog + Specifies whether the DNS server uses the system event log for logging. + + .PARAMETER WriteThrough + Specifies whether the DNS server logs write-throughs. +#> +function Set-TargetResource +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $DnsServer, + + [Parameter()] + [Boolean] + $Answers, + + [Parameter()] + [Boolean] + $EnableLogFileRollover, + + [Parameter()] + [Boolean] + $EnableLoggingForLocalLookupEvent, + + [Parameter()] + [Boolean] + $EnableLoggingForPluginDllEvent, + + [Parameter()] + [Boolean] + $EnableLoggingForRecursiveLookupEvent, + + [Parameter()] + [Boolean] + $EnableLoggingForRemoteServerEvent, + + [Parameter()] + [Boolean] + $EnableLoggingForServerStartStopEvent, + + [Parameter()] + [Boolean] + $EnableLoggingForTombstoneEvent, + + [Parameter()] + [Boolean] + $EnableLoggingForZoneDataWriteEvent, + + [Parameter()] + [Boolean] + $EnableLoggingForZoneLoadingEvent, + + [Parameter()] + [Boolean] + $EnableLoggingToFile, + + [Parameter()] + [UInt32] + $EventLogLevel, + + [Parameter()] + [String[]] + $FilterIPAddressList, + + [Parameter()] + [Boolean] + $FullPackets, + + [Parameter()] + [System.String] + $LogFilePath, + + [Parameter()] + [UInt32] + $MaxMBFileSize, + + [Parameter()] + [Boolean] + $Notifications, + + [Parameter()] + [Boolean] + $Queries, + + [Parameter()] + [Boolean] + $QuestionTransactions, + + [Parameter()] + [Boolean] + $ReceivePackets, + + [Parameter()] + [Boolean] + $SaveLogsToPersistentStorage, + + [Parameter()] + [Boolean] + $SendPackets, + + [Parameter()] + [Boolean] + $TcpPackets, + + [Parameter()] + [Boolean] + $UdpPackets, + + [Parameter()] + [Boolean] + $UnmatchedResponse, + + [Parameter()] + [Boolean] + $Update, + + [Parameter()] + [Boolean] + $UseSystemEventLog, + + [Parameter()] + [Boolean] + $WriteThrough + ) + + $PSBoundParameters.Remove('DnsServer') + + $setDnsServerDiagnosticsParameters = Remove-CommonParameter -Hashtable $PSBoundParameters + $setDnsServerDiagnosticsParameters['ErrorAction'] = 'Stop' + + if ($DnsServer -ne 'localhost') + { + $setDnsServerDiagnosticsParameters['ComputerName'] = $DnsServer + } + + Write-Verbose -Message $script:localizedData.SettingDnsServerDiagnosticsMessage + + Set-DnsServerDiagnostics @setDnsServerDiagnosticsParameters +} + +<# + .SYNOPSIS + This will set the desired state + + .PARAMETER DnsServer + Specifies the DNS server to connect to, or use 'localhost' for the current + node. + + .PARAMETER Answers + Specifies whether to enable the logging of DNS responses. + + .PARAMETER EnableLogFileRollover + Specifies whether to enable log file rollover. + + .PARAMETER EnableLoggingForLocalLookupEvent + Specifies whether the DNS server logs local lookup events. + + .PARAMETER EnableLoggingForPluginDllEvent + Specifies whether the DNS server logs dynamic link library (DLL) plug-in events. + + .PARAMETER EnableLoggingForRecursiveLookupEvent + Specifies whether the DNS server logs recursive lookup events. + + .PARAMETER EnableLoggingForRemoteServerEvent + Specifies whether the DNS server logs remote server events. + + .PARAMETER EnableLoggingForServerStartStopEvent + Specifies whether the DNS server logs server start and stop events. + + .PARAMETER EnableLoggingForTombstoneEvent + Specifies whether the DNS server logs tombstone events. + + .PARAMETER EnableLoggingForZoneDataWriteEvent + Specifies Controls whether the DNS server logs zone data write events. + + .PARAMETER EnableLoggingForZoneLoadingEvent + Specifies whether the DNS server logs zone load events. + + .PARAMETER EnableLoggingToFile + Specifies whether the DNS server logs logging-to-file. + + .PARAMETER EventLogLevel + Specifies an event log level. Valid values are Warning, Error, and None. + + .PARAMETER FilterIPAddressList + Specifies an array of IP addresses to filter. When you enable logging, traffic to and from these IP addresses is logged. If you do not specify any IP addresses, traffic to and from all IP addresses is logged. + + .PARAMETER FullPackets + Specifies whether the DNS server logs full packets. + + .PARAMETER LogFilePath + Specifies a log file path. + + .PARAMETER MaxMBFileSize + Specifies the maximum size of the log file. This parameter is relevant if you set EnableLogFileRollover and EnableLoggingToFile to $True. + + .PARAMETER Notifications + Specifies whether the DNS server logs notifications. + + .PARAMETER Queries + Specifies whether the DNS server allows query packet exchanges to pass through the content filter, such as the IPFilterList parameter. + + .PARAMETER QuestionTransactions + Specifies whether the DNS server logs queries. + + .PARAMETER ReceivePackets + Specifies whether the DNS server logs receive packets. + + .PARAMETER SaveLogsToPersistentStorage + Specifies whether the DNS server saves logs to persistent storage. + + .PARAMETER SendPackets + Specifies whether the DNS server logs send packets. + + .PARAMETER TcpPackets + Specifies whether the DNS server logs TCP packets. + + .PARAMETER UdpPackets + Specifies whether the DNS server logs UDP packets. + + .PARAMETER UnmatchedResponse + Specifies whether the DNS server logs unmatched responses. + + .PARAMETER Update + Specifies whether the DNS server logs updates. + + .PARAMETER UseSystemEventLog + Specifies whether the DNS server uses the system event log for logging. + + .PARAMETER WriteThrough + Specifies whether the DNS server logs write-throughs. +#> +function Test-TargetResource +{ + [CmdletBinding()] + [OutputType([Boolean])] + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $DnsServer, + + [Parameter()] + [Boolean] + $Answers, + + [Parameter()] + [Boolean] + $EnableLogFileRollover, + + [Parameter()] + [Boolean] + $EnableLoggingForLocalLookupEvent, + + [Parameter()] + [Boolean] + $EnableLoggingForPluginDllEvent, + + [Parameter()] + [Boolean] + $EnableLoggingForRecursiveLookupEvent, + + [Parameter()] + [Boolean] + $EnableLoggingForRemoteServerEvent, + + [Parameter()] + [Boolean] + $EnableLoggingForServerStartStopEvent, + + [Parameter()] + [Boolean] + $EnableLoggingForTombstoneEvent, + + [Parameter()] + [Boolean] + $EnableLoggingForZoneDataWriteEvent, + + [Parameter()] + [Boolean] + $EnableLoggingForZoneLoadingEvent, + + [Parameter()] + [Boolean] + $EnableLoggingToFile, + + [Parameter()] + [UInt32] + $EventLogLevel, + + [Parameter()] + [String[]] + $FilterIPAddressList, + + [Parameter()] + [Boolean] + $FullPackets, + + [Parameter()] + [System.String] + $LogFilePath, + + [Parameter()] + [UInt32] + $MaxMBFileSize, + + [Parameter()] + [Boolean] + $Notifications, + + [Parameter()] + [Boolean] + $Queries, + + [Parameter()] + [Boolean] + $QuestionTransactions, + + [Parameter()] + [Boolean] + $ReceivePackets, + + [Parameter()] + [Boolean] + $SaveLogsToPersistentStorage, + + [Parameter()] + [Boolean] + $SendPackets, + + [Parameter()] + [Boolean] + $TcpPackets, + + [Parameter()] + [Boolean] + $UdpPackets, + + [Parameter()] + [Boolean] + $UnmatchedResponse, + + [Parameter()] + [Boolean] + $Update, + + [Parameter()] + [Boolean] + $UseSystemEventLog, + + [Parameter()] + [Boolean] + $WriteThrough + ) + + Write-Verbose -Message $script:localizedData.EvaluatingDnsServerDiagnosticsMessage + + $currentState = Get-TargetResource -DnsServer $DnsServer + + $null = $PSBoundParameters.Remove('DnsServer') + + $result = Test-DscDnsParameterState -CurrentValues $currentState -DesiredValues $PSBoundParameters -TurnOffTypeChecking -Verbose:$VerbosePreference + + return $result +} + +Export-ModuleMember -Function *-TargetResource diff --git a/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/DSCResources/DSC_DnsServerDiagnostics/DSC_DnsServerDiagnostics.schema.mof b/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/DSCResources/DSC_DnsServerDiagnostics/DSC_DnsServerDiagnostics.schema.mof new file mode 100644 index 0000000..c1d3c99 --- /dev/null +++ b/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/DSCResources/DSC_DnsServerDiagnostics/DSC_DnsServerDiagnostics.schema.mof @@ -0,0 +1,33 @@ +[ClassVersion("1.0.0.0"), FriendlyName("DnsServerDiagnostics")] +class DSC_DnsServerDiagnostics : OMI_BaseResource +{ + [Key, Description("Specifies the DNS server to connect to, or use 'localhost' for the current node.")] String DnsServer; + [Write, Description("Specifies whether to enable the logging of DNS responses.")] Boolean Answers; + [Write, Description("Specifies whether to enable log file rollover.")] Boolean EnableLogFileRollover; + [Write, Description("Specifies whether the DNS server logs local lookup events.")] Boolean EnableLoggingForLocalLookupEvent; + [Write, Description("Specifies whether the DNS server logs dynamic link library (DLL) plug-in events.")] Boolean EnableLoggingForPluginDllEvent; + [Write, Description("Specifies whether the DNS server logs recursive lookup events.")] Boolean EnableLoggingForRecursiveLookupEvent; + [Write, Description("Specifies whether the DNS server logs remote server events.")] Boolean EnableLoggingForRemoteServerEvent; + [Write, Description("Specifies whether the DNS server logs server start and stop events.")] Boolean EnableLoggingForServerStartStopEvent; + [Write, Description("Specifies whether the DNS server logs tombstone events.")] Boolean EnableLoggingForTombstoneEvent; + [Write, Description("Specifies whether the DNS server logs zone data write events.")] Boolean EnableLoggingForZoneDataWriteEvent; + [Write, Description("Specifies whether the DNS server logs zone load events.")] Boolean EnableLoggingForZoneLoadingEvent; + [Write, Description("Specifies whether the DNS server logs logging-to-file.")] Boolean EnableLoggingToFile; + [Write, Description("Specifies an event log level. Valid values are Warning, Error, and None.")] Uint32 EventLogLevel; + [Write, Description("Specifies an array of IP addresses to filter. When you enable logging, traffic to and from these IP addresses is logged. If you do not specify any IP addresses, traffic to and from all IP addresses is logged.")] String FilterIPAddressList[]; + [Write, Description("Specifies whether the DNS server logs full packets.")] Boolean FullPackets; + [Write, Description("Specifies a log file path.")] String LogFilePath; + [Write, Description("Specifies the maximum size of the log file. This parameter is relevant if you set **EnableLogFileRollover** and **EnableLoggingToFile** to `$true`.")] Uint32 MaxMBFileSize; + [Write, Description("Specifies whether the DNS server logs notifications.")] Boolean Notifications; + [Write, Description("Specifies whether the DNS server allows query packet exchanges to pass through the content filter, such as the **FilterIPAddressList** parameter.")] Boolean Queries; + [Write, Description("Specifies whether the DNS server logs queries.")] Boolean QuestionTransactions; + [Write, Description("Specifies whether the DNS server logs receive packets.")] Boolean ReceivePackets; + [Write, Description("Specifies whether the DNS server saves logs to persistent storage.")] Boolean SaveLogsToPersistentStorage; + [Write, Description("Specifies whether the DNS server logs send packets.")] Boolean SendPackets; + [Write, Description("Specifies whether the DNS server logs TCP packets.")] Boolean TcpPackets; + [Write, Description("Specifies whether the DNS server logs UDP packets.")] Boolean UdpPackets; + [Write, Description("Specifies whether the DNS server logs unmatched responses.")] Boolean UnmatchedResponse; + [Write, Description("Specifies whether the DNS server logs updates.")] Boolean Update; + [Write, Description("Specifies whether the DNS server uses the system event log for logging.")] Boolean UseSystemEventLog; + [Write, Description("Specifies whether the DNS server logs write-throughs.")] Boolean WriteThrough; +}; diff --git a/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/DSCResources/DSC_DnsServerDiagnostics/README.md b/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/DSCResources/DSC_DnsServerDiagnostics/README.md new file mode 100644 index 0000000..20300f2 --- /dev/null +++ b/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/DSCResources/DSC_DnsServerDiagnostics/README.md @@ -0,0 +1,12 @@ +# Description + +The DnsServerDiagnostics DSC resource manages the debugging and logging +parameters on a Domain Name System (DNS) server. + +If the parameter **DnsServer** is set to `'localhost'` then the resource +can normally use the default credentials (SYSTEM) to configure the DNS server +settings. If using any other value for the parameter **DnsServer** make sure +that the credential the resource is run as have the correct permissions +at the target node and the necessary network traffic is permitted. +It is possible to run the resource with specific credentials using the +built-in parameter **PsDscRunAsCredential**. diff --git a/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/DSCResources/DSC_DnsServerDiagnostics/en-US/DSC_DnsServerDiagnostics.strings.psd1 b/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/DSCResources/DSC_DnsServerDiagnostics/en-US/DSC_DnsServerDiagnostics.strings.psd1 new file mode 100644 index 0000000..17cdc47 --- /dev/null +++ b/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/DSCResources/DSC_DnsServerDiagnostics/en-US/DSC_DnsServerDiagnostics.strings.psd1 @@ -0,0 +1,6 @@ +# culture="en-US" +ConvertFrom-StringData @' + GettingDnsServerDiagnosticsMessage = Getting DNS Server diagnostics + SettingDnsServerDiagnosticsMessage = Setting DNS Server diagnostics + EvaluatingDnsServerDiagnosticsMessage = Evaluating the DNS Server Diagnostics +'@ diff --git a/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/DSCResources/DSC_DnsServerDiagnostics/en-US/about_DnsServerDiagnostics.help.txt b/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/DSCResources/DSC_DnsServerDiagnostics/en-US/about_DnsServerDiagnostics.help.txt new file mode 100644 index 0000000..3a49856 --- /dev/null +++ b/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/DSCResources/DSC_DnsServerDiagnostics/en-US/about_DnsServerDiagnostics.help.txt @@ -0,0 +1,221 @@ +.NAME + DnsServerDiagnostics + +.DESCRIPTION + The DnsServerDiagnostics DSC resource manages the debugging and logging + parameters on a Domain Name System (DNS) server. + + If the parameter DnsServer is set to 'localhost' then the resource + can normally use the default credentials (SYSTEM) to configure the DNS server + settings. If using any other value for the parameter DnsServer make sure + that the credential the resource is run as have the correct permissions + at the target node and the necessary network traffic is permitted. + It is possible to run the resource with specific credentials using the + built-in parameter PsDscRunAsCredential. + +.PARAMETER DnsServer + Key - String + Specifies the DNS server to connect to, or use 'localhost' for the current node. + +.PARAMETER Answers + Write - Boolean + Specifies whether to enable the logging of DNS responses. + +.PARAMETER EnableLogFileRollover + Write - Boolean + Specifies whether to enable log file rollover. + +.PARAMETER EnableLoggingForLocalLookupEvent + Write - Boolean + Specifies whether the DNS server logs local lookup events. + +.PARAMETER EnableLoggingForPluginDllEvent + Write - Boolean + Specifies whether the DNS server logs dynamic link library (DLL) plug-in events. + +.PARAMETER EnableLoggingForRecursiveLookupEvent + Write - Boolean + Specifies whether the DNS server logs recursive lookup events. + +.PARAMETER EnableLoggingForRemoteServerEvent + Write - Boolean + Specifies whether the DNS server logs remote server events. + +.PARAMETER EnableLoggingForServerStartStopEvent + Write - Boolean + Specifies whether the DNS server logs server start and stop events. + +.PARAMETER EnableLoggingForTombstoneEvent + Write - Boolean + Specifies whether the DNS server logs tombstone events. + +.PARAMETER EnableLoggingForZoneDataWriteEvent + Write - Boolean + Specifies whether the DNS server logs zone data write events. + +.PARAMETER EnableLoggingForZoneLoadingEvent + Write - Boolean + Specifies whether the DNS server logs zone load events. + +.PARAMETER EnableLoggingToFile + Write - Boolean + Specifies whether the DNS server logs logging-to-file. + +.PARAMETER EventLogLevel + Write - UInt32 + Specifies an event log level. Valid values are Warning, Error, and None. + +.PARAMETER FilterIPAddressList + Write - StringArray + Specifies an array of IP addresses to filter. When you enable logging, traffic to and from these IP addresses is logged. If you do not specify any IP addresses, traffic to and from all IP addresses is logged. + +.PARAMETER FullPackets + Write - Boolean + Specifies whether the DNS server logs full packets. + +.PARAMETER LogFilePath + Write - String + Specifies a log file path. + +.PARAMETER MaxMBFileSize + Write - UInt32 + Specifies the maximum size of the log file. This parameter is relevant if you set EnableLogFileRollover and EnableLoggingToFile to $true. + +.PARAMETER Notifications + Write - Boolean + Specifies whether the DNS server logs notifications. + +.PARAMETER Queries + Write - Boolean + Specifies whether the DNS server allows query packet exchanges to pass through the content filter, such as the FilterIPAddressList parameter. + +.PARAMETER QuestionTransactions + Write - Boolean + Specifies whether the DNS server logs queries. + +.PARAMETER ReceivePackets + Write - Boolean + Specifies whether the DNS server logs receive packets. + +.PARAMETER SaveLogsToPersistentStorage + Write - Boolean + Specifies whether the DNS server saves logs to persistent storage. + +.PARAMETER SendPackets + Write - Boolean + Specifies whether the DNS server logs send packets. + +.PARAMETER TcpPackets + Write - Boolean + Specifies whether the DNS server logs TCP packets. + +.PARAMETER UdpPackets + Write - Boolean + Specifies whether the DNS server logs UDP packets. + +.PARAMETER UnmatchedResponse + Write - Boolean + Specifies whether the DNS server logs unmatched responses. + +.PARAMETER Update + Write - Boolean + Specifies whether the DNS server logs updates. + +.PARAMETER UseSystemEventLog + Write - Boolean + Specifies whether the DNS server uses the system event log for logging. + +.PARAMETER WriteThrough + Write - Boolean + Specifies whether the DNS server logs write-throughs. + +.EXAMPLE 1 + +This configuration will manage a DNS server's diagnostics settings + +Configuration DnsServerDiagnostics_CurrentNode_Config +{ + Import-DscResource -ModuleName 'DnsServerDsc' + + Node localhost + { + DnsServerDiagnostics 'Diagnostics' + { + DnsServer = 'localhost' + Answers = $true + EnableLogFileRollover = $true + EnableLoggingForLocalLookupEvent = $true + EnableLoggingForPluginDllEvent = $true + EnableLoggingForRecursiveLookupEvent = $true + EnableLoggingForRemoteServerEvent = $true + EnableLoggingForServerStartStopEvent = $true + EnableLoggingForTombstoneEvent = $true + EnableLoggingForZoneDataWriteEvent = $true + EnableLoggingForZoneLoadingEvent = $true + EnableLoggingToFile = $true + EventLogLevel = 7 + FilterIPAddressList = @('10.0.10.1', '10.0.10.2') + FullPackets = $false + LogFilePath = 'd:\dnslogs\dns.log' + MaxMBFileSize = 500000000 + Notifications = $true + Queries = $true + QuestionTransactions = $true + ReceivePackets = $false + SaveLogsToPersistentStorage = $true + SendPackets = $false + TcpPackets = $false + UdpPackets = $false + UnmatchedResponse = $false + Update = $true + UseSystemEventLog = $true + WriteThrough = $true + } + } +} + +.EXAMPLE 2 + +This configuration will manage a DNS server's diagnostics settings + +Configuration DnsServerDiagnostics_RemoteNode_Config +{ + Import-DscResource -ModuleName 'DnsServerDsc' + + Node localhost + { + DnsServerDiagnostics 'Diagnostics' + { + DnsServer = 'dns1.company.local' + Answers = $true + EnableLogFileRollover = $true + EnableLoggingForLocalLookupEvent = $true + EnableLoggingForPluginDllEvent = $true + EnableLoggingForRecursiveLookupEvent = $true + EnableLoggingForRemoteServerEvent = $true + EnableLoggingForServerStartStopEvent = $true + EnableLoggingForTombstoneEvent = $true + EnableLoggingForZoneDataWriteEvent = $true + EnableLoggingForZoneLoadingEvent = $true + EnableLoggingToFile = $true + EventLogLevel = 7 + FilterIPAddressList = @('10.0.10.1', '10.0.10.2') + FullPackets = $false + LogFilePath = 'd:\dnslogs\dns.log' + MaxMBFileSize = 500000000 + Notifications = $true + Queries = $true + QuestionTransactions = $true + ReceivePackets = $false + SaveLogsToPersistentStorage = $true + SendPackets = $false + TcpPackets = $false + UdpPackets = $false + UnmatchedResponse = $false + Update = $true + UseSystemEventLog = $true + WriteThrough = $true + } + } +} + diff --git a/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/DSCResources/DSC_DnsServerForwarder/DSC_DnsServerForwarder.psm1 b/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/DSCResources/DSC_DnsServerForwarder/DSC_DnsServerForwarder.psm1 new file mode 100644 index 0000000..aa33af6 --- /dev/null +++ b/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/DSCResources/DSC_DnsServerForwarder/DSC_DnsServerForwarder.psm1 @@ -0,0 +1,189 @@ +$script:dscResourceCommonPath = Join-Path -Path $PSScriptRoot -ChildPath '..\..\Modules\DscResource.Common' +$script:dnsServerDscCommonPath = Join-Path -Path $PSScriptRoot -ChildPath '..\..\Modules\DnsServerDsc.Common' + +Import-Module -Name $script:dscResourceCommonPath +Import-Module -Name $script:dnsServerDscCommonPath + +$script:localizedData = Get-LocalizedData -DefaultUICulture 'en-US' + +function Get-TargetResource +{ + [OutputType([System.Collections.Hashtable])] + param + ( + [Parameter(Mandatory = $true)] + [ValidateSet('Yes')] + [System.String] + $IsSingleInstance + ) + + Write-Verbose -Message $script:localizedData.GettingDnsForwardersMessage + + $currentServerForwarders = Get-DnsServerForwarder + + $targetResource = @{ + IsSingleInstance = $IsSingleInstance + IPAddresses = @() + UseRootHint = $currentServerForwarders.UseRootHint + EnableReordering = $currentServerForwarders.EnableReordering + Timeout = $currentServerForwarders.Timeout + } + + [System.Array] $currentIPs = $currentServerForwarders.IPAddress + + if ($currentIPs) + { + $targetResource.IPAddresses = $currentIPs + } + + return $targetResource +} + +function Set-TargetResource +{ + param + ( + [Parameter(Mandatory = $true)] + [ValidateSet('Yes')] + [System.String] + $IsSingleInstance, + + [Parameter()] + [AllowEmptyCollection()] + [string[]] + $IPAddresses, + + [Parameter()] + [System.Boolean] + $UseRootHint, + + [Parameter()] + [System.Boolean] + $EnableReordering, + + [Parameter()] + [ValidateRange(0, 15)] + [System.UInt32] + $Timeout + ) + + $setDnsServerForwarderParameters = @{} + + if ($PSBoundParameters.ContainsKey('IPAddresses')) + { + if ($IPAddresses.Count -eq 0) + { + Write-Verbose -Message $script:localizedData.DeletingDnsForwardersMessage + + Get-DnsServerForwarder | Remove-DnsServerForwarder -Force + } + else + { + Write-Verbose -Message $script:localizedData.SettingDnsForwardersMessage + + $setDnsServerForwarderParameters['IPAddress'] = $IPAddresses + } + } + + if ($PSBoundParameters.ContainsKey('UseRootHint')) + { + Write-Verbose -Message ($script:localizedData.SettingUseRootHintProperty -f $UseRootHint) + + $setDnsServerForwarderParameters['UseRootHint'] = $UseRootHint + } + + if ($PSBoundParameters.ContainsKey('EnableReordering')) + { + Write-Verbose -Message ($script:localizedData.SettingEnableReorderingProperty -f $EnableReordering) + + $setDnsServerForwarderParameters['EnableReordering'] = $EnableReordering + } + + if ($PSBoundParameters.ContainsKey('Timeout')) + { + Write-Verbose -Message ($script:localizedData.SettingTimeoutProperty -f $Timeout) + + $setDnsServerForwarderParameters['Timeout'] = $Timeout + } + + # Only do set if there are any parameters values added to the hashtable. + if ($setDnsServerForwarderParameters.Count -gt 0) + { + Set-DnsServerForwarder @setDnsServerForwarderParameters -WarningAction 'SilentlyContinue' + } +} + +function Test-TargetResource +{ + [OutputType([System.Boolean])] + param + ( + [Parameter(Mandatory = $true)] + [ValidateSet('Yes')] + [System.String] + $IsSingleInstance, + + [Parameter()] + [AllowEmptyCollection()] + [string[]] + $IPAddresses, + + [Parameter()] + [System.Boolean] + $UseRootHint, + + [Parameter()] + [System.Boolean] + $EnableReordering, + + [Parameter()] + [ValidateRange(0, 15)] + [System.UInt32] + $Timeout + ) + + Write-Verbose -Message $script:localizedData.ValidatingIPAddressesMessage + + $currentConfiguration = Get-TargetResource -IsSingleInstance $IsSingleInstance + + [System.Array] $currentIPs = $currentConfiguration.IPAddresses + + if ($currentIPs.Count -ne $IPAddresses.Count) + { + return $false + } + + foreach ($ip in $IPAddresses) + { + if ($ip -notin $currentIPs) + { + return $false + } + } + + if ($PSBoundParameters.ContainsKey('UseRootHint')) + { + if ($currentConfiguration.UseRootHint -ne $UseRootHint) + { + return $false + } + } + + if ($PSBoundParameters.ContainsKey('EnableReordering')) + { + if ($currentConfiguration.EnableReordering -ne $EnableReordering) + { + return $false + } + } + + if ($PSBoundParameters.ContainsKey('Timeout')) + { + if ($currentConfiguration.Timeout -ne $Timeout) + { + return $false + } + } + + return $true +} diff --git a/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/DSCResources/DSC_DnsServerForwarder/DSC_DnsServerForwarder.schema.mof b/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/DSCResources/DSC_DnsServerForwarder/DSC_DnsServerForwarder.schema.mof new file mode 100644 index 0000000..8ddca06 --- /dev/null +++ b/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/DSCResources/DSC_DnsServerForwarder/DSC_DnsServerForwarder.schema.mof @@ -0,0 +1,9 @@ +[ClassVersion("1.0.0.0"), FriendlyName("DnsServerForwarder")] +class DSC_DnsServerForwarder : OMI_BaseResource +{ + [Key, Description("Specifies the resource is a single instance, the value must be `'Yes'`."), ValueMap{"Yes"}, Values{"Yes"}] String IsSingleInstance; + [Write, Description("IP addresses of the forwarders")] String IPAddresses[]; + [Write, Description("Specifies if you want to use root hint or not.")] Boolean UseRootHint; + [Write, Description("Specifies whether to enable the DNS server to reorder forwarders dynamically.")] Boolean EnableReordering; + [Write, Description("Specifies the number of seconds that the DNS server waits for a response from the forwarder. The minimum value is 0, and the maximum value is 15.")] UInt32 Timeout; +}; diff --git a/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/DSCResources/DSC_DnsServerForwarder/README.md b/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/DSCResources/DSC_DnsServerForwarder/README.md new file mode 100644 index 0000000..68f7868 --- /dev/null +++ b/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/DSCResources/DSC_DnsServerForwarder/README.md @@ -0,0 +1,6 @@ +# Description + +The DnsServerForwarder DSC resource manages the DNS forwarder list of a +Domain Name System (DNS) server. If the parameter `EnableReordering` is set +to `$false` then the preferred forwarder can be put in the series of forwarder +IP addresses. diff --git a/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/DSCResources/DSC_DnsServerForwarder/en-US/DSC_DnsServerForwarder.strings.psd1 b/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/DSCResources/DSC_DnsServerForwarder/en-US/DSC_DnsServerForwarder.strings.psd1 new file mode 100644 index 0000000..c1224fd --- /dev/null +++ b/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/DSCResources/DSC_DnsServerForwarder/en-US/DSC_DnsServerForwarder.strings.psd1 @@ -0,0 +1,10 @@ +# culture="en-US" +ConvertFrom-StringData @' + GettingDnsForwardersMessage = Getting current DNS forwarders. + SettingDnsForwardersMessage = Setting DNS forwarders. + ValidatingIPAddressesMessage = Validate IP addresses. + DeletingDnsForwardersMessage = Deleting DNS forwarders. + SettingUseRootHintProperty = Setting the use root hint property to {0}. + SettingEnableReorderingProperty = Setting the enable DNS reordering property to {0}. + SettingTimeoutProperty = Setting the timeout property to {0}. +'@ diff --git a/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/DSCResources/DSC_DnsServerForwarder/en-US/about_DnsServerForwarder.help.txt b/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/DSCResources/DSC_DnsServerForwarder/en-US/about_DnsServerForwarder.help.txt new file mode 100644 index 0000000..0a87be2 --- /dev/null +++ b/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/DSCResources/DSC_DnsServerForwarder/en-US/about_DnsServerForwarder.help.txt @@ -0,0 +1,147 @@ +.NAME + DnsServerForwarder + +.DESCRIPTION + The DnsServerForwarder DSC resource manages the DNS forwarder list of a + Domain Name System (DNS) server. If the parameter EnableReordering is set + to $false then the preferred forwarder can be put in the series of forwarder + IP addresses. + +.PARAMETER IsSingleInstance + Key - String + Allowed values: Yes + Specifies the resource is a single instance, the value must be 'Yes'. + +.PARAMETER IPAddresses + Write - StringArray + IP addresses of the forwarders + +.PARAMETER UseRootHint + Write - Boolean + Specifies if you want to use root hint or not. + +.PARAMETER EnableReordering + Write - Boolean + Specifies whether to enable the DNS server to reorder forwarders dynamically. + +.PARAMETER Timeout + Write - UInt32 + Specifies the number of seconds that the DNS server waits for a response from the forwarder. The minimum value is 0, and the maximum value is 15. + +.EXAMPLE 1 + +This configuration will set the DNS forwarders + +Configuration DnsServerForwarder_set_config +{ + Import-DscResource -ModuleName 'DnsServerDsc' + + Node localhost + { + + DnsServerForwarder 'SetForwarders' + { + IsSingleInstance = 'Yes' + IPAddresses = @('192.168.0.10', '192.168.0.11') + UseRootHint = $false + } + } +} + +.EXAMPLE 2 + +This configuration will remove all the DNS forwarders + +Configuration DnsServerForwarder_remove_config +{ + Import-DscResource -ModuleName 'DnsServerDsc' + + Node localhost + { + DnsServerForwarder 'SetForwarders' + { + IsSingleInstance = 'Yes' + IPAddresses = @() + UseRootHint = $false + } + } +} + +.EXAMPLE 3 + +This configuration will remove all the DNS forwarders + +Configuration DnsServerForwarder_SetUseRootHint_Config +{ + Import-DscResource -ModuleName 'DnsServerDsc' + + Node localhost + { + DnsServerForwarder 'SetUseRootHints' + { + IsSingleInstance = 'Yes' + UseRootHint = $true + } + } +} + +.EXAMPLE 4 + +This configuration will set the DNS forwarders and enable dynamic reordering. + +Configuration DnsServerForwarder_EnableReordering_Config +{ + Import-DscResource -ModuleName 'DnsServerDsc' + + Node localhost + { + DnsServerForwarder 'SetUseRootHints' + { + IsSingleInstance = 'Yes' + IPAddresses = @('192.168.0.10', '192.168.0.11') + UseRootHint = $false + EnableReordering = $true + } + } +} + +.EXAMPLE 5 + +This configuration will set the DNS forwarders and disable dynamic reordering. + +Configuration DnsServerForwarder_DisableReordering_Config +{ + Import-DscResource -ModuleName 'DnsServerDsc' + + Node localhost + { + DnsServerForwarder 'SetUseRootHints' + { + IsSingleInstance = 'Yes' + IPAddresses = @('192.168.0.10', '192.168.0.11') + UseRootHint = $false + EnableReordering = $false + } + } +} + +.EXAMPLE 6 + +This configuration will set the DNS forwarders and disable dynamic reordering. + +Configuration DnsServerForwarder_SetTimeout_Config +{ + Import-DscResource -ModuleName 'DnsServerDsc' + + Node localhost + { + DnsServerForwarder 'SetUseRootHints' + { + IsSingleInstance = 'Yes' + IPAddresses = @('192.168.0.10', '192.168.0.11') + UseRootHint = $false + Timeout = 10 + } + } +} + diff --git a/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/DSCResources/DSC_DnsServerPrimaryZone/DSC_DnsServerPrimaryZone.psm1 b/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/DSCResources/DSC_DnsServerPrimaryZone/DSC_DnsServerPrimaryZone.psm1 new file mode 100644 index 0000000..a2bdbd6 --- /dev/null +++ b/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/DSCResources/DSC_DnsServerPrimaryZone/DSC_DnsServerPrimaryZone.psm1 @@ -0,0 +1,187 @@ +$script:dscResourceCommonPath = Join-Path -Path $PSScriptRoot -ChildPath '..\..\Modules\DscResource.Common' +$script:dnsServerDscCommonPath = Join-Path -Path $PSScriptRoot -ChildPath '..\..\Modules\DnsServerDsc.Common' + +Import-Module -Name $script:dscResourceCommonPath +Import-Module -Name $script:dnsServerDscCommonPath + +$script:localizedData = Get-LocalizedData -DefaultUICulture 'en-US' + +function Get-TargetResource +{ + [CmdletBinding()] + [OutputType([System.Collections.Hashtable])] + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $Name, + + [Parameter()] + [ValidateNotNullOrEmpty()] + [System.String] + $ZoneFile = "$Name.dns", + + [Parameter()] + [ValidateSet('None','NonSecureAndSecure')] + [System.String] + $DynamicUpdate = 'None', + + [Parameter()] + [ValidateSet('Present','Absent')] + [System.String] + $Ensure = 'Present' + ) + + Assert-Module -ModuleName 'DNSServer' + + Write-Verbose ($script:localizedData.CheckingZoneMessage -f $Name, $Ensure) + + $dnsServerZone = Get-DnsServerZone -Name $Name -ErrorAction SilentlyContinue + + $targetResource = @{ + Name = $Name + ZoneFile = $dnsServerZone.ZoneFile + DynamicUpdate = $dnsServerZone.DynamicUpdate + Ensure = if ($null -eq $dnsServerZone) { 'Absent' } else { 'Present' } + } + + return $targetResource + +} #end function Get-TargetResource + +function Test-TargetResource +{ + [CmdletBinding()] + [OutputType([System.Boolean])] + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $Name, + + [Parameter()] + [ValidateNotNullOrEmpty()] + [System.String] + $ZoneFile = "$Name.dns", + + [Parameter()] + [ValidateSet('None','NonSecureAndSecure')] + [System.String] + $DynamicUpdate = 'None', + + [Parameter()] + [ValidateSet('Present','Absent')] + [System.String] + $Ensure = 'Present' + ) + + $targetResource = Get-TargetResource @PSBoundParameters + + $targetResourceInCompliance = $true + + if ($Ensure -eq 'Present') + { + if ($targetResource.Ensure -eq 'Present') + { + if ($targetResource.ZoneFile -ne $ZoneFile) + { + Write-Verbose ($script:localizedData.NotDesiredPropertyMessage -f 'ZoneFile', $targetResource.ZoneFile, $ZoneFile) + + $targetResourceInCompliance = $false + } + elseif ($targetResource.DynamicUpdate -ne $DynamicUpdate) + { + Write-Verbose ($script:localizedData.NotDesiredPropertyMessage -f 'DynamicUpdate', $targetResource.DynamicUpdate, $DynamicUpdate) + + $targetResourceInCompliance = $false + } + } + else + { + # Dns zone is present and needs removing + Write-Verbose ($script:localizedData.NotDesiredPropertyMessage -f 'Ensure', 'Absent', 'Present') + + $targetResourceInCompliance = $false + } + } + else + { + if ($targetResource.Ensure -eq 'Present') + { + # Dns zone is absent and should be present + Write-Verbose ($script:localizedData.NotDesiredPropertyMessage -f 'Ensure', 'Absent', 'Present') + + $targetResourceInCompliance = $false + } + } + + return $targetResourceInCompliance + +} #end function Test-TargetResource + +function Set-TargetResource +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $Name, + + [Parameter()] + [ValidateNotNullOrEmpty()] + [System.String] + $ZoneFile = "$Name.dns", + + [Parameter()] + [ValidateSet('None','NonSecureAndSecure')] + [System.String] + $DynamicUpdate = 'None', + + [Parameter()] + [ValidateSet('Present','Absent')] + [System.String] + $Ensure = 'Present' + ) + + Assert-Module -ModuleName 'DNSServer' + + if ($Ensure -eq 'Present') + { + Write-Verbose ($script:localizedData.CheckingZoneMessage -f $Name, $Ensure) + + $dnsServerZone = Get-DnsServerZone -Name $Name -ErrorAction SilentlyContinue + + if ($dnsServerZone) + { + ## Update the existing zone + if ($dnsServerZone.ZoneFile -ne $ZoneFile) + { + $dnsServerZone | Set-DnsServerPrimaryZone -ZoneFile $ZoneFile + + Write-Verbose ($script:localizedData.SetPropertyMessage -f 'ZoneFile') + } + if ($dnsServerZone.DynamicUpdate -ne $DynamicUpdate) + { + $dnsServerZone | Set-DnsServerPrimaryZone -DynamicUpdate $DynamicUpdate + + Write-Verbose ($script:localizedData.SetPropertyMessage -f 'DynamicUpdate') + } + } + elseif (-not $dnsServerZone) + { + # Create the zone + Write-Verbose ($script:localizedData.AddingZoneMessage -f $Name) + + Add-DnsServerPrimaryZone -Name $Name -ZoneFile $ZoneFile -DynamicUpdate $DynamicUpdate + } + } + elseif ($Ensure -eq 'Absent') + { + # Remove the DNS Server zone + Write-Verbose ($script:localizedData.RemovingZoneMessage -f $Name) + + Get-DnsServerZone -Name $Name | Remove-DnsServerZone -Force + } + +} #end function Set-TargetResource diff --git a/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/DSCResources/DSC_DnsServerPrimaryZone/DSC_DnsServerPrimaryZone.schema.mof b/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/DSCResources/DSC_DnsServerPrimaryZone/DSC_DnsServerPrimaryZone.schema.mof new file mode 100644 index 0000000..07dc2e2 --- /dev/null +++ b/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/DSCResources/DSC_DnsServerPrimaryZone/DSC_DnsServerPrimaryZone.schema.mof @@ -0,0 +1,8 @@ +[ClassVersion("1.0.0.0"), FriendlyName("DnsServerPrimaryZone")] +class DSC_DnsServerPrimaryZone : OMI_BaseResource +{ + [Key, Description("Name of the DNS Server primary zone")] String Name; + [Write, Description("Name of the DNS Server primary zone file. If not specified, defaults to 'ZoneName.dns'.")] String ZoneFile; + [Write, Description("Primary zone dynamic DNS update option. Defaults to `'None'`."), ValueMap{"None","NonSecureAndSecure"}, Values{"None","NonSecureAndSecure"}] String DynamicUpdate; + [Write, Description("Whether the DNS zone should be present or absent"), ValueMap{"Present","Absent"}, Values{"Present","Absent"}] String Ensure; +}; diff --git a/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/DSCResources/DSC_DnsServerPrimaryZone/README.md b/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/DSCResources/DSC_DnsServerPrimaryZone/README.md new file mode 100644 index 0000000..218a6b3 --- /dev/null +++ b/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/DSCResources/DSC_DnsServerPrimaryZone/README.md @@ -0,0 +1,3 @@ +# Description + +The DnsServerPrimaryZone DSC resource manages a standalone file-backed Primary zone on a given Domain Name System (DNS) server. diff --git a/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/DSCResources/DSC_DnsServerPrimaryZone/en-US/DSC_DnsServerPrimaryZone.strings.psd1 b/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/DSCResources/DSC_DnsServerPrimaryZone/en-US/DSC_DnsServerPrimaryZone.strings.psd1 new file mode 100644 index 0000000..8195b73 --- /dev/null +++ b/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/DSCResources/DSC_DnsServerPrimaryZone/en-US/DSC_DnsServerPrimaryZone.strings.psd1 @@ -0,0 +1,8 @@ +# culture="en-US" +ConvertFrom-StringData @' + CheckingZoneMessage = Checking DNS server zone with name '{0}' is '{1}'... + AddingZoneMessage = Adding DNS server zone '{0}' ... + RemovingZoneMessage = Removing DNS server zone '{0}' ... + NotDesiredPropertyMessage = DNS server zone property '{0}' is not correct. Expected '{1}', actual '{2}' + SetPropertyMessage = DNS server zone property '{0}' is set +'@ diff --git a/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/DSCResources/DSC_DnsServerPrimaryZone/en-US/about_DnsServerPrimaryZone.help.txt b/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/DSCResources/DSC_DnsServerPrimaryZone/en-US/about_DnsServerPrimaryZone.help.txt new file mode 100644 index 0000000..cfb17fb --- /dev/null +++ b/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/DSCResources/DSC_DnsServerPrimaryZone/en-US/about_DnsServerPrimaryZone.help.txt @@ -0,0 +1,135 @@ +.NAME + DnsServerPrimaryZone + +.DESCRIPTION + The DnsServerPrimaryZone DSC resource manages a standalone file-backed Primary zone on a given Domain Name System (DNS) server. + +.PARAMETER Name + Key - String + Name of the DNS Server primary zone + +.PARAMETER ZoneFile + Write - String + Name of the DNS Server primary zone file. If not specified, defaults to 'ZoneName.dns'. + +.PARAMETER DynamicUpdate + Write - String + Allowed values: None, NonSecureAndSecure + Primary zone dynamic DNS update option. Defaults to 'None'. + +.PARAMETER Ensure + Write - String + Allowed values: Present, Absent + Whether the DNS zone should be present or absent + +.EXAMPLE 1 + +This configuration will add a file-backed classful reverse primary zone +using the resource default parameter values. + +Configuration DnsServerPrimaryZone_AddClassfulReversePrimaryZone_Config +{ + Import-DscResource -ModuleName 'DnsServerDsc' + + Node localhost + { + DnsServerPrimaryZone 'AddPrimaryZone' + { + Name = '1.168.192.in-addr.arpa' + } + } +} + +.EXAMPLE 2 + +This configuration will add a file-backed classless reverse primary zone +using the resource default parameter values. + +Configuration DnsServerPrimaryZone_AddClasslessReversePrimaryZone_Config +{ + Import-DscResource -ModuleName 'DnsServerDsc' + + Node localhost + { + DnsServerPrimaryZone 'AddPrimaryZone' + { + Name = '64-26.100.168.192.in-addr.arpa' + } + } +} + +.EXAMPLE 3 + +This configuration will add a file-backed primary zone using the resource +default parameter values. + +Configuration DnsServerPrimaryZone_AddPrimaryZoneUsingDefaults_Config +{ + Import-DscResource -ModuleName 'DnsServerDsc' + + Node localhost + { + DnsServerPrimaryZone 'AddPrimaryZone' + { + Name = 'demo.contoso.com' + } + } +} + +.EXAMPLE 4 + +This configuration will add a file-backed primary zone using the resource +default parameter values. + +Configuration DnsServerPrimaryZone_AddPrimaryZoneWithSpecificValues_Config +{ + Import-DscResource -ModuleName 'DnsServerDsc' + + Node localhost + { + DnsServerPrimaryZone 'AddPrimaryZone' + { + Ensure = 'Present' + Name = 'demo.contoso.com' + ZoneFile = 'demo.contoso.com.dns' + DynamicUpdate = 'NonSecureAndSecure' + } + } +} + +.EXAMPLE 5 + +This configuration will remove a file-backed primary zone. + +Configuration DnsServerPrimaryZone_RemovePrimaryZone_Config +{ + Import-DscResource -ModuleName 'DnsServerDsc' + + Node localhost + { + DnsServerPrimaryZone 'RemovePrimaryZone' + { + Ensure = 'Absent' + Name = 'demo.contoso.com' + } + } +} + +.EXAMPLE 6 + +This configuration will remove a file-backed primary zone. + +Configuration DnsServerPrimaryZone_RemoveReversePrimaryZone_Config +{ + Import-DscResource -ModuleName 'DnsServerDsc' + + Node localhost + { + DnsServerPrimaryZone 'RemovePrimaryZone' + { + Ensure = 'Absent' + Name = '1.168.192.in-addr.arpa' + } + } +} + diff --git a/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/DSCResources/DSC_DnsServerRootHint/DSC_DnsServerRootHint.psm1 b/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/DSCResources/DSC_DnsServerRootHint/DSC_DnsServerRootHint.psm1 new file mode 100644 index 0000000..440ac47 --- /dev/null +++ b/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/DSCResources/DSC_DnsServerRootHint/DSC_DnsServerRootHint.psm1 @@ -0,0 +1,124 @@ +$script:dscResourceCommonPath = Join-Path -Path $PSScriptRoot -ChildPath '..\..\Modules\DscResource.Common' +$script:dnsServerDscCommonPath = Join-Path -Path $PSScriptRoot -ChildPath '..\..\Modules\DnsServerDsc.Common' + +Import-Module -Name $script:dscResourceCommonPath +Import-Module -Name $script:dnsServerDscCommonPath + +$script:localizedData = Get-LocalizedData -DefaultUICulture 'en-US' + +<# + + .SYNOPSIS + Get desired state + + .PARAMETER IsSingleInstance + Key for the resource. This value must be set to 'Yes' + +#> +function Get-TargetResource +{ + [OutputType([System.Collections.Hashtable])] + param + ( + [Parameter(Mandatory = $true)] + [ValidateSet('Yes')] + [System.String] + $IsSingleInstance, + + [Parameter(Mandatory = $true)] + [Microsoft.Management.Infrastructure.CimInstance[]] + [AllowEmptyCollection()] + $NameServer + ) + + Assert-Module -ModuleName 'DNSServer' + + Write-Verbose -Message $script:localizedData.GettingCurrentRootHintsMessage + + $result = @{ + IsSingleInstance = 'Yes' + NameServer = Convert-RootHintsToHashtable -RootHints @(Get-DnsServerRootHint) + } + + Write-Verbose -Message ($script:localizedData.FoundRootHintsMessage -f $result.NameServer.Count) + $result +} + +<# + + .SYNOPSIS + Set desired state + + .PARAMETER IsSingleInstance + Key for the resource. This value must be set to 'Yes' + + .PARAMETER NameServer + A list of names and IP addresses as a hashtable. This may look like this: NameServer = @{ 'rh1.vm.net.' = '20.1.1.1' } + +#> +function Set-TargetResource +{ + param + ( + [Parameter(Mandatory = $true)] + [ValidateSet('Yes')] + [System.String] + $IsSingleInstance, + + [Parameter(Mandatory = $true)] + [Microsoft.Management.Infrastructure.CimInstance[]] + [AllowEmptyCollection()] + $NameServer + ) + + Write-Verbose -Message $script:localizedData.RemovingAllRootHintsMessage + Get-DnsServerRootHint | Remove-DnsServerRootHint -Force + + foreach ($item in $NameServer) + { + Write-Verbose -Message ($script:localizedData.AddingRootHintMessage -f $item.Key) + Add-DnsServerRootHint -NameServer $item.Key -IPAddress ($item.value -split ',' | ForEach-Object { $_.Trim() }) + } +} + +<# + + .SYNOPSIS + Test desired state + + .PARAMETER IsSingleInstance + Key for the resource. This value must be set to 'Yes' + + .PARAMETER NameServer + A list of names and IP addresses as a hashtable. This may look like this: NameServer = @{ 'rh1.vm.net.' = '20.1.1.1' } + +#> +function Test-TargetResource +{ + [OutputType([Bool])] + param + ( + [Parameter(Mandatory = $true)] + [ValidateSet('Yes')] + [System.String] + $IsSingleInstance, + + [Parameter(Mandatory = $true)] + [Microsoft.Management.Infrastructure.CimInstance[]] + [AllowEmptyCollection()] + $NameServer + ) + + Write-Verbose -Message $script:localizedData.ValidatingRootHintsMessage + $currentState = Get-TargetResource @PSBoundParameters + $desiredState = $PSBoundParameters + + foreach ($entry in $desiredState.NameServer) + { + $entry.Value = $entry.Value -replace ' ', '' + } + + $result = Test-DscDnsParameterState -CurrentValues $currentState -DesiredValues $desiredState -TurnOffTypeChecking -ReverseCheck + + $result +} diff --git a/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/DSCResources/DSC_DnsServerRootHint/DSC_DnsServerRootHint.schema.mof b/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/DSCResources/DSC_DnsServerRootHint/DSC_DnsServerRootHint.schema.mof new file mode 100644 index 0000000..0a69aa7 --- /dev/null +++ b/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/DSCResources/DSC_DnsServerRootHint/DSC_DnsServerRootHint.schema.mof @@ -0,0 +1,6 @@ +[ClassVersion("1.0.0.0"), FriendlyName("DnsServerRootHint")] +class DSC_DnsServerRootHint : OMI_BaseResource +{ + [Key, Description("Specifies the resource is a single instance, the value must be `'Yes'`."), ValueMap{"Yes"}, Values{"Yes"}] String IsSingleInstance; + [Required, Description("A hashtable that defines the name server. Key and value must be strings."), EmbeddedInstance("MSFT_KeyValuePair")] String NameServer[]; +}; diff --git a/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/DSCResources/DSC_DnsServerRootHint/README.md b/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/DSCResources/DSC_DnsServerRootHint/README.md new file mode 100644 index 0000000..e86dfeb --- /dev/null +++ b/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/DSCResources/DSC_DnsServerRootHint/README.md @@ -0,0 +1,3 @@ +# Description + +The DnsServerRootHint DSC resource manages root hints on a Domain Name System (DNS) server. diff --git a/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/DSCResources/DSC_DnsServerRootHint/en-US/DSC_DnsServerRootHint.strings.psd1 b/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/DSCResources/DSC_DnsServerRootHint/en-US/DSC_DnsServerRootHint.strings.psd1 new file mode 100644 index 0000000..013f0c9 --- /dev/null +++ b/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/DSCResources/DSC_DnsServerRootHint/en-US/DSC_DnsServerRootHint.strings.psd1 @@ -0,0 +1,8 @@ +# culture="en-US" +ConvertFrom-StringData @' + GettingCurrentRootHintsMessage = Getting current root hints. + FoundRootHintsMessage = Found {0} root hints. + RemovingAllRootHintsMessage = Removing all root hints. + AddingRootHintMessage = Adding root hint '{0}'. + ValidatingRootHintsMessage = Validating root hints. +'@ diff --git a/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/DSCResources/DSC_DnsServerRootHint/en-US/about_DnsServerRootHint.help.txt b/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/DSCResources/DSC_DnsServerRootHint/en-US/about_DnsServerRootHint.help.txt new file mode 100644 index 0000000..d1a40a6 --- /dev/null +++ b/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/DSCResources/DSC_DnsServerRootHint/en-US/about_DnsServerRootHint.help.txt @@ -0,0 +1,65 @@ +.NAME + DnsServerRootHint + +.DESCRIPTION + The DnsServerRootHint DSC resource manages root hints on a Domain Name System (DNS) server. + +.PARAMETER IsSingleInstance + Key - String + Allowed values: Yes + Specifies the resource is a single instance, the value must be 'Yes'. + +.PARAMETER NameServer + Required - InstanceArray + A hashtable that defines the name server. Key and value must be strings. + +.EXAMPLE 1 + +This configuration will manage the DNS server root hints + +Configuration DnsServerRootHint_set_config +{ + Import-DscResource -ModuleName 'DnsServerDsc' + + Node localhost + { + DnsServerRootHint 'RootHints' + { + IsSingleInstance = 'Yes' + NameServer = @{ + 'A.ROOT-SERVERS.NET.' = '2001:503:ba3e::2:30' + 'B.ROOT-SERVERS.NET.' = '2001:500:84::b' + 'C.ROOT-SERVERS.NET.' = '2001:500:2::c' + 'D.ROOT-SERVERS.NET.' = '2001:500:2d::d' + 'E.ROOT-SERVERS.NET.' = '192.203.230.10' + 'F.ROOT-SERVERS.NET.' = '2001:500:2f::f' + 'G.ROOT-SERVERS.NET.' = '192.112.36.4' + 'H.ROOT-SERVERS.NET.' = '2001:500:1::53' + 'I.ROOT-SERVERS.NET.' = '2001:7fe::53' + 'J.ROOT-SERVERS.NET.' = '2001:503:c27::2:30' + 'K.ROOT-SERVERS.NET.' = '2001:7fd::1' + 'L.ROOT-SERVERS.NET.' = '2001:500:9f::42' + 'M.ROOT-SERVERS.NET.' = '2001:dc3::353' + } + } + } +} + +.EXAMPLE 2 + +This configuration will remove the DNS server root hints + +Configuration DnsServerRootHint_remove_config +{ + Import-DscResource -ModuleName 'DnsServerDsc' + + Node localhost + { + DnsServerRootHint 'RootHints' + { + IsSingleInstance = 'Yes' + NameServer = @{ } + } + } +} + diff --git a/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/DSCResources/DSC_DnsServerSecondaryZone/DSC_DnsServerSecondaryZone.psm1 b/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/DSCResources/DSC_DnsServerSecondaryZone/DSC_DnsServerSecondaryZone.psm1 new file mode 100644 index 0000000..54b669e --- /dev/null +++ b/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/DSCResources/DSC_DnsServerSecondaryZone/DSC_DnsServerSecondaryZone.psm1 @@ -0,0 +1,277 @@ +$script:dscResourceCommonPath = Join-Path -Path $PSScriptRoot -ChildPath '..\..\Modules\DscResource.Common' +$script:dnsServerDscCommonPath = Join-Path -Path $PSScriptRoot -ChildPath '..\..\Modules\DnsServerDsc.Common' + +Import-Module -Name $script:dscResourceCommonPath +Import-Module -Name $script:dnsServerDscCommonPath + +$script:localizedData = Get-LocalizedData -DefaultUICulture 'en-US' + +function Get-TargetResource +{ + [CmdletBinding()] + [OutputType([System.Collections.Hashtable])] + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $Name, + + [Parameter(Mandatory = $true)] + [String[]] + $MasterServers + ) + +#region Input Validation + + # Check for DnsServer module/role + Assert-Module -ModuleName 'DnsServer' + +#endregion + + Write-Verbose -Message 'Getting DNS zone.' + $dnsZone = Get-DnsServerZone -Name $Name -ErrorAction SilentlyContinue + if ($dnsZone) + { + $Ensure = 'Present' + } + else + { + $Ensure = 'Absent' + } + + @{ + Name = $Name + Ensure = $Ensure + MasterServers = [string[]]$($dnsZone.MasterServers.IPAddressToString) + Type = $dnsZone.ZoneType + } +} + +function Set-TargetResource +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $Name, + + [Parameter(Mandatory = $true)] + [String[]] + $MasterServers, + + [Parameter()] + [ValidateSet("Present","Absent")] + [System.String] + $Ensure = 'Present' + ) + Write-Verbose -Message 'Setting DNS zone.' + if ($PSBoundParameters.ContainsKey('Debug')) + { + $null = $PSBoundParameters.Remove('Debug') + } + Test-ResourceProperties @PSBoundParameters -Apply + + # Restart the DNS service + Restart-Service DNS +} + +function Test-TargetResource +{ + [CmdletBinding()] + [OutputType([System.Boolean])] + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $Name, + + [Parameter(Mandatory = $true)] + [String[]] + $MasterServers, + + [Parameter()] + [ValidateSet("Present","Absent")] + [System.String] + $Ensure = 'Present' + ) + +#region Input Validation + + # Check for DnsServer module/role + Assert-Module -ModuleName 'DnsServer' + +#endregion + Write-Verbose -Message 'Validating DNS zone.' + if ($PSBoundParameters.ContainsKey('Debug')) + { + $null = $PSBoundParameters.Remove('Debug') + } + Test-ResourceProperties @PSBoundParameters + +} + +#region Helper Functions +function Test-ResourceProperties +{ + [CmdletBinding()] + [OutputType([bool])] + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $Name, + + [Parameter(Mandatory = $true)] + [String[]] + $MasterServers, + + [Parameter()] + [ValidateSet("Present","Absent")] + [System.String] + $Ensure = 'Present', + + [Parameter()] + [Switch] + $Apply + ) + + $zoneMessage = $($script:localizedData.CheckingZoneMessage) -f $Name + Write-Verbose -Message $zoneMessage + + $dnsZone = Get-DnsServerZone -Name $Name -ErrorAction SilentlyContinue + + # Found DNS Zone + if ($dnsZone) + { + $testZoneMessage = $($script:localizedData.TestZoneMessage) -f 'present', $Ensure + Write-Verbose -Message $testZoneMessage + + # If the zone should be present + if ($Ensure -eq 'Present') + { + # Check if the zone is secondary + $secondaryZoneMessage = $script:localizedData.CheckingSecondaryZoneMessage + Write-Verbose -Message $secondaryZoneMessage + + # If the zone is already secondary zone + if ($dnsZone.ZoneType -eq "Secondary") + { + $correctZoneMessage = $($script:localizedData.AlreadySecondaryZoneMessage) -f $Name + Write-Verbose -Message $correctZoneMessage + + # Check the master server property + $checkPropertyMessage = $($script:localizedData.CheckPropertyMessage) -f 'master servers' + Write-Verbose -Message $checkPropertyMessage + + # Compare the master server property + if ((-not $dnsZone.MasterServers) -or (Compare-Object $($dnsZone.MasterServers.IPAddressToString) $MasterServers)) + { + $notDesiredPropertyMessage = $($script:localizedData.NotDesiredPropertyMessage) -f ` + 'master servers',$MasterServers,$dnsZone.MasterServers + Write-Verbose -Message $notDesiredPropertyMessage + + if ($Apply) + { + Set-DnsServerSecondaryZone -Name $Name -MasterServers $MasterServers + + $setPropertyMessage = $($script:localizedData.SetPropertyMessage) -f 'master servers' + Write-Verbose -Message $setPropertyMessage + } + else + { + return $false + } + } # end master server mismatch + else + { + $desiredPropertyMessage = $($script:localizedData.DesiredPropertyMessage) -f 'master servers' + Write-Verbose -Message $desiredPropertyMessage + if (-not $Apply) + { + return $true + } + } # end master servers match + + } # end zone is already secondary + + # If the zone is not secondary, make it so + else + { + $notCorrectZoneMessage = $($script:localizedData.NotSecondaryZoneMessage) -f $Name,$dnsZone.ZoneType + Write-Verbose -Message $notCorrectZoneMessage + + # Convert the zone to Secondary zone + if ($Apply) + { + ConvertTo-DnsServerSecondaryZone -Name $Name -MasterServers $MasterServers -ZoneFile $Name -Force + + $setZoneMessage = $($script:localizedData.SetSecondaryZoneMessage) -f $Name + Write-Verbose -Message $setZoneMessage + } + else + { + return $false + } + } # end zone is not secondary + + }# end ensure -eq present + + # If zone should be absent + else + { + if ($Apply) + { + $removingZoneMessage = $script:localizedData.RemovingZoneMessage + Write-Verbose -Message $removingZoneMessage + + Remove-DnsServerZone -Name $Name -Force + + $deleteZoneMessage = $($script:localizedData.DeleteZoneMessage) -f $Name + Write-Verbose -Message $deleteZoneMessage + } + else + { + return $false + } + } # end ensure -eq absent + + } # end found dns zone + + # Not found DNS Zone + else + { + $testZoneMessage = $($script:localizedData.TestZoneMessage) -f 'absent', $Ensure + Write-Verbose -Message $testZoneMessage + + if ($Ensure -eq 'Present') + { + if ($Apply) + { + $addingSecondaryZoneMessage = $script:localizedData.AddingSecondaryZoneMessage + Write-Verbose -Message $addingSecondaryZoneMessage + + # Add the zone and start the transfer + Add-DnsServerSecondaryZone -Name $Name -MasterServers $MasterServers -ZoneFile $Name + Start-DnsServerZoneTransfer -Name $Name -FullTransfer + + $newSecondaryZoneMessage = $($script:localizedData.NewSecondaryZoneMessage) -f $Name + Write-Verbose -Message $newSecondaryZoneMessage + } + else + { + return $false + } + } # end ensure -eq Present + else + { + if (-not $Apply) + { + return $true + } + } # end ensure -eq Absent + } +} +#endregion + +Export-ModuleMember -Function *-TargetResource diff --git a/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/DSCResources/DSC_DnsServerSecondaryZone/DSC_DnsServerSecondaryZone.schema.mof b/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/DSCResources/DSC_DnsServerSecondaryZone/DSC_DnsServerSecondaryZone.schema.mof new file mode 100644 index 0000000..3f2651e --- /dev/null +++ b/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/DSCResources/DSC_DnsServerSecondaryZone/DSC_DnsServerSecondaryZone.schema.mof @@ -0,0 +1,8 @@ +[ClassVersion("1.0.0.0"), FriendlyName("DnsServerSecondaryZone")] +class DSC_DnsServerSecondaryZone : OMI_BaseResource +{ + [Key, Description("Name of the secondary zone")] String Name; + [Required, Description("IP address or DNS name of the secondary DNS servers")] String MasterServers[]; + [Write, Description("Whether the secondary zone should be present or absent."), ValueMap{"Present","Absent"}, Values{"Present","Absent"}] String Ensure; + [Read, Description("Type of the DNS server zone")] String Type; +}; diff --git a/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/DSCResources/DSC_DnsServerSecondaryZone/README.md b/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/DSCResources/DSC_DnsServerSecondaryZone/README.md new file mode 100644 index 0000000..db16a50 --- /dev/null +++ b/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/DSCResources/DSC_DnsServerSecondaryZone/README.md @@ -0,0 +1,6 @@ +# Description + +The DnsServerSecondaryZone DSC resource manages a standalone file-backed +secondary zone on a Domain Name System (DNS) server. Secondary zones allow +client machine in primary DNS zones to do DNS resolution of machines in the +secondary DNS zone. diff --git a/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/DSCResources/DSC_DnsServerSecondaryZone/en-US/DSC_DnsServerSecondaryZone.strings.psd1 b/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/DSCResources/DSC_DnsServerSecondaryZone/en-US/DSC_DnsServerSecondaryZone.strings.psd1 new file mode 100644 index 0000000..30664cd --- /dev/null +++ b/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/DSCResources/DSC_DnsServerSecondaryZone/en-US/DSC_DnsServerSecondaryZone.strings.psd1 @@ -0,0 +1,17 @@ +# culture="en-US" +ConvertFrom-StringData @' + CheckingZoneMessage = Checking DNS server zone with name {0} ... + TestZoneMessage = Named DNS server zone is {0} and it should be {1} + RemovingZoneMessage = Removing DNS server zone ... + DeleteZoneMessage = DNS server zone {0} is now absent + CheckingSecondaryZoneMessage = Checking if the DNS server zone is a secondary zone ... + AlreadySecondaryZoneMessage = DNS server zone {0} is already a secondary zone + NotSecondaryZoneMessage = DNS server zone {0} is not a secondary zone but {1} zone + AddingSecondaryZoneMessage = Adding secondary DNS server zone ... + NewSecondaryZoneMessage = DNS server secondary zone {0} is now present + SetSecondaryZoneMessage = DNS server zone {0} is now a secondary zone + CheckPropertyMessage = Checking DNS secondary server {0} ... + NotDesiredPropertyMessage = DNS server secondary zone {0} is not correct. Expected {1}, actual {2} + DesiredPropertyMessage = DNS server secondary zone {0} is correct + SetPropertyMessage = DNS server secondary zone {0} is set +'@ diff --git a/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/DSCResources/DSC_DnsServerSecondaryZone/en-US/about_DnsServerSecondaryZone.help.txt b/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/DSCResources/DSC_DnsServerSecondaryZone/en-US/about_DnsServerSecondaryZone.help.txt new file mode 100644 index 0000000..f26d53c --- /dev/null +++ b/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/DSCResources/DSC_DnsServerSecondaryZone/en-US/about_DnsServerSecondaryZone.help.txt @@ -0,0 +1,45 @@ +.NAME + DnsServerSecondaryZone + +.DESCRIPTION + The DnsServerSecondaryZone DSC resource manages a standalone file-backed + secondary zone on a Domain Name System (DNS) server. Secondary zones allow + client machine in primary DNS zones to do DNS resolution of machines in the + secondary DNS zone. + +.PARAMETER Name + Key - String + Name of the secondary zone + +.PARAMETER MasterServers + Required - StringArray + IP address or DNS name of the secondary DNS servers + +.PARAMETER Ensure + Write - String + Allowed values: Present, Absent + Whether the secondary zone should be present or absent. + +.PARAMETER Type + Read - String + Type of the DNS server zone + +.EXAMPLE 1 + +This configuration will manage a secondary standalone DNS zone + +Configuration DnsServerSecondaryZone_config +{ + Import-DscResource -ModuleName 'DnsServerDsc' + + Node localhost + { + DnsServerSecondaryZone 'sec' + { + Ensure = 'Present' + Name = 'demo.contoso.com' + MasterServers = '192.168.10.2' + } + } +} + diff --git a/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/DSCResources/DSC_DnsServerSetting/DSC_DnsServerSetting.psm1 b/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/DSCResources/DSC_DnsServerSetting/DSC_DnsServerSetting.psm1 new file mode 100644 index 0000000..e883959 --- /dev/null +++ b/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/DSCResources/DSC_DnsServerSetting/DSC_DnsServerSetting.psm1 @@ -0,0 +1,1480 @@ +$script:dscResourceCommonPath = Join-Path -Path $PSScriptRoot -ChildPath '..\..\Modules\DscResource.Common' +$script:dnsServerDscCommonPath = Join-Path -Path $PSScriptRoot -ChildPath '..\..\Modules\DnsServerDsc.Common' + +Import-Module -Name $script:dscResourceCommonPath +Import-Module -Name $script:dnsServerDscCommonPath + +$script:localizedData = Get-LocalizedData -DefaultUICulture 'en-US' + +$script:timeSpanProperties = @( + 'LameDelegationTTL' + 'MaximumSignatureScanPeriod' + 'MaximumTrustAnchorActiveRefreshInterval' + 'ZoneWritebackInterval' +) + +<# + .SYNOPSIS + Returns the current state of the DNS server settings. + + .PARAMETER DnsServer + Specifies the DNS server to connect to, or use 'localhost' for the current + node. +#> +function Get-TargetResource +{ + [CmdletBinding()] + [OutputType([System.Collections.Hashtable])] + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $DnsServer + ) + + Assert-Module -ModuleName 'DnsServer' + + Write-Verbose ($script:localizedData.GettingDnsServerSettings) + + $getDnsServerSettingParameters = @{ + All = $true + } + + if ($DnsServer -ne 'localhost') + { + $getDnsServerSettingParameters['ComputerName'] = $DnsServer + } + + $dnsServerInstance = Get-DnsServerSetting @getDnsServerSettingParameters + + $classProperties = @( + 'LocalNetPriority' + 'AutoConfigFileZones' + 'AddressAnswerLimit' + 'UpdateOptions' + 'DisableAutoReverseZone' + 'StrictFileParsing' + 'EnableDirectoryPartitions' + 'XfrConnectTimeout' + 'AllowUpdate' + 'BootMethod' + 'LooseWildcarding' + 'BindSecondaries' + 'AutoCacheUpdate' + 'EnableDnsSec' + 'NameCheckFlag' + 'SendPort' + 'WriteAuthorityNS' + 'ListeningIPAddress' + 'RpcProtocol' + 'RoundRobin' + 'ForwardDelegations' + 'EnableIPv6' + 'EnableOnlineSigning' + 'EnableDuplicateQuerySuppression' + 'AllowCnameAtNs' + 'EnableRsoForRodc' + 'OpenAclOnProxyUpdates' + 'NoUpdateDelegations' + 'EnableUpdateForwarding' + 'EnableWinsR' + 'DeleteOutsideGlue' + 'AppendMsZoneTransferTag' + 'AllowReadOnlyZoneTransfer' + 'EnableSendErrorSuppression' + 'SilentlyIgnoreCnameUpdateConflicts' + 'EnableIQueryResponseGeneration' + 'AdminConfigured' + 'PublishAutoNet' + 'ReloadException' + 'IgnoreServerLevelPolicies' + 'IgnoreAllPolicies' + 'EnableVersionQuery' + 'AutoCreateDelegation' + 'RemoteIPv4RankBoost' + 'RemoteIPv6RankBoost' + 'MaximumRodcRsoQueueLength' + 'MaximumRodcRsoAttemptsPerCycle' + 'MaxResourceRecordsInNonSecureUpdate' + 'LocalNetPriorityMask' + 'TcpReceivePacketSize' + 'SelfTest' + 'XfrThrottleMultiplier' + 'SocketPoolSize' + 'QuietRecvFaultInterval' + 'QuietRecvLogInterval' + 'SyncDsZoneSerial' + 'ScopeOptionValue' + 'VirtualizationInstanceOptionValue' + 'ServerLevelPluginDll' + 'RootTrustAnchorsURL' + 'SocketPoolExcludedPortRanges' + 'LameDelegationTTL' + 'MaximumSignatureScanPeriod' + 'MaximumTrustAnchorActiveRefreshInterval' + 'ZoneWritebackInterval' + + # Read-only properties + 'DsAvailable' + 'MajorVersion' + 'MinorVersion' + 'BuildNumber' + 'IsReadOnlyDC' + 'AllIPAddress' + 'ForestDirectoryPartitionBaseName' + 'DomainDirectoryPartitionBaseName' + 'MaximumUdpPacketSize' + ) + + $returnValue = @{} + + foreach ($property in $classProperties) + { + if ($property -in $script:timeSpanProperties) + { + $returnValue.Add($property, $dnsServerInstance.$property.ToString()) + } + else + { + $returnValue.Add($property, $dnsServerInstance.$property) + } + } + + $returnValue.DnsServer = $DnsServer + + return $returnValue +} + +<# + .SYNOPSIS + Set the desired state of the DNS server settings. + + .PARAMETER DnsServer + Specifies the DNS server to connect to, or use 'localhost' for the current + node. + + .PARAMETER AddressAnswerLimit + Specifies the maximum number of A (host IP address) resource records that + the DNS server can insert in the answer section of a response to an A record + query (a query for an IP address). The value of this entry also influences + the setting of the truncation bit. If the value of this entry can be between + 5 and 28, or 0. The truncation bit is not set on the response, even when the + packet space is exceeded. + + .PARAMETER AllowUpdate + Specifies whether the DNS Server accepts dynamic update requests. $true to + allow any DNS update operation; otherwise, $false. + + .PARAMETER AutoCacheUpdate + Specifies whether the DNS Server attempts to update its cache entries using + data from root servers. $true to cache delegation information; otherwise, + $false. + + .PARAMETER AutoConfigFileZones + Specifies the type of zones for which SOA and NS records will be automatically + configured with the DNS server's local host name as the primary DNS server for + the zone when the zone is loaded from file. + + .PARAMETER BindSecondaries + Specifies whether the server will permit send DNS zone transfer response + messages with more than one record in each response if the zone transfer + request did not have the characters MS appended to it. If set to $true, + the DNS server will include only one record in each response if the zone + transfer request did not have the characters MS appended to it. + + .PARAMETER BootMethod + Specifies the boot method used by the DNS server. + + .PARAMETER DisableAutoReverseZone + Specifies whether the DNS Server automatically creates standard reverse + look up zones. $true to disables automatic reverse zones; otherwise, $false. + + .PARAMETER EnableDirectoryPartitions + Specifies whether the DNS server will support application directory partitions. + + .PARAMETER EnableDnsSec + Specifies whether the DNS Server includes DNSSEC-specific RRs, KEY, SIG, + and NXT in a response. $true to enable DNSSEC validation on the DNS server; + otherwise, $false. + + .PARAMETER ForwardDelegations + Specifies how the DNS server will handle forwarding and delegations. If + set to $true, the DNS server MUST use forwarders instead of a cached + delegation when both are available. Otherwise, the DNS server MUST use a + cached delegation instead of forwarders when both are available. + + .PARAMETER ListeningIPAddress + Specifies the listening IP addresses of the DNS server. The list of IP + addresses on which the DNS Server can receive queries. + + .PARAMETER LocalNetPriority + Specifies whether the DNS Server gives priority to the local net address + when returning A records. $true to return A records in order of their + similarity to the IP address of the querying client.; otherwise, $false. + + .PARAMETER LooseWildcarding + Specifies he type of algorithm that the DNS server will use to locate a + wildcard node when using a DNS wildcard record RFC1034 to answer a query. + If true, the DNS server will use the first node it encounters with a record + of the same type as the query type. Otherwise, the DNS server will use the + first node it encounters that has records of any type. + + .PARAMETER NameCheckFlag + Specifies the level of domain name checking and validation on the DNS server, + the set of eligible characters to be used in DNS names. + + .PARAMETER RoundRobin + Specifies whether the DNS Server round robins multiple A records. $true to + enable Round-robin DNS on the DNS server; otherwise, $false. + + .PARAMETER RpcProtocol + Specifies the DNS_RPC_PROTOCOLS section 2.2.1.1.2 value corresponding to + the RPC protocols to which the DNS server will respond. If this value is + set to 0x00000000, the DNS server MUST NOT respond to RPC requests for + any protocol. + + .PARAMETER SendPort + Specifies the port number to use as the source port when sending UDP queries + to a remote DNS server. If set to zero, the DNS server allow the stack to + select a random port. + + .PARAMETER StrictFileParsing + Specifies whether the DNS server will treat errors encountered while reading + zones from a file as fatal. + + .PARAMETER UpdateOptions + Specifies the DNS update options used by the DNS server. + + .PARAMETER WriteAuthorityNS + Specifies whether the DNS server will include NS records for the root of a + zone in DNS responses that are answered using authoritative zone data. + + .PARAMETER XfrConnectTimeout + Specifies the time span, in seconds, in which a primary DNS server waits + for a transfer response from its secondary server. The default value is 30. + After the time-out value expires, the connection is terminated. + + .PARAMETER EnableIPv6 + Specifies whether IPv6 should be enabled on the DNS Server. $true to enable + IPv6 on the DNS server; otherwise, $false. + + .PARAMETER EnableOnlineSigning + Specifies whether online signing should be enabled on the DNS Server. $true + to enable online signing; otherwise, $false. + + .PARAMETER EnableDuplicateQuerySuppression + Specifies whether the DNS server will not send remote queries when there is + already a remote query with the same name and query type outstanding. + + .PARAMETER AllowCnameAtNs + Specifies whether the server will permit the target domain names of NS records + to resolve to CNAME records. If $true, this pattern of DNS records will be + allowed; otherwise, the DNS server will return errors when encountering this + pattern of DNS records while resolving queries. + + .PARAMETER EnableRsoForRodc + Specifies whether the DNS server will attempt to replicate single updated + DNS objects from remote directory servers ahead of normally scheduled replication + when operating on a directory server that does not support write operations. + + .PARAMETER OpenAclOnProxyUpdates + Specifies whether the DNS server allows sharing of DNS records with the + DnsUpdateProxy group when processing updates in secure zones that are stored + in the directory service. + + .PARAMETER NoUpdateDelegations + Specifies whether the DNS server will accept DNS updates to delegation + records of type NS. + + .PARAMETER EnableUpdateForwarding + Specifies whether the DNS server will forward updates received for secondary + zones to the primary DNS server for the zone. + + .PARAMETER EnableWinsR + Specifies whether the DNS server will perform NetBIOS name resolution in + order to map IP addresses to machine names while processing queries in zones + where WINS-R information has been configured. + + .PARAMETER DeleteOutsideGlue + Specifies whether the DNS server will delete DNS glue records found outside + a delegated subzone when reading records from persistent storage. + + .PARAMETER AppendMsZoneTransferTag + Specifies whether the DNS server will indicate to the remote DNS servers + that it supports multiple DNS records in each zone transfer response message + by appending the characters MS at the end of zone transfer requests. The + value SHOULD be limited to 0x00000000 and 0x0000000, but it MAY be any value. + + .PARAMETER AllowReadOnlyZoneTransfer + Specifies whether the DNS server will allow zone transfers for zones that + are stored in the directory server when the directory server does not support + write operations. + + .PARAMETER EnableSendErrorSuppression + Specifies whether the DNS server will attempt to suppress large volumes + of DNS error responses sent to remote IP addresses that may be attempting + to attack the DNS server. + + .PARAMETER SilentlyIgnoreCnameUpdateConflicts + Specifies whether the DNS server will ignore CNAME conflicts during DNS + update processing. + + .PARAMETER EnableIQueryResponseGeneration + Specifies whether the DNS server will fabricate IQUERY responses. If set + to $true, the DNS server MUST fabricate IQUERY responses when it receives + queries of type IQUERY. Otherwise, the DNS server will return an error when + such queries are received. + + .PARAMETER AdminConfigured + Specifies whether the server has been configured by an administrator. + + .PARAMETER PublishAutoNet + Specifies whether the DNS server will publish local IPv4 addresses in the + 169.254.x.x subnet as IPv4 addresses for the local machine's domain name. + + .PARAMETER ReloadException + Specifies whether the DNS server will perform an internal restart if an + unexpected fatal error is encountered. + + .PARAMETER IgnoreServerLevelPolicies + Specifies whether to ignore the server level policies on the DNS server. + $true to ignore the server level policies on the DNS server; otherwise, + $false. + + .PARAMETER IgnoreAllPolicies + Specifies whether to ignore all policies on the DNS server. $true to ignore + all policies on the DNS server; otherwise, $false. + + .PARAMETER EnableVersionQuery + Specifies what version information the DNS server will respond with when a + DNS query with class set to CHAOS and type set to TXT is received. + + .PARAMETER AutoCreateDelegation + Specifies possible settings for automatic delegation creation for new zones + on the DNS server. The value SHOULD be limited to the range from 0x00000000 + to 0x00000002, inclusive, but it MAY be any value. + + .PARAMETER RemoteIPv4RankBoost + Specifies the value to add to all IPv4 addresses for remote DNS servers when + selecting between IPv4 and IPv6 remote DNS server addresses. The value MUST + be limited to the range from 0x00000000 to 0x0000000A, inclusive. + + .PARAMETER RemoteIPv6RankBoost + Specifies the value to add to all IPv6 addresses for remote DNS servers when + selecting between IPv4 and IPv6 remote DNS server addresses. The value MUST + be limited to the range from 0x00000000 to 0x0000000A, inclusive. + + .PARAMETER MaximumRodcRsoQueueLength + Specifies the maximum number of single object replication operations that + may be queued at any given time by the DNS server. The value MUST be limited + to the range from 0x00000000 to 0x000F4240, inclusive. If the value is + 0x00000000 the DNS server MUST NOT enforce an upper bound on the number of + single object replication operations queued at any given time. + + .PARAMETER MaximumRodcRsoAttemptsPerCycle + Specifies the maximum number of queued single object replication operations + that should be attempted during each five minute interval of DNS server + operation. The value MUST be limited to the range from 0x00000001 to 0x000F4240, + inclusive. + + .PARAMETER MaxResourceRecordsInNonSecureUpdate + Specifies the maximum number of resource records that the DNS server will + accept in a single DNS update request. The value SHOULD be limited to the + range from 0x0000000A to 0x00000078, inclusive, but it MAY be any value. + + .PARAMETER LocalNetPriorityMask + Specifies the value which specifies the network mask the DNS server will + use to sort IPv4 addresses. A value of 0xFFFFFFFF indicates that the DNS + server MUST use traditional IPv4 network mask for the address. Any other + value is a network mask, in host byte order that the DNS server MUST use + to retrieve network masks from IP addresses for sorting purposes. + + .PARAMETER TcpReceivePacketSize + Specifies the maximum TCP packet size, in bytes, that the DNS server can + accept. The value MUST be limited to the range from 0x00004000 to 0x00010000, + inclusive. + + .PARAMETER SelfTest + Specifies the mask value indicating whether data consistency checking + should be performed once, each time the service starts. If the check fails, + the server posts an event log warning. If the least significant bit (regardless + of other bits) of this value is one, the DNS server will verify for each + active and update-allowing primary zone, that the IP address records are + present in the zone for the zone's SOA record's master server. If the + least significant bit (regardless of other bits) of this value is zero, + no data consistency checking will be performed. + + .PARAMETER XfrThrottleMultiplier + Specifies the multiple used to determine how long the DNS server should + refuse zone transfer requests after a successful zone transfer has been + completed. The total time for which a zone will refuse another zone + transfer request at the end of a successful zone transfer is computed as + this value multiplied by the number of seconds required for the zone + transfer that just completed. The server SHOULD refuse zone transfer + requests for no more than ten minutes. The value SHOULD be limited to + the range from 0x00000000 to 0x00000064, inclusive, but it MAY be any + value. + + .PARAMETER SocketPoolSize + Specifies the number of UDP sockets per address family that the DNS server + will use for sending remote queries. + + .PARAMETER QuietRecvFaultInterval + Specifies the minimum time interval, in seconds, starting when the server + begins waiting for the query to arrive on the network, after which the + server MAY log a debug message indicating that the server is to stop running. + If the value is zero or is less than the value of QuietRecvLogInterval*, + then the value of QuietRecvLogInterval MUST be used. If the value is + greater than or equal to the value of QuietRecvLogInterval, then the + literal value of QuietRecvFaultInterval MUST be used. Used to debug + reception of UDP traffic for a recursive query. + + .PARAMETER QuietRecvLogInterval + Specifies the minimum time interval, in seconds, starting when the server + begins waiting for the query to arrive on the network, or when the server + logs an eponymous debug message for the query, after which the server MUST + log a debug message indicating that the server is still waiting to receive + network traffic. If the value is zero, logging associated with the two + QuietRecv properties MUST be disabled, and the QuietRecvFaultInterval + property MUST be ignored. If the value is non-zero, logging associated with + the two QuietRecv properties MUST be enabled, and the QuietRecvFaultInterval + property MUST NOT be ignored. Used to debug reception of UDP traffic for a + recursive query. + + .PARAMETER SyncDsZoneSerial + Specifies the conditions under which the DNS server should immediately + commit uncommitted zone serial numbers to persistent storage. The value + SHOULD be limited to the range from 0x00000000 to 0x00000004, inclusive, + but it MAY be any value. + + .PARAMETER ScopeOptionValue + Specifies the extension mechanism for the DNS (ENDS0) scope setting on the + DNS server. + + .PARAMETER VirtualizationInstanceOptionValue + Specifies the virtualization instance option to be sent in ENDS0. + + .PARAMETER ServerLevelPluginDll + Specifies the path of a custom plug-in. When DllPath specifies the fully + qualified path name of a valid DNS server plug-in, the DNS server calls + functions in the plug-in to resolve name queries that are outside the + scope of all locally hosted zones. If a queried name is out of the scope + of the plug-in, the DNS server performs name resolution using forwarding + or recursion, as configured. If DllPath is not specified, the DNS server + ceases to use a custom plug-in if a custom plug-in was previously configured. + + .PARAMETER RootTrustAnchorsURL + Specifies the URL of the root trust anchor on the DNS server. + + .PARAMETER SocketPoolExcludedPortRanges + Specifies the port ranges that should be excluded. + + .PARAMETER LameDelegationTTL + Specifies the time span that must elapse before the DNS server will re-query + DNS servers of the parent zone when a lame delegation is encountered. The + value SHOULD be limited to the range from 0x00000000 to 0x00278D00 30 days, + inclusive, but it MAY be any value. + + .PARAMETER MaximumSignatureScanPeriod + Specifies the maximum period between zone scans to update DnsSec signatures + for resource records. + + .PARAMETER MaximumTrustAnchorActiveRefreshInterval + Specifies the maximum value for the active refresh interval for a trust + anchor. Must not be higher than 15 days. + + .PARAMETER ZoneWritebackInterval + Specifies the zone write back interval for file backed zones. +#> +function Set-TargetResource +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $DnsServer, + + [Parameter()] + [System.UInt32] + $AddressAnswerLimit, + + [Parameter()] + [System.Boolean] + $AllowUpdate, + + [Parameter()] + [System.Boolean] + $AutoCacheUpdate, + + [Parameter()] + [System.UInt32] + $AutoConfigFileZones, + + [Parameter()] + [System.Boolean] + $BindSecondaries, + + [Parameter()] + [System.UInt32] + $BootMethod, + + [Parameter()] + [System.Boolean] + $DisableAutoReverseZone, + + [Parameter()] + [System.Boolean] + $EnableDirectoryPartitions, + + [Parameter()] + [System.Boolean] + $EnableDnsSec, + + [Parameter()] + [System.Boolean] + $ForwardDelegations, + + [Parameter()] + [System.String[]] + $ListeningIPAddress, + + [Parameter()] + [System.Boolean] + $LocalNetPriority, + + [Parameter()] + [System.Boolean] + $LooseWildcarding, + + [Parameter()] + [System.UInt32] + $NameCheckFlag, + + [Parameter()] + [System.Boolean] + $RoundRobin, + + [Parameter()] + [System.UInt32] + $RpcProtocol, + + [Parameter()] + [System.UInt32] + $SendPort, + + [Parameter()] + [System.Boolean] + $StrictFileParsing, + + [Parameter()] + [System.UInt32] + $UpdateOptions, + + [Parameter()] + [System.Boolean] + $WriteAuthorityNS, + + [Parameter()] + [System.UInt32] + $XfrConnectTimeout, + + [Parameter()] + [System.Boolean] + $EnableIPv6, + + [Parameter()] + [System.Boolean] + $EnableOnlineSigning, + + [Parameter()] + [System.Boolean] + $EnableDuplicateQuerySuppression, + + [Parameter()] + [System.Boolean] + $AllowCnameAtNs, + + [Parameter()] + [System.Boolean] + $EnableRsoForRodc, + + [Parameter()] + [System.Boolean] + $OpenAclOnProxyUpdates, + + [Parameter()] + [System.Boolean] + $NoUpdateDelegations, + + [Parameter()] + [System.Boolean] + $EnableUpdateForwarding, + + [Parameter()] + [System.Boolean] + $EnableWinsR, + + [Parameter()] + [System.Boolean] + $DeleteOutsideGlue, + + [Parameter()] + [System.Boolean] + $AppendMsZoneTransferTag, + + [Parameter()] + [System.Boolean] + $AllowReadOnlyZoneTransfer, + + [Parameter()] + [System.Boolean] + $EnableSendErrorSuppression, + + [Parameter()] + [System.Boolean] + $SilentlyIgnoreCnameUpdateConflicts, + + [Parameter()] + [System.Boolean] + $EnableIQueryResponseGeneration, + + [Parameter()] + [System.Boolean] + $AdminConfigured, + + [Parameter()] + [System.Boolean] + $PublishAutoNet, + + [Parameter()] + [System.Boolean] + $ReloadException, + + [Parameter()] + [System.Boolean] + $IgnoreServerLevelPolicies, + + [Parameter()] + [System.Boolean] + $IgnoreAllPolicies, + + [Parameter()] + [System.UInt32] + $EnableVersionQuery, + + [Parameter()] + [System.UInt32] + $AutoCreateDelegation, + + [Parameter()] + [System.UInt32] + $RemoteIPv4RankBoost, + + [Parameter()] + [System.UInt32] + $RemoteIPv6RankBoost, + + [Parameter()] + [System.UInt32] + $MaximumRodcRsoQueueLength, + + [Parameter()] + [System.UInt32] + $MaximumRodcRsoAttemptsPerCycle, + + [Parameter()] + [System.UInt32] + $MaxResourceRecordsInNonSecureUpdate, + + [Parameter()] + [System.UInt32] + $LocalNetPriorityMask, + + [Parameter()] + [System.UInt32] + $TcpReceivePacketSize, + + [Parameter()] + [System.UInt32] + $SelfTest, + + [Parameter()] + [System.UInt32] + $XfrThrottleMultiplier, + + [Parameter()] + [System.UInt32] + $SocketPoolSize, + + [Parameter()] + [System.UInt32] + $QuietRecvFaultInterval, + + [Parameter()] + [System.UInt32] + $QuietRecvLogInterval, + + [Parameter()] + [System.UInt32] + $SyncDsZoneSerial, + + [Parameter()] + [System.UInt32] + $ScopeOptionValue, + + [Parameter()] + [System.UInt32] + $VirtualizationInstanceOptionValue, + + [Parameter()] + [System.String] + $ServerLevelPluginDll, + + [Parameter()] + [System.String] + $RootTrustAnchorsURL, + + [Parameter()] + [System.String[]] + $SocketPoolExcludedPortRanges, + + [Parameter()] + [System.String] + $LameDelegationTTL, + + [Parameter()] + [System.String] + $MaximumSignatureScanPeriod, + + [Parameter()] + [System.String] + $MaximumTrustAnchorActiveRefreshInterval, + + [Parameter()] + [System.String] + $ZoneWritebackInterval + ) + + Assert-Module -ModuleName 'DnsServer' + + $PSBoundParameters.Remove('DnsServer') + + $dnsProperties = Remove-CommonParameter -Hashtable $PSBoundParameters + + $getDnServerSettingResult = Get-DnsServerSetting -All + + $propertiesInDesiredState = @() + + foreach ($property in $dnsProperties.keys) + { + if ($property -in ('ListeningIPAddress', 'SocketPoolExcludedPortRanges')) + { + # Compare array + + $compareObjectParameters = @{ + ReferenceObject = $dnsProperties.$property + DifferenceObject = $getDnServerSettingResult.$property + } + + $isPropertyInDesiredState = -not (Compare-Object @compareObjectParameters) + } + else + { + $isPropertyInDesiredState = $dnsProperties.$property -eq $getDnServerSettingResult.$property + } + + if ($isPropertyInDesiredState) + { + # Property in desired state. + + Write-Verbose -Message ($script:localizedData.PropertyInDesiredState -f $property) + + $propertiesInDesiredState += $property + + } + else + { + # Property not in desired state. + + Write-Verbose -Message ($script:localizedData.SetDnsServerSetting -f $property, ($dnsProperties[$property] -join ', ')) + } + } + + # Remove passed parameters that are in desired state. + $propertiesInDesiredState | ForEach-Object -Process { + $dnsProperties.Remove($_) + } + + if ($dnsProperties.Keys.Count -eq 0) + { + Write-Verbose -Message $script:localizedData.SettingsInDesiredState + } + else + { + # Set all desired values for the properties that were not in desired state. + $dnsProperties.Keys | ForEach-Object -Process { + $property = $_ + + if ($property -in $script:timeSpanProperties) + { + $timeSpan = New-TimeSpan + + <# + When this resource is converted to a class-based resource this should + be replaced by private function ConvertTo-TimeSpan. + #> + if (-not [System.TimeSpan]::TryParse($dnsProperties.$property, [ref] $timeSpan)) + { + throw ($script:localizedData.UnableToParseTimeSpan -f $dnsProperties.$property, $property ) + } + + $getDnServerSettingResult.$property = $timeSpan + } + else + { + $getDnServerSettingResult.$property = $dnsProperties.$property + } + } + + $setDnServerSettingParameters = @{ + ErrorAction = 'Stop' + } + + if ($DnsServer -ne 'localhost') + { + $setDnServerSettingParameters['ComputerName'] = $DnsServer + } + + $getDnServerSettingResult | Set-DnsServerSetting @setDnServerSettingParameters + } +} + +<# + .SYNOPSIS + Tests the desired state of the DNS server settings. + + .PARAMETER DnsServer + Specifies the DNS server to connect to, or use 'localhost' for the current + node. + + .PARAMETER AddressAnswerLimit + Specifies the maximum number of A (host IP address) resource records that + the DNS server can insert in the answer section of a response to an A record + query (a query for an IP address). The value of this entry also influences + the setting of the truncation bit. If the value of this entry can be between + 5 and 28, or 0. The truncation bit is not set on the response, even when the + packet space is exceeded. + + .PARAMETER AllowUpdate + Specifies whether the DNS Server accepts dynamic update requests. $true to + allow any DNS update operation; otherwise, $false. + + .PARAMETER AutoCacheUpdate + Specifies whether the DNS Server attempts to update its cache entries using + data from root servers. $true to cache delegation information; otherwise, + $false. + + .PARAMETER AutoConfigFileZones + Specifies the type of zones for which SOA and NS records will be automatically + configured with the DNS server's local host name as the primary DNS server for + the zone when the zone is loaded from file. + + .PARAMETER BindSecondaries + Specifies whether the server will permit send DNS zone transfer response + messages with more than one record in each response if the zone transfer + request did not have the characters MS appended to it. If set to $true, + the DNS server will include only one record in each response if the zone + transfer request did not have the characters MS appended to it. + + .PARAMETER BootMethod + Specifies the boot method used by the DNS server. + + .PARAMETER DisableAutoReverseZone + Specifies whether the DNS Server automatically creates standard reverse + look up zones. $true to disables automatic reverse zones; otherwise, $false. + + .PARAMETER EnableDirectoryPartitions + Specifies whether the DNS server will support application directory partitions. + + .PARAMETER EnableDnsSec + Specifies whether the DNS Server includes DNSSEC-specific RRs, KEY, SIG, + and NXT in a response. $true to enable DNSSEC validation on the DNS server; + otherwise, $false. + + .PARAMETER ForwardDelegations + Specifies how the DNS server will handle forwarding and delegations. If + set to $true, the DNS server MUST use forwarders instead of a cached + delegation when both are available. Otherwise, the DNS server MUST use a + cached delegation instead of forwarders when both are available. + + .PARAMETER ListeningIPAddress + Specifies the listening IP addresses of the DNS server. The list of IP + addresses on which the DNS Server can receive queries. + + .PARAMETER LocalNetPriority + Specifies whether the DNS Server gives priority to the local net address + when returning A records. $true to return A records in order of their + similarity to the IP address of the querying client.; otherwise, $false. + + .PARAMETER LooseWildcarding + Specifies he type of algorithm that the DNS server will use to locate a + wildcard node when using a DNS wildcard record RFC1034 to answer a query. + If true, the DNS server will use the first node it encounters with a record + of the same type as the query type. Otherwise, the DNS server will use the + first node it encounters that has records of any type. + + .PARAMETER NameCheckFlag + Specifies the level of domain name checking and validation on the DNS server, + the set of eligible characters to be used in DNS names. + + .PARAMETER RoundRobin + Specifies whether the DNS Server round robins multiple A records. $true to + enable Round-robin DNS on the DNS server; otherwise, $false. + + .PARAMETER RpcProtocol + Specifies the DNS_RPC_PROTOCOLS section 2.2.1.1.2 value corresponding to + the RPC protocols to which the DNS server will respond. If this value is + set to 0x00000000, the DNS server MUST NOT respond to RPC requests for + any protocol. + + .PARAMETER SendPort + Specifies the port number to use as the source port when sending UDP queries + to a remote DNS server. If set to zero, the DNS server allow the stack to + select a random port. + + .PARAMETER StrictFileParsing + Specifies whether the DNS server will treat errors encountered while reading + zones from a file as fatal. + + .PARAMETER UpdateOptions + Specifies the DNS update options used by the DNS server. + + .PARAMETER WriteAuthorityNS + Specifies whether the DNS server will include NS records for the root of a + zone in DNS responses that are answered using authoritative zone data. + + .PARAMETER XfrConnectTimeout + Specifies the time span, in seconds, in which a primary DNS server waits + for a transfer response from its secondary server. The default value is 30. + After the time-out value expires, the connection is terminated. + + .PARAMETER EnableIPv6 + Specifies whether IPv6 should be enabled on the DNS Server. $true to enable + IPv6 on the DNS server; otherwise, $false. + + .PARAMETER EnableOnlineSigning + Specifies whether online signing should be enabled on the DNS Server. $true + to enable online signing; otherwise, $false. + + .PARAMETER EnableDuplicateQuerySuppression + Specifies whether the DNS server will not send remote queries when there is + already a remote query with the same name and query type outstanding. + + .PARAMETER AllowCnameAtNs + Specifies whether the server will permit the target domain names of NS records + to resolve to CNAME records. If $true, this pattern of DNS records will be + allowed; otherwise, the DNS server will return errors when encountering this + pattern of DNS records while resolving queries. + + .PARAMETER EnableRsoForRodc + Specifies whether the DNS server will attempt to replicate single updated + DNS objects from remote directory servers ahead of normally scheduled replication + when operating on a directory server that does not support write operations. + + .PARAMETER OpenAclOnProxyUpdates + Specifies whether the DNS server allows sharing of DNS records with the + DnsUpdateProxy group when processing updates in secure zones that are stored + in the directory service. + + .PARAMETER NoUpdateDelegations + Specifies whether the DNS server will accept DNS updates to delegation + records of type NS. + + .PARAMETER EnableUpdateForwarding + Specifies whether the DNS server will forward updates received for secondary + zones to the primary DNS server for the zone. + + .PARAMETER EnableWinsR + Specifies whether the DNS server will perform NetBIOS name resolution in + order to map IP addresses to machine names while processing queries in zones + where WINS-R information has been configured. + + .PARAMETER DeleteOutsideGlue + Specifies whether the DNS server will delete DNS glue records found outside + a delegated subzone when reading records from persistent storage. + + .PARAMETER AppendMsZoneTransferTag + Specifies whether the DNS server will indicate to the remote DNS servers + that it supports multiple DNS records in each zone transfer response message + by appending the characters MS at the end of zone transfer requests. The + value SHOULD be limited to 0x00000000 and 0x0000000, but it MAY be any value. + + .PARAMETER AllowReadOnlyZoneTransfer + Specifies whether the DNS server will allow zone transfers for zones that + are stored in the directory server when the directory server does not support + write operations. + + .PARAMETER EnableSendErrorSuppression + Specifies whether the DNS server will attempt to suppress large volumes + of DNS error responses sent to remote IP addresses that may be attempting + to attack the DNS server. + + .PARAMETER SilentlyIgnoreCnameUpdateConflicts + Specifies whether the DNS server will ignore CNAME conflicts during DNS + update processing. + + .PARAMETER EnableIQueryResponseGeneration + Specifies whether the DNS server will fabricate IQUERY responses. If set + to $true, the DNS server MUST fabricate IQUERY responses when it receives + queries of type IQUERY. Otherwise, the DNS server will return an error when + such queries are received. + + .PARAMETER AdminConfigured + Specifies whether the server has been configured by an administrator. + + .PARAMETER PublishAutoNet + Specifies whether the DNS server will publish local IPv4 addresses in the + 169.254.x.x subnet as IPv4 addresses for the local machine's domain name. + + .PARAMETER ReloadException + Specifies whether the DNS server will perform an internal restart if an + unexpected fatal error is encountered. + + .PARAMETER IgnoreServerLevelPolicies + Specifies whether to ignore the server level policies on the DNS server. + $true to ignore the server level policies on the DNS server; otherwise, + $false. + + .PARAMETER IgnoreAllPolicies + Specifies whether to ignore all policies on the DNS server. $true to ignore + all policies on the DNS server; otherwise, $false. + + .PARAMETER EnableVersionQuery + Specifies what version information the DNS server will respond with when a + DNS query with class set to CHAOS and type set to TXT is received. + + .PARAMETER AutoCreateDelegation + Specifies possible settings for automatic delegation creation for new zones + on the DNS server. The value SHOULD be limited to the range from 0x00000000 + to 0x00000002, inclusive, but it MAY be any value. + + .PARAMETER RemoteIPv4RankBoost + Specifies the value to add to all IPv4 addresses for remote DNS servers when + selecting between IPv4 and IPv6 remote DNS server addresses. The value MUST + be limited to the range from 0x00000000 to 0x0000000A, inclusive. + + .PARAMETER RemoteIPv6RankBoost + Specifies the value to add to all IPv6 addresses for remote DNS servers when + selecting between IPv4 and IPv6 remote DNS server addresses. The value MUST + be limited to the range from 0x00000000 to 0x0000000A, inclusive. + + .PARAMETER MaximumRodcRsoQueueLength + Specifies the maximum number of single object replication operations that + may be queued at any given time by the DNS server. The value MUST be limited + to the range from 0x00000000 to 0x000F4240, inclusive. If the value is + 0x00000000 the DNS server MUST NOT enforce an upper bound on the number of + single object replication operations queued at any given time. + + .PARAMETER MaximumRodcRsoAttemptsPerCycle + Specifies the maximum number of queued single object replication operations + that should be attempted during each five minute interval of DNS server + operation. The value MUST be limited to the range from 0x00000001 to 0x000F4240, + inclusive. + + .PARAMETER MaxResourceRecordsInNonSecureUpdate + Specifies the maximum number of resource records that the DNS server will + accept in a single DNS update request. The value SHOULD be limited to the + range from 0x0000000A to 0x00000078, inclusive, but it MAY be any value. + + .PARAMETER LocalNetPriorityMask + Specifies the value which specifies the network mask the DNS server will + use to sort IPv4 addresses. A value of 0xFFFFFFFF indicates that the DNS + server MUST use traditional IPv4 network mask for the address. Any other + value is a network mask, in host byte order that the DNS server MUST use + to retrieve network masks from IP addresses for sorting purposes. + + .PARAMETER TcpReceivePacketSize + Specifies the maximum TCP packet size, in bytes, that the DNS server can + accept. The value MUST be limited to the range from 0x00004000 to 0x00010000, + inclusive. + + .PARAMETER SelfTest + Specifies the mask value indicating whether data consistency checking + should be performed once, each time the service starts. If the check fails, + the server posts an event log warning. If the least significant bit (regardless + of other bits) of this value is one, the DNS server will verify for each + active and update-allowing primary zone, that the IP address records are + present in the zone for the zone's SOA record's master server. If the + least significant bit (regardless of other bits) of this value is zero, + no data consistency checking will be performed. + + .PARAMETER XfrThrottleMultiplier + Specifies the multiple used to determine how long the DNS server should + refuse zone transfer requests after a successful zone transfer has been + completed. The total time for which a zone will refuse another zone + transfer request at the end of a successful zone transfer is computed as + this value multiplied by the number of seconds required for the zone + transfer that just completed. The server SHOULD refuse zone transfer + requests for no more than ten minutes. The value SHOULD be limited to + the range from 0x00000000 to 0x00000064, inclusive, but it MAY be any + value. + + .PARAMETER SocketPoolSize + Specifies the number of UDP sockets per address family that the DNS server + will use for sending remote queries. + + .PARAMETER QuietRecvFaultInterval + Specifies the minimum time interval, in seconds, starting when the server + begins waiting for the query to arrive on the network, after which the + server MAY log a debug message indicating that the server is to stop running. + If the value is zero or is less than the value of QuietRecvLogInterval*, + then the value of QuietRecvLogInterval MUST be used. If the value is + greater than or equal to the value of QuietRecvLogInterval, then the + literal value of QuietRecvFaultInterval MUST be used. Used to debug + reception of UDP traffic for a recursive query. + + .PARAMETER QuietRecvLogInterval + Specifies the minimum time interval, in seconds, starting when the server + begins waiting for the query to arrive on the network, or when the server + logs an eponymous debug message for the query, after which the server MUST + log a debug message indicating that the server is still waiting to receive + network traffic. If the value is zero, logging associated with the two + QuietRecv properties MUST be disabled, and the QuietRecvFaultInterval + property MUST be ignored. If the value is non-zero, logging associated with + the two QuietRecv properties MUST be enabled, and the QuietRecvFaultInterval + property MUST NOT be ignored. Used to debug reception of UDP traffic for a + recursive query. + + .PARAMETER SyncDsZoneSerial + Specifies the conditions under which the DNS server should immediately + commit uncommitted zone serial numbers to persistent storage. The value + SHOULD be limited to the range from 0x00000000 to 0x00000004, inclusive, + but it MAY be any value. + + .PARAMETER ScopeOptionValue + Specifies the extension mechanism for the DNS (ENDS0) scope setting on the + DNS server. + + .PARAMETER VirtualizationInstanceOptionValue + Specifies the virtualization instance option to be sent in ENDS0. + + .PARAMETER ServerLevelPluginDll + Specifies the path of a custom plug-in. When DllPath specifies the fully + qualified path name of a valid DNS server plug-in, the DNS server calls + functions in the plug-in to resolve name queries that are outside the + scope of all locally hosted zones. If a queried name is out of the scope + of the plug-in, the DNS server performs name resolution using forwarding + or recursion, as configured. If DllPath is not specified, the DNS server + ceases to use a custom plug-in if a custom plug-in was previously configured. + + .PARAMETER RootTrustAnchorsURL + Specifies the URL of the root trust anchor on the DNS server. + + .PARAMETER SocketPoolExcludedPortRanges + Specifies the port ranges that should be excluded. + + .PARAMETER LameDelegationTTL + Specifies the time span that must elapse before the DNS server will re-query + DNS servers of the parent zone when a lame delegation is encountered. The + value SHOULD be limited to the range from 0x00000000 to 0x00278D00 30 days, + inclusive, but it MAY be any value. + + .PARAMETER MaximumSignatureScanPeriod + Specifies the maximum period between zone scans to update DnsSec signatures + for resource records. + + .PARAMETER MaximumTrustAnchorActiveRefreshInterval + Specifies the maximum value for the active refresh interval for a trust + anchor. Must not be higher than 15 days. + + .PARAMETER ZoneWritebackInterval + Specifies the zone write back interval for file backed zones. +#> +function Test-TargetResource +{ + [CmdletBinding()] + [OutputType([System.Boolean])] + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $DnsServer, + + [Parameter()] + [System.UInt32] + $AddressAnswerLimit, + + [Parameter()] + [System.Boolean] + $AllowUpdate, + + [Parameter()] + [System.Boolean] + $AutoCacheUpdate, + + [Parameter()] + [System.UInt32] + $AutoConfigFileZones, + + [Parameter()] + [System.Boolean] + $BindSecondaries, + + [Parameter()] + [System.UInt32] + $BootMethod, + + [Parameter()] + [System.Boolean] + $DisableAutoReverseZone, + + [Parameter()] + [System.Boolean] + $EnableDirectoryPartitions, + + [Parameter()] + [System.Boolean] + $EnableDnsSec, + + [Parameter()] + [System.Boolean] + $ForwardDelegations, + + [Parameter()] + [System.String[]] + $ListeningIPAddress, + + [Parameter()] + [System.Boolean] + $LocalNetPriority, + + [Parameter()] + [System.Boolean] + $LooseWildcarding, + + [Parameter()] + [System.UInt32] + $NameCheckFlag, + + [Parameter()] + [System.Boolean] + $RoundRobin, + + [Parameter()] + [System.UInt32] + $RpcProtocol, + + [Parameter()] + [System.UInt32] + $SendPort, + + [Parameter()] + [System.Boolean] + $StrictFileParsing, + + [Parameter()] + [System.UInt32] + $UpdateOptions, + + [Parameter()] + [System.Boolean] + $WriteAuthorityNS, + + [Parameter()] + [System.UInt32] + $XfrConnectTimeout, + + [Parameter()] + [System.Boolean] + $EnableIPv6, + + [Parameter()] + [System.Boolean] + $EnableOnlineSigning, + + [Parameter()] + [System.Boolean] + $EnableDuplicateQuerySuppression, + + [Parameter()] + [System.Boolean] + $AllowCnameAtNs, + + [Parameter()] + [System.Boolean] + $EnableRsoForRodc, + + [Parameter()] + [System.Boolean] + $OpenAclOnProxyUpdates, + + [Parameter()] + [System.Boolean] + $NoUpdateDelegations, + + [Parameter()] + [System.Boolean] + $EnableUpdateForwarding, + + [Parameter()] + [System.Boolean] + $EnableWinsR, + + [Parameter()] + [System.Boolean] + $DeleteOutsideGlue, + + [Parameter()] + [System.Boolean] + $AppendMsZoneTransferTag, + + [Parameter()] + [System.Boolean] + $AllowReadOnlyZoneTransfer, + + [Parameter()] + [System.Boolean] + $EnableSendErrorSuppression, + + [Parameter()] + [System.Boolean] + $SilentlyIgnoreCnameUpdateConflicts, + + [Parameter()] + [System.Boolean] + $EnableIQueryResponseGeneration, + + [Parameter()] + [System.Boolean] + $AdminConfigured, + + [Parameter()] + [System.Boolean] + $PublishAutoNet, + + [Parameter()] + [System.Boolean] + $ReloadException, + + [Parameter()] + [System.Boolean] + $IgnoreServerLevelPolicies, + + [Parameter()] + [System.Boolean] + $IgnoreAllPolicies, + + [Parameter()] + [System.UInt32] + $EnableVersionQuery, + + [Parameter()] + [System.UInt32] + $AutoCreateDelegation, + + [Parameter()] + [System.UInt32] + $RemoteIPv4RankBoost, + + [Parameter()] + [System.UInt32] + $RemoteIPv6RankBoost, + + [Parameter()] + [System.UInt32] + $MaximumRodcRsoQueueLength, + + [Parameter()] + [System.UInt32] + $MaximumRodcRsoAttemptsPerCycle, + + [Parameter()] + [System.UInt32] + $MaxResourceRecordsInNonSecureUpdate, + + [Parameter()] + [System.UInt32] + $LocalNetPriorityMask, + + [Parameter()] + [System.UInt32] + $TcpReceivePacketSize, + + [Parameter()] + [System.UInt32] + $SelfTest, + + [Parameter()] + [System.UInt32] + $XfrThrottleMultiplier, + + [Parameter()] + [System.UInt32] + $SocketPoolSize, + + [Parameter()] + [System.UInt32] + $QuietRecvFaultInterval, + + [Parameter()] + [System.UInt32] + $QuietRecvLogInterval, + + [Parameter()] + [System.UInt32] + $SyncDsZoneSerial, + + [Parameter()] + [System.UInt32] + $ScopeOptionValue, + + [Parameter()] + [System.UInt32] + $VirtualizationInstanceOptionValue, + + [Parameter()] + [System.String] + $ServerLevelPluginDll, + + [Parameter()] + [System.String] + $RootTrustAnchorsURL, + + [Parameter()] + [System.String[]] + $SocketPoolExcludedPortRanges, + + [Parameter()] + [System.String] + $LameDelegationTTL, + + [Parameter()] + [System.String] + $MaximumSignatureScanPeriod, + + [Parameter()] + [System.String] + $MaximumTrustAnchorActiveRefreshInterval, + + [Parameter()] + [System.String] + $ZoneWritebackInterval + ) + + Write-Verbose -Message $script:localizedData.EvaluatingDnsServerSettings + + $currentState = Get-TargetResource -DnsServer $DnsServer + + $null = $PSBoundParameters.Remove('DnsServer') + + $result = $true + + # Returns an item for each property that is not in desired state. + if (Compare-DscParameterState -CurrentValues $currentState -DesiredValues $PSBoundParameters -Verbose:$VerbosePreference) + { + $result = $false + } + + return $result +} diff --git a/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/DSCResources/DSC_DnsServerSetting/DSC_DnsServerSetting.schema.mof b/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/DSCResources/DSC_DnsServerSetting/DSC_DnsServerSetting.schema.mof new file mode 100644 index 0000000..c313e88 --- /dev/null +++ b/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/DSCResources/DSC_DnsServerSetting/DSC_DnsServerSetting.schema.mof @@ -0,0 +1,79 @@ +[ClassVersion("1.0.0.0"), FriendlyName("DnsServerSetting")] +class DSC_DnsServerSetting : OMI_BaseResource +{ + [Key, Description("Specifies the DNS server to connect to, or use 'localhost' for the current node.")] String DnsServer; + [Write, Description("Specifies the maximum number of A (host IP address) resource records that the DNS server can insert in the answer section of a response to an A record query (a query for an IP address). The value of this entry also influences the setting of the truncation bit. If the value of this entry can be between `5` and `28`, or `0`. The truncation bit is not set on the response, even when the packet space is exceeded.")] UInt32 AddressAnswerLimit; + [Write, Description("Specifies whether the DNS Server accepts dynamic update requests. `$true` to allow any DNS update operation; otherwise, `$false`.")] Boolean AllowUpdate; + [Write, Description("Specifies whether the DNS Server attempts to update its cache entries using data from root servers. `$true` to cache delegation information; otherwise, `$false`.")] Boolean AutoCacheUpdate; + [Write, Description("Specifies the type of zones for which SOA and NS records will be automatically configured with the DNS server's local host name as the primary DNS server for the zone when the zone is loaded from file.")] UInt32 AutoConfigFileZones; + [Write, Description("Specifies whether the server will permit send DNS zone transfer response messages with more than one record in each response if the zone transfer request did not have the characters MS appended to it. If set to `$true`, the DNS server will include only one record in each response if the zone transfer request did not have the characters MS appended to it.")] Boolean BindSecondaries; + [Write, Description("Specifies the boot method used by the DNS server.")] UInt32 BootMethod; + [Write, Description("Specifies whether the DNS Server automatically creates standard reverse look up zones. `$true` to disables automatic reverse zones; otherwise, `$false`.")] Boolean DisableAutoReverseZone; + [Write, Description("Specifies whether the DNS server will support application directory partitions.")] Boolean EnableDirectoryPartitions; + [Write, Description("Specifies whether the DNS Server includes DNSSEC-specific RRs, KEY, SIG, and NXT in a response. `$true` to enable DNSSEC validation on the DNS server; otherwise, `$false`.")] Boolean EnableDnsSec; + [Write, Description("Specifies how the DNS server will handle forwarding and delegations. If set to `$true`, the DNS server MUST use forwarders instead of a cached delegation when both are available. Otherwise, the DNS server MUST use a cached delegation instead of forwarders when both are available.")] Boolean ForwardDelegations; + [Write, Description("Specifies the listening IP addresses of the DNS server. The list of IP addresses on which the DNS Server can receive queries.")] String ListeningIPAddress[]; + [Write, Description("Specifies whether the DNS Server gives priority to the local net address when returning A records. `$true` to return A records in order of their similarity to the IP address of the querying client.; otherwise, `$false`.")] Boolean LocalNetPriority; + [Write, Description("Specifies he type of algorithm that the DNS server will use to locate a wildcard node when using a DNS wildcard record RFC1034 to answer a query. If true, the DNS server will use the first node it encounters with a record of the same type as the query type. Otherwise, the DNS server will use the first node it encounters that has records of any type.")] Boolean LooseWildcarding; + [Write, Description("Specifies the level of domain name checking and validation on the DNS server, the set of eligible characters to be used in DNS names.")] UInt32 NameCheckFlag; + [Write, Description("Specifies whether the DNS Server round robins multiple A records. `$true` to enable Round-robin DNS on the DNS server; otherwise, `$false`.")] Boolean RoundRobin; + [Write, Description("Specifies the DNS_RPC_PROTOCOLS section 2.2.1.1.2 value corresponding to the RPC protocols to which the DNS server will respond. If this value is set to `0x00000000`, the DNS server MUST NOT respond to RPC requests for any protocol.")] UInt32 RpcProtocol; + [Write, Description("Specifies the port number to use as the source port when sending UDP queries to a remote DNS server. If set to zero, the DNS server allow the stack to select a random port.")] UInt32 SendPort; + [Write, Description("Specifies whether the DNS server will treat errors encountered while reading zones from a file as fatal.")] Boolean StrictFileParsing; + [Write, Description("Specifies the DNS update options used by the DNS server.")] UInt32 UpdateOptions; + [Write, Description("Specifies whether the DNS server will include NS records for the root of a zone in DNS responses that are answered using authoritative zone data.")] Boolean WriteAuthorityNS; + [Write, Description("Specifies the time span, in seconds, in which a primary DNS server waits for a transfer response from its secondary server. The default value is `30`. After the time-out value expires, the connection is terminated.")] UInt32 XfrConnectTimeout; + [Write, Description("Specifies whether IPv6 should be enabled on the DNS Server. `$true` to enable IPv6 on the DNS server; otherwise, `$false`.")] Boolean EnableIPv6; + [Write, Description("Specifies whether online signing should be enabled on the DNS Server. `$true` to enable online signing; otherwise, `$false`.")] Boolean EnableOnlineSigning; + [Write, Description("Specifies whether the DNS server will not send remote queries when there is already a remote query with the same name and query type outstanding.")] Boolean EnableDuplicateQuerySuppression; + [Write, Description("Specifies whether the server will permit the target domain names of NS records to resolve to CNAME records. If `$true`, this pattern of DNS records will be allowed; otherwise, the DNS server will return errors when encountering this pattern of DNS records while resolving queries.")] Boolean AllowCnameAtNs; + [Write, Description("Specifies whether the DNS server will attempt to replicate single updated DNS objects from remote directory servers ahead of normally scheduled replication when operating on a directory server that does not support write operations.")] Boolean EnableRsoForRodc; + [Write, Description("Specifies whether the DNS server allows sharing of DNS records with the DnsUpdateProxy group when processing updates in secure zones that are stored in the directory service.")] Boolean OpenAclOnProxyUpdates; + [Write, Description("Specifies whether the DNS server will accept DNS updates to delegation records of type NS.")] Boolean NoUpdateDelegations; + [Write, Description("Specifies whether the DNS server will forward updates received for secondary zones to the primary DNS server for the zone.")] Boolean EnableUpdateForwarding; + [Write, Description("Specifies whether the DNS server will perform NetBIOS name resolution in order to map IP addresses to machine names while processing queries in zones where WINS-R information has been configured.")] Boolean EnableWinsR; + [Write, Description("Specifies whether the DNS server will delete DNS glue records found outside a delegated subzone when reading records from persistent storage.")] Boolean DeleteOutsideGlue; + [Write, Description("Specifies whether the DNS server will indicate to the remote DNS servers that it supports multiple DNS records in each zone transfer response message by appending the characters MS at the end of zone transfer requests. The value SHOULD be limited to `0x00000000` and `0x0000000`, but it MAY be any value.")] Boolean AppendMsZoneTransferTag; + [Write, Description("Specifies whether the DNS server will allow zone transfers for zones that are stored in the directory server when the directory server does not support write operations.")] Boolean AllowReadOnlyZoneTransfer; + [Write, Description("Specifies whether the DNS server will attempt to suppress large volumes of DNS error responses sent to remote IP addresses that may be attempting to attack the DNS server.")] Boolean EnableSendErrorSuppression; + [Write, Description("Specifies whether the DNS server will ignore CNAME conflicts during DNS update processing.")] Boolean SilentlyIgnoreCnameUpdateConflicts; + [Write, Description("Specifies whether the DNS server will fabricate IQUERY responses. If set to `$true`, the DNS server MUST fabricate IQUERY responses when it receives queries of type IQUERY. Otherwise, the DNS server will return an error when such queries are received.")] Boolean EnableIQueryResponseGeneration; + [Write, Description("Specifies whether the server has been configured by an administrator.")] Boolean AdminConfigured; + [Write, Description("Specifies whether the DNS server will publish local IPv4 addresses in the 169.254.x.x subnet as IPv4 addresses for the local machine's domain name.")] Boolean PublishAutoNet; + [Write, Description("Specifies whether the DNS server will perform an internal restart if an unexpected fatal error is encountered.")] Boolean ReloadException; + [Write, Description("Specifies whether to ignore the server level policies on the DNS server. `$true` to ignore the server level policies on the DNS server; otherwise, `$false`.")] Boolean IgnoreServerLevelPolicies; + [Write, Description("Specifies whether to ignore all policies on the DNS server. `$true` to ignore all policies on the DNS server; otherwise, `$false`.")] Boolean IgnoreAllPolicies; + [Write, Description("Specifies what version information the DNS server will respond with when a DNS query with class set to CHAOS and type set to TXT is received.")] UInt32 EnableVersionQuery; + [Write, Description("Specifies possible settings for automatic delegation creation for new zones on the DNS server. The value SHOULD be limited to the range from `0x00000000` to `0x00000002`, inclusive, but it MAY be any value.")] UInt32 AutoCreateDelegation; + [Write, Description("Specifies the value to add to all IPv4 addresses for remote DNS servers when selecting between IPv4 and IPv6 remote DNS server addresses. The value MUST be limited to the range from `0x00000000` to `0x0000000A`, inclusive.")] UInt32 RemoteIPv4RankBoost; + [Write, Description("Specifies the value to add to all IPv6 addresses for remote DNS servers when selecting between IPv4 and IPv6 remote DNS server addresses. The value MUST be limited to the range from `0x00000000` to `0x0000000A`, inclusive.")] UInt32 RemoteIPv6RankBoost; + [Write, Description("Specifies the maximum number of single object replication operations that may be queued at any given time by the DNS server. The value MUST be limited to the range from `0x00000000` to `0x000F4240`, inclusive. If the value is `0x00000000` the DNS server MUST NOT enforce an upper bound on the number of single object replication operations queued at any given time.")] UInt32 MaximumRodcRsoQueueLength; + [Write, Description("Specifies the maximum number of queued single object replication operations that should be attempted during each five minute interval of DNS server operation. The value MUST be limited to the range from `0x00000001` to `0x000F4240`, inclusive.")] UInt32 MaximumRodcRsoAttemptsPerCycle; + [Write, Description("Specifies the maximum number of resource records that the DNS server will accept in a single DNS update request. The value SHOULD be limited to the range from `0x0000000A` to `0x00000078`, inclusive, but it MAY be any value.")] UInt32 MaxResourceRecordsInNonSecureUpdate; + [Write, Description("Specifies the value which specifies the network mask the DNS server will use to sort IPv4 addresses. A value of `0xFFFFFFFF` indicates that the DNS server MUST use traditional IPv4 network mask for the address. Any other value is a network mask, in host byte order that the DNS server MUST use to retrieve network masks from IP addresses for sorting purposes.")] UInt32 LocalNetPriorityMask; + [Write, Description("Specifies the maximum TCP packet size, in bytes, that the DNS server can accept. The value MUST be limited to the range from `0x00004000` to `0x00010000`, inclusive.")] UInt32 TcpReceivePacketSize; + [Write, Description("Specifies the mask value indicating whether data consistency checking should be performed once, each time the service starts. If the check fails, the server posts an event log warning. If the least significant bit (regardless of other bits) of this value is one, the DNS server will verify for each active and update-allowing primary zone, that the IP address records are present in the zone for the zone's SOA record's master server. If the least significant bit (regardless of other bits) of this value is zero, no data consistency checking will be performed.")] UInt32 SelfTest; + [Write, Description("Specifies the multiple used to determine how long the DNS server should refuse zone transfer requests after a successful zone transfer has been completed. The total time for which a zone will refuse another zone transfer request at the end of a successful zone transfer is computed as this value multiplied by the number of seconds required for the zone transfer that just completed. The server SHOULD refuse zone transfer requests for no more than ten minutes. The value SHOULD be limited to the range from `0x00000000` to `0x00000064`, inclusive, but it MAY be any value.")] UInt32 XfrThrottleMultiplier; + [Write, Description("Specifies the number of UDP sockets per address family that the DNS server will use for sending remote queries.")] UInt32 SocketPoolSize; + [Write, Description("Specifies the minimum time interval, in seconds, starting when the server begins waiting for the query to arrive on the network, after which the server MAY log a debug message indicating that the server is to stop running. If the value is zero or is less than the value of **QuietRecvLogInterval*, then the value of **QuietRecvLogInterval** MUST be used. If the value is greater than or equal to the value of **QuietRecvLogInterval**, then the literal value of **QuietRecvFaultInterval** MUST be used. Used to debug reception of UDP traffic for a recursive query.")] UInt32 QuietRecvFaultInterval; + [Write, Description("Specifies the minimum time interval, in seconds, starting when the server begins waiting for the query to arrive on the network, or when the server logs an eponymous debug message for the query, after which the server MUST log a debug message indicating that the server is still waiting to receive network traffic. If the value is zero, logging associated with the two **QuietRecv** properties MUST be disabled, and the **QuietRecvFaultInterval** property MUST be ignored. If the value is non-zero, logging associated with the two **QuietRecv** properties MUST be enabled, and the **QuietRecvFaultInterval** property MUST NOT be ignored. Used to debug reception of UDP traffic for a recursive query.")] UInt32 QuietRecvLogInterval; + [Write, Description("Specifies the conditions under which the DNS server should immediately commit uncommitted zone serial numbers to persistent storage. The value SHOULD be limited to the range from `0x00000000` to `0x00000004`, inclusive, but it MAY be any value.")] UInt32 SyncDsZoneSerial; + [Write, Description("Specifies the extension mechanism for the DNS (ENDS0) scope setting on the DNS server.")] UInt32 ScopeOptionValue; + [Write, Description("Specifies the virtualization instance option to be sent in ENDS0.")] UInt32 VirtualizationInstanceOptionValue; + [Write, Description("Specifies the path of a custom plug-in. When DllPath specifies the fully qualified path name of a valid DNS server plug-in, the DNS server calls functions in the plug-in to resolve name queries that are outside the scope of all locally hosted zones. If a queried name is out of the scope of the plug-in, the DNS server performs name resolution using forwarding or recursion, as configured. If DllPath is not specified, the DNS server ceases to use a custom plug-in if a custom plug-in was previously configured.")] String ServerLevelPluginDll; + [Write, Description("Specifies the URL of the root trust anchor on the DNS server.")] String RootTrustAnchorsURL; + [Write, Description("Specifies the port ranges that should be excluded.")] String SocketPoolExcludedPortRanges[]; + [Write, Description("Specifies the time span that must elapse before the DNS server will re-query DNS servers of the parent zone when a lame delegation is encountered. The value SHOULD be limited to the range from `0x00000000` to `0x00278D00` 30 days, inclusive, but it MAY be any value.")] String LameDelegationTTL; + [Write, Description("Specifies the maximum period between zone scans to update DnsSec signatures for resource records.")] String MaximumSignatureScanPeriod; + [Write, Description("Specifies the maximum value for the active refresh interval for a trust anchor. Must not be higher than 15 days.")] String MaximumTrustAnchorActiveRefreshInterval; + [Write, Description("Specifies the zone write back interval for file backed zones.")] String ZoneWritebackInterval; + [Read, Description("Returns `$true` if the DNS server has Active Directory integrated DNS enabled; otherwise, `$false`.")] Boolean DsAvailable; + [Read, Description("Returns the major version of the OS of the DNS server.")] UInt32 MajorVersion; + [Read, Description("Returns the minor version of the OS of the DNS server.")] UInt32 MinorVersion; + [Read, Description("Returns the The build version of the OS of the DNS server.")] UInt32 BuildNumber; + [Read, Description("Returns `$true` if write operations are enabled on the directory server; otherwise, `$false`.")] Boolean IsReadOnlyDC; + [Read, Description("Returns all of the IP addresses managed by the DNS server.")] String AllIPAddress[]; + [Read, Description("Returns the application directory partition for the forest the DNS server belongs to. Applicable only for active directory integrated DNS server.")] String ForestDirectoryPartitionBaseName; + [Read, Description("Returns the application directory partition for the domain the DNS server belongs to. Applicable only for active directory integrated DNS server.")] String DomainDirectoryPartitionBaseName; + [Read, Description("Returns the maximum UDP packet size, in bytes, that the DNS server can accept.")] UInt32 MaximumUdpPacketSize; +}; diff --git a/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/DSCResources/DSC_DnsServerSetting/README.md b/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/DSCResources/DSC_DnsServerSetting/README.md new file mode 100644 index 0000000..67b1432 --- /dev/null +++ b/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/DSCResources/DSC_DnsServerSetting/README.md @@ -0,0 +1,24 @@ +# Description + +The DnsServerSetting DSC resource manages the Domain Name System (DNS) server +settings and properties. + +If the parameter **DnsServer** is set to `'localhost'` then the resource +can normally use the default credentials (SYSTEM) to configure the DNS server +settings. If using any other value for the parameter **DnsServer** make sure +that the credential the resource is run as have the correct permissions +at the target node and the necessary network traffic is permitted (_WsMan_ +protocol). It is possible to run the resource with specific credentials using the +built-in parameter **PsDscRunAsCredential**. + +Please see [DnsServerSetting class](https://docs.microsoft.com/en-us/previous-versions/windows/desktop/dnsserverpsprov/dnsserversetting) +for more information around the properties this resource supports. + +## Requirements + +- Target machine must be running Windows Server 2012 or later. + - Properties `RootTrustAnchorsURL` and `ZoneWritebackInterval` is not + supported by _Windows Server 2012_. + - Properties `IgnoreServerLevelPolicies`, `IgnoreAllPolicies`, + `ScopeOptionValue`, and `VirtualizationInstanceOptionValue` are not + supported by _Windows Server 2012_ and _Windows Server 2012 R2_. diff --git a/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/DSCResources/DSC_DnsServerSetting/en-US/DSC_DnsServerSetting.strings.psd1 b/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/DSCResources/DSC_DnsServerSetting/en-US/DSC_DnsServerSetting.strings.psd1 new file mode 100644 index 0000000..aa0c0bf --- /dev/null +++ b/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/DSCResources/DSC_DnsServerSetting/en-US/DSC_DnsServerSetting.strings.psd1 @@ -0,0 +1,9 @@ +# culture="en-US" +ConvertFrom-StringData @' + GettingDnsServerSettings = Getting DNS Server Settings. (DSS0001) + SetDnsServerSetting = Setting DNS Server setting '{0}' to value '{1}'. (DSS0002) + EvaluatingDnsServerSettings = Evaluating the DNS Server settings. (DSS0003) + PropertyInDesiredState = The property '{0}' is already in desired state. (DSS0004) + SettingsInDesiredState = The DNS Server settings are in desired state. (DSS0005) + UnableToParseTimeSpan = Could not parse the value '{0}' of the property '{1}'. (DSS0006) +'@ diff --git a/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/DSCResources/DSC_DnsServerSetting/en-US/about_DnsServerSetting.help.txt b/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/DSCResources/DSC_DnsServerSetting/en-US/about_DnsServerSetting.help.txt new file mode 100644 index 0000000..eae80a8 --- /dev/null +++ b/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/DSCResources/DSC_DnsServerSetting/en-US/about_DnsServerSetting.help.txt @@ -0,0 +1,493 @@ +.NAME + DnsServerSetting + +.DESCRIPTION + The DnsServerSetting DSC resource manages the Domain Name System (DNS) server + settings and properties. + + If the parameter DnsServer is set to 'localhost' then the resource + can normally use the default credentials (SYSTEM) to configure the DNS server + settings. If using any other value for the parameter DnsServer make sure + that the credential the resource is run as have the correct permissions + at the target node and the necessary network traffic is permitted (WsMan + protocol). It is possible to run the resource with specific credentials using the + built-in parameter PsDscRunAsCredential. + + Please see https://docs.microsoft.com/en-us/previous-versions/windows/desktop/dnsserverpsprov/dnsserversetting + for more information around the properties this resource supports. + + ## Requirements + + - Target machine must be running Windows Server 2012 or later. + - Properties RootTrustAnchorsURL and ZoneWritebackInterval is not + supported by Windows Server 2012. + - Properties IgnoreServerLevelPolicies, IgnoreAllPolicies, + ScopeOptionValue, and VirtualizationInstanceOptionValue are not + supported by Windows Server 2012 and Windows Server 2012 R2. + +.PARAMETER DnsServer + Key - String + Specifies the DNS server to connect to, or use 'localhost' for the current node. + +.PARAMETER AddressAnswerLimit + Write - UInt32 + Specifies the maximum number of A (host IP address) resource records that the DNS server can insert in the answer section of a response to an A record query (a query for an IP address). The value of this entry also influences the setting of the truncation bit. If the value of this entry can be between 5 and 28, or 0. The truncation bit is not set on the response, even when the packet space is exceeded. + +.PARAMETER AllowUpdate + Write - Boolean + Specifies whether the DNS Server accepts dynamic update requests. $true to allow any DNS update operation; otherwise, $false. + +.PARAMETER AutoCacheUpdate + Write - Boolean + Specifies whether the DNS Server attempts to update its cache entries using data from root servers. $true to cache delegation information; otherwise, $false. + +.PARAMETER AutoConfigFileZones + Write - UInt32 + Specifies the type of zones for which SOA and NS records will be automatically configured with the DNS server's local host name as the primary DNS server for the zone when the zone is loaded from file. + +.PARAMETER BindSecondaries + Write - Boolean + Specifies whether the server will permit send DNS zone transfer response messages with more than one record in each response if the zone transfer request did not have the characters MS appended to it. If set to $true, the DNS server will include only one record in each response if the zone transfer request did not have the characters MS appended to it. + +.PARAMETER BootMethod + Write - UInt32 + Specifies the boot method used by the DNS server. + +.PARAMETER DisableAutoReverseZone + Write - Boolean + Specifies whether the DNS Server automatically creates standard reverse look up zones. $true to disables automatic reverse zones; otherwise, $false. + +.PARAMETER EnableDirectoryPartitions + Write - Boolean + Specifies whether the DNS server will support application directory partitions. + +.PARAMETER EnableDnsSec + Write - Boolean + Specifies whether the DNS Server includes DNSSEC-specific RRs, KEY, SIG, and NXT in a response. $true to enable DNSSEC validation on the DNS server; otherwise, $false. + +.PARAMETER ForwardDelegations + Write - Boolean + Specifies how the DNS server will handle forwarding and delegations. If set to $true, the DNS server MUST use forwarders instead of a cached delegation when both are available. Otherwise, the DNS server MUST use a cached delegation instead of forwarders when both are available. + +.PARAMETER ListeningIPAddress + Write - StringArray + Specifies the listening IP addresses of the DNS server. The list of IP addresses on which the DNS Server can receive queries. + +.PARAMETER LocalNetPriority + Write - Boolean + Specifies whether the DNS Server gives priority to the local net address when returning A records. $true to return A records in order of their similarity to the IP address of the querying client.; otherwise, $false. + +.PARAMETER LooseWildcarding + Write - Boolean + Specifies he type of algorithm that the DNS server will use to locate a wildcard node when using a DNS wildcard record RFC1034 to answer a query. If true, the DNS server will use the first node it encounters with a record of the same type as the query type. Otherwise, the DNS server will use the first node it encounters that has records of any type. + +.PARAMETER NameCheckFlag + Write - UInt32 + Specifies the level of domain name checking and validation on the DNS server, the set of eligible characters to be used in DNS names. + +.PARAMETER RoundRobin + Write - Boolean + Specifies whether the DNS Server round robins multiple A records. $true to enable Round-robin DNS on the DNS server; otherwise, $false. + +.PARAMETER RpcProtocol + Write - UInt32 + Specifies the DNSRPCPROTOCOLS section 2.2.1.1.2 value corresponding to the RPC protocols to which the DNS server will respond. If this value is set to 0x00000000, the DNS server MUST NOT respond to RPC requests for any protocol. + +.PARAMETER SendPort + Write - UInt32 + Specifies the port number to use as the source port when sending UDP queries to a remote DNS server. If set to zero, the DNS server allow the stack to select a random port. + +.PARAMETER StrictFileParsing + Write - Boolean + Specifies whether the DNS server will treat errors encountered while reading zones from a file as fatal. + +.PARAMETER UpdateOptions + Write - UInt32 + Specifies the DNS update options used by the DNS server. + +.PARAMETER WriteAuthorityNS + Write - Boolean + Specifies whether the DNS server will include NS records for the root of a zone in DNS responses that are answered using authoritative zone data. + +.PARAMETER XfrConnectTimeout + Write - UInt32 + Specifies the time span, in seconds, in which a primary DNS server waits for a transfer response from its secondary server. The default value is 30. After the time-out value expires, the connection is terminated. + +.PARAMETER EnableIPv6 + Write - Boolean + Specifies whether IPv6 should be enabled on the DNS Server. $true to enable IPv6 on the DNS server; otherwise, $false. + +.PARAMETER EnableOnlineSigning + Write - Boolean + Specifies whether online signing should be enabled on the DNS Server. $true to enable online signing; otherwise, $false. + +.PARAMETER EnableDuplicateQuerySuppression + Write - Boolean + Specifies whether the DNS server will not send remote queries when there is already a remote query with the same name and query type outstanding. + +.PARAMETER AllowCnameAtNs + Write - Boolean + Specifies whether the server will permit the target domain names of NS records to resolve to CNAME records. If $true, this pattern of DNS records will be allowed; otherwise, the DNS server will return errors when encountering this pattern of DNS records while resolving queries. + +.PARAMETER EnableRsoForRodc + Write - Boolean + Specifies whether the DNS server will attempt to replicate single updated DNS objects from remote directory servers ahead of normally scheduled replication when operating on a directory server that does not support write operations. + +.PARAMETER OpenAclOnProxyUpdates + Write - Boolean + Specifies whether the DNS server allows sharing of DNS records with the DnsUpdateProxy group when processing updates in secure zones that are stored in the directory service. + +.PARAMETER NoUpdateDelegations + Write - Boolean + Specifies whether the DNS server will accept DNS updates to delegation records of type NS. + +.PARAMETER EnableUpdateForwarding + Write - Boolean + Specifies whether the DNS server will forward updates received for secondary zones to the primary DNS server for the zone. + +.PARAMETER EnableWinsR + Write - Boolean + Specifies whether the DNS server will perform NetBIOS name resolution in order to map IP addresses to machine names while processing queries in zones where WINS-R information has been configured. + +.PARAMETER DeleteOutsideGlue + Write - Boolean + Specifies whether the DNS server will delete DNS glue records found outside a delegated subzone when reading records from persistent storage. + +.PARAMETER AppendMsZoneTransferTag + Write - Boolean + Specifies whether the DNS server will indicate to the remote DNS servers that it supports multiple DNS records in each zone transfer response message by appending the characters MS at the end of zone transfer requests. The value SHOULD be limited to 0x00000000 and 0x0000000, but it MAY be any value. + +.PARAMETER AllowReadOnlyZoneTransfer + Write - Boolean + Specifies whether the DNS server will allow zone transfers for zones that are stored in the directory server when the directory server does not support write operations. + +.PARAMETER EnableSendErrorSuppression + Write - Boolean + Specifies whether the DNS server will attempt to suppress large volumes of DNS error responses sent to remote IP addresses that may be attempting to attack the DNS server. + +.PARAMETER SilentlyIgnoreCnameUpdateConflicts + Write - Boolean + Specifies whether the DNS server will ignore CNAME conflicts during DNS update processing. + +.PARAMETER EnableIQueryResponseGeneration + Write - Boolean + Specifies whether the DNS server will fabricate IQUERY responses. If set to $true, the DNS server MUST fabricate IQUERY responses when it receives queries of type IQUERY. Otherwise, the DNS server will return an error when such queries are received. + +.PARAMETER AdminConfigured + Write - Boolean + Specifies whether the server has been configured by an administrator. + +.PARAMETER PublishAutoNet + Write - Boolean + Specifies whether the DNS server will publish local IPv4 addresses in the 169.254.x.x subnet as IPv4 addresses for the local machine's domain name. + +.PARAMETER ReloadException + Write - Boolean + Specifies whether the DNS server will perform an internal restart if an unexpected fatal error is encountered. + +.PARAMETER IgnoreServerLevelPolicies + Write - Boolean + Specifies whether to ignore the server level policies on the DNS server. $true to ignore the server level policies on the DNS server; otherwise, $false. + +.PARAMETER IgnoreAllPolicies + Write - Boolean + Specifies whether to ignore all policies on the DNS server. $true to ignore all policies on the DNS server; otherwise, $false. + +.PARAMETER EnableVersionQuery + Write - UInt32 + Specifies what version information the DNS server will respond with when a DNS query with class set to CHAOS and type set to TXT is received. + +.PARAMETER AutoCreateDelegation + Write - UInt32 + Specifies possible settings for automatic delegation creation for new zones on the DNS server. The value SHOULD be limited to the range from 0x00000000 to 0x00000002, inclusive, but it MAY be any value. + +.PARAMETER RemoteIPv4RankBoost + Write - UInt32 + Specifies the value to add to all IPv4 addresses for remote DNS servers when selecting between IPv4 and IPv6 remote DNS server addresses. The value MUST be limited to the range from 0x00000000 to 0x0000000A, inclusive. + +.PARAMETER RemoteIPv6RankBoost + Write - UInt32 + Specifies the value to add to all IPv6 addresses for remote DNS servers when selecting between IPv4 and IPv6 remote DNS server addresses. The value MUST be limited to the range from 0x00000000 to 0x0000000A, inclusive. + +.PARAMETER MaximumRodcRsoQueueLength + Write - UInt32 + Specifies the maximum number of single object replication operations that may be queued at any given time by the DNS server. The value MUST be limited to the range from 0x00000000 to 0x000F4240, inclusive. If the value is 0x00000000 the DNS server MUST NOT enforce an upper bound on the number of single object replication operations queued at any given time. + +.PARAMETER MaximumRodcRsoAttemptsPerCycle + Write - UInt32 + Specifies the maximum number of queued single object replication operations that should be attempted during each five minute interval of DNS server operation. The value MUST be limited to the range from 0x00000001 to 0x000F4240, inclusive. + +.PARAMETER MaxResourceRecordsInNonSecureUpdate + Write - UInt32 + Specifies the maximum number of resource records that the DNS server will accept in a single DNS update request. The value SHOULD be limited to the range from 0x0000000A to 0x00000078, inclusive, but it MAY be any value. + +.PARAMETER LocalNetPriorityMask + Write - UInt32 + Specifies the value which specifies the network mask the DNS server will use to sort IPv4 addresses. A value of 0xFFFFFFFF indicates that the DNS server MUST use traditional IPv4 network mask for the address. Any other value is a network mask, in host byte order that the DNS server MUST use to retrieve network masks from IP addresses for sorting purposes. + +.PARAMETER TcpReceivePacketSize + Write - UInt32 + Specifies the maximum TCP packet size, in bytes, that the DNS server can accept. The value MUST be limited to the range from 0x00004000 to 0x00010000, inclusive. + +.PARAMETER SelfTest + Write - UInt32 + Specifies the mask value indicating whether data consistency checking should be performed once, each time the service starts. If the check fails, the server posts an event log warning. If the least significant bit (regardless of other bits) of this value is one, the DNS server will verify for each active and update-allowing primary zone, that the IP address records are present in the zone for the zone's SOA record's master server. If the least significant bit (regardless of other bits) of this value is zero, no data consistency checking will be performed. + +.PARAMETER XfrThrottleMultiplier + Write - UInt32 + Specifies the multiple used to determine how long the DNS server should refuse zone transfer requests after a successful zone transfer has been completed. The total time for which a zone will refuse another zone transfer request at the end of a successful zone transfer is computed as this value multiplied by the number of seconds required for the zone transfer that just completed. The server SHOULD refuse zone transfer requests for no more than ten minutes. The value SHOULD be limited to the range from 0x00000000 to 0x00000064, inclusive, but it MAY be any value. + +.PARAMETER SocketPoolSize + Write - UInt32 + Specifies the number of UDP sockets per address family that the DNS server will use for sending remote queries. + +.PARAMETER QuietRecvFaultInterval + Write - UInt32 + Specifies the minimum time interval, in seconds, starting when the server begins waiting for the query to arrive on the network, after which the server MAY log a debug message indicating that the server is to stop running. If the value is zero or is less than the value of QuietRecvLogInterval, then the value of QuietRecvLogInterval MUST be used. If the value is greater than or equal to the value of QuietRecvLogInterval, then the literal value of QuietRecvFaultInterval* MUST be used. Used to debug reception of UDP traffic for a recursive query. + +.PARAMETER QuietRecvLogInterval + Write - UInt32 + Specifies the minimum time interval, in seconds, starting when the server begins waiting for the query to arrive on the network, or when the server logs an eponymous debug message for the query, after which the server MUST log a debug message indicating that the server is still waiting to receive network traffic. If the value is zero, logging associated with the two QuietRecv properties MUST be disabled, and the QuietRecvFaultInterval property MUST be ignored. If the value is non-zero, logging associated with the two QuietRecv properties MUST be enabled, and the QuietRecvFaultInterval property MUST NOT be ignored. Used to debug reception of UDP traffic for a recursive query. + +.PARAMETER SyncDsZoneSerial + Write - UInt32 + Specifies the conditions under which the DNS server should immediately commit uncommitted zone serial numbers to persistent storage. The value SHOULD be limited to the range from 0x00000000 to 0x00000004, inclusive, but it MAY be any value. + +.PARAMETER ScopeOptionValue + Write - UInt32 + Specifies the extension mechanism for the DNS (ENDS0) scope setting on the DNS server. + +.PARAMETER VirtualizationInstanceOptionValue + Write - UInt32 + Specifies the virtualization instance option to be sent in ENDS0. + +.PARAMETER ServerLevelPluginDll + Write - String + Specifies the path of a custom plug-in. When DllPath specifies the fully qualified path name of a valid DNS server plug-in, the DNS server calls functions in the plug-in to resolve name queries that are outside the scope of all locally hosted zones. If a queried name is out of the scope of the plug-in, the DNS server performs name resolution using forwarding or recursion, as configured. If DllPath is not specified, the DNS server ceases to use a custom plug-in if a custom plug-in was previously configured. + +.PARAMETER RootTrustAnchorsURL + Write - String + Specifies the URL of the root trust anchor on the DNS server. + +.PARAMETER SocketPoolExcludedPortRanges + Write - StringArray + Specifies the port ranges that should be excluded. + +.PARAMETER LameDelegationTTL + Write - String + Specifies the time span that must elapse before the DNS server will re-query DNS servers of the parent zone when a lame delegation is encountered. The value SHOULD be limited to the range from 0x00000000 to 0x00278D00 30 days, inclusive, but it MAY be any value. + +.PARAMETER MaximumSignatureScanPeriod + Write - String + Specifies the maximum period between zone scans to update DnsSec signatures for resource records. + +.PARAMETER MaximumTrustAnchorActiveRefreshInterval + Write - String + Specifies the maximum value for the active refresh interval for a trust anchor. Must not be higher than 15 days. + +.PARAMETER ZoneWritebackInterval + Write - String + Specifies the zone write back interval for file backed zones. + +.PARAMETER DsAvailable + Read - Boolean + Returns $true if the DNS server has Active Directory integrated DNS enabled; otherwise, $false. + +.PARAMETER MajorVersion + Read - UInt32 + Returns the major version of the OS of the DNS server. + +.PARAMETER MinorVersion + Read - UInt32 + Returns the minor version of the OS of the DNS server. + +.PARAMETER BuildNumber + Read - UInt32 + Returns the The build version of the OS of the DNS server. + +.PARAMETER IsReadOnlyDC + Read - Boolean + Returns $true if write operations are enabled on the directory server; otherwise, $false. + +.PARAMETER AllIPAddress + Read - StringArray + Returns all of the IP addresses managed by the DNS server. + +.PARAMETER ForestDirectoryPartitionBaseName + Read - String + Returns the application directory partition for the forest the DNS server belongs to. Applicable only for active directory integrated DNS server. + +.PARAMETER DomainDirectoryPartitionBaseName + Read - String + Returns the application directory partition for the domain the DNS server belongs to. Applicable only for active directory integrated DNS server. + +.PARAMETER MaximumUdpPacketSize + Read - UInt32 + Returns the maximum UDP packet size, in bytes, that the DNS server can accept. + +.EXAMPLE 1 + +This configuration will manage the DNS server settings on the current +node. + +Configuration DnsServerSetting_CurrentNode_Config +{ + Import-DscResource -ModuleName 'DnsServerDsc' + + Node localhost + { + DnsServerSetting 'DnsServerProperties' + { + DnsServer = 'localhost' + LocalNetPriority = $true + RoundRobin = $true + RpcProtocol = 0 + NameCheckFlag = 2 + AutoConfigFileZones = 1 + AddressAnswerLimit = 0 + UpdateOptions = 783 + DisableAutoReverseZone = $false + StrictFileParsing = $false + EnableDirectoryPartitions = $false + XfrConnectTimeout = 30 + BootMethod = 3 + AllowUpdate = $true + LooseWildcarding = $false + BindSecondaries = $false + AutoCacheUpdate = $false + EnableDnsSec = $true + SendPort = 0 + WriteAuthorityNS = $false + ListeningIPAddress = @('192.168.1.10', '192.168.2.10') + ForwardDelegations = $false + EnableIPv6 = $true + EnableOnlineSigning = $true + EnableDuplicateQuerySuppression = $true + AllowCnameAtNs = $true + EnableRsoForRodc = $true + OpenAclOnProxyUpdates = $true + NoUpdateDelegations = $false + EnableUpdateForwarding = $false + EnableWinsR = $true + DeleteOutsideGlue = $false + AppendMsZoneTransferTag = $false + AllowReadOnlyZoneTransfer = $false + EnableSendErrorSuppression = $true + SilentlyIgnoreCnameUpdateConflicts = $false + EnableIQueryResponseGeneration = $false + AdminConfigured = $true + PublishAutoNet = $false + ReloadException = $false + IgnoreServerLevelPolicies = $false + IgnoreAllPolicies = $false + EnableVersionQuery = 0 + AutoCreateDelegation = 2 + RemoteIPv4RankBoost = 5 + RemoteIPv6RankBoost = 0 + MaximumRodcRsoQueueLength = 300 + MaximumRodcRsoAttemptsPerCycle = 100 + MaxResourceRecordsInNonSecureUpdate = 30 + LocalNetPriorityMask = 255 + TcpReceivePacketSize = 65536 + SelfTest = 4294967295 + XfrThrottleMultiplier = 10 + SocketPoolSize = 2500 + QuietRecvFaultInterval = 0 + QuietRecvLogInterval = 0 + SyncDsZoneSerial = 2 + ScopeOptionValue = 0 + VirtualizationInstanceOptionValue = 0 + ServerLevelPluginDll = 'C:\dns\plugin.dll' + RootTrustAnchorsURL = 'https://data.iana.org/root-anchors/oroot-anchors.xml' + SocketPoolExcludedPortRanges = @() + LameDelegationTTL = '00:00:00' + MaximumSignatureScanPeriod = '2.00:00:00' + MaximumTrustAnchorActiveRefreshInterval = '15.00:00:00' + ZoneWritebackInterval = '00:01:00' + } + } +} + +.EXAMPLE 2 + +This configuration will manage the DNS server settings on the current +node. + +Configuration DnsServerSetting_RemoteNode_Config +{ + Import-DscResource -ModuleName 'DnsServerDsc' + + Node localhost + { + DnsServerSetting 'DnsServerProperties' + { + DnsServer = 'dns1.company.local' + LocalNetPriority = $true + RoundRobin = $true + RpcProtocol = 0 + NameCheckFlag = 2 + AutoConfigFileZones = 1 + AddressAnswerLimit = 0 + UpdateOptions = 783 + DisableAutoReverseZone = $false + StrictFileParsing = $false + EnableDirectoryPartitions = $false + XfrConnectTimeout = 30 + BootMethod = 3 + AllowUpdate = $true + LooseWildcarding = $false + BindSecondaries = $false + AutoCacheUpdate = $false + EnableDnsSec = $true + SendPort = 0 + WriteAuthorityNS = $false + ListeningIPAddress = @('192.168.1.10', '192.168.2.10') + ForwardDelegations = $false + EnableIPv6 = $true + EnableOnlineSigning = $true + EnableDuplicateQuerySuppression = $true + AllowCnameAtNs = $true + EnableRsoForRodc = $true + OpenAclOnProxyUpdates = $true + NoUpdateDelegations = $false + EnableUpdateForwarding = $false + EnableWinsR = $true + DeleteOutsideGlue = $false + AppendMsZoneTransferTag = $false + AllowReadOnlyZoneTransfer = $false + EnableSendErrorSuppression = $true + SilentlyIgnoreCnameUpdateConflicts = $false + EnableIQueryResponseGeneration = $false + AdminConfigured = $true + PublishAutoNet = $false + ReloadException = $false + IgnoreServerLevelPolicies = $false + IgnoreAllPolicies = $false + EnableVersionQuery = 0 + AutoCreateDelegation = 2 + RemoteIPv4RankBoost = 5 + RemoteIPv6RankBoost = 0 + MaximumRodcRsoQueueLength = 300 + MaximumRodcRsoAttemptsPerCycle = 100 + MaxResourceRecordsInNonSecureUpdate = 30 + LocalNetPriorityMask = 255 + TcpReceivePacketSize = 65536 + SelfTest = 4294967295 + XfrThrottleMultiplier = 10 + SocketPoolSize = 2500 + QuietRecvFaultInterval = 0 + QuietRecvLogInterval = 0 + SyncDsZoneSerial = 2 + ScopeOptionValue = 0 + VirtualizationInstanceOptionValue = 0 + ServerLevelPluginDll = 'C:\dns\plugin.dll' + RootTrustAnchorsURL = 'https://data.iana.org/root-anchors/oroot-anchors.xml' + SocketPoolExcludedPortRanges = @() + LameDelegationTTL = '00:00:00' + MaximumSignatureScanPeriod = '2.00:00:00' + MaximumTrustAnchorActiveRefreshInterval = '15.00:00:00' + ZoneWritebackInterval = '00:01:00' + } + } +} + diff --git a/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/DSCResources/DSC_DnsServerSettingLegacy/DSC_DnsServerSettingLegacy.psm1 b/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/DSCResources/DSC_DnsServerSettingLegacy/DSC_DnsServerSettingLegacy.psm1 new file mode 100644 index 0000000..c222c0e --- /dev/null +++ b/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/DSCResources/DSC_DnsServerSettingLegacy/DSC_DnsServerSettingLegacy.psm1 @@ -0,0 +1,260 @@ +$script:dscResourceCommonPath = Join-Path -Path $PSScriptRoot -ChildPath '..\..\Modules\DscResource.Common' +$script:dnsServerDscCommonPath = Join-Path -Path $PSScriptRoot -ChildPath '..\..\Modules\DnsServerDsc.Common' + +Import-Module -Name $script:dscResourceCommonPath +Import-Module -Name $script:dnsServerDscCommonPath + +$script:localizedData = Get-LocalizedData -DefaultUICulture 'en-US' + +<# + This resource contains a references to an offensive word that we do not use + in the DSC community. But due to the underlying class MicrosoftDNS_Server is + using the offensive word this resource need to use it too. This will change + as soon as the underlying class changes, or we can remove the property + altogether, see https://docs.microsoft.com/en-us/windows/win32/dns/microsoftdns-server. +#> + +<# + .SYNOPSIS + Returns the current state of the DNS server settings. + + .PARAMETER DnsServer + Specifies the DNS server to connect to, or use 'localhost' for the current + node. +#> +function Get-TargetResource +{ + [CmdletBinding()] + [OutputType([System.Collections.Hashtable])] + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $DnsServer + ) + + Assert-Module -ModuleName 'DnsServer' + + Write-Verbose -Message $script:localizedData.GettingDnsServerSettings + + $dnsServerInstance = Get-CimClassMicrosoftDnsServer -DnsServer $DnsServer + + $returnValue = @{} + + $classProperties = @( + 'DisjointNets' + 'LogLevel' + 'IsSlave' + ) + + foreach ($property in $classProperties) + { + $propertyName = $property + + if ($propertyName -eq 'IsSlave') + { + $propertyName = 'NoForwarderRecursion' + } + + $returnValue.Add($propertyName, $dnsServerInstance.$property) + } + + $returnValue.DnsServer = $DnsServer + + return $returnValue +} + +<# + .SYNOPSIS + Set the desired state of the DNS server legacy settings. + + .PARAMETER DnsServer + Specifies the DNS server to connect to, or use 'localhost' for the current + node. + + .PARAMETER DisjointNets + Indicates whether the default port binding for a socket used to send queries + to remote DNS Servers can be overridden. + + .PARAMETER NoForwarderRecursion + TRUE if the DNS server does not use recursion when name-resolution through + forwarders fails. + + .PARAMETER LogLevel + Indicates which policies are activated in the Event Viewer system log. +#> +function Set-TargetResource +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $DnsServer, + + [Parameter()] + [System.Boolean] + $DisjointNets, + + [Parameter()] + [System.Boolean] + $NoForwarderRecursion, + + [Parameter()] + [System.UInt32] + $LogLevel + ) + + Assert-Module -ModuleName 'DnsServer' + + $getTargetResourceResult = Get-TargetResource -DnsServer $DnsServer + + $PSBoundParameters.Remove('DnsServer') + + $dnsProperties = Remove-CommonParameter -Hashtable $PSBoundParameters + + $dnsServerInstance = Get-CimClassMicrosoftDnsServer -DnsServer $DnsServer + + $propertiesInDesiredState = @() + + foreach ($property in $dnsProperties.keys) + { + if ($dnsProperties.$property -ne $getTargetResourceResult.$property) + { + # Property not in desired state. + + Write-Verbose -Message ($script:localizedData.SetDnsServerSetting -f $property, $dnsProperties[$property]) + } + else + { + # Property in desired state. + + Write-Verbose -Message ($script:localizedData.PropertyInDesiredState -f $property) + + $propertiesInDesiredState += $property + } + } + + # Remove passed parameters that are in desired state. + $propertiesInDesiredState | ForEach-Object -Process { + $dnsProperties.Remove($_) + } + + # Handle renaming properties to what the class expects. + if ($dnsProperties.ContainsKey('NoForwarderRecursion')) + { + $dnsProperties.IsSlave = $dnsProperties.NoForwarderRecursion + + $dnsProperties.Remove('NoForwarderRecursion') + } + + if ($dnsProperties.Keys.Count -eq 0) + { + Write-Verbose -Message $script:localizedData.LegacySettingsInDesiredState + } + else + { + $setCimInstanceParameters = @{ + InputObject = $dnsServerInstance + Property = $dnsProperties + ErrorAction = 'Stop' + } + + if ($DnsServer -ne 'localhost') + { + $setCimInstanceParameters['ComputerName'] = $DnsServer + } + + Set-CimInstance @setCimInstanceParameters + } +} + +<# + .SYNOPSIS + Tests the desired state of the DNS server settings. + + .PARAMETER DnsServer + Specifies the DNS server to connect to, or use 'localhost' for the current + node. + + .PARAMETER DnsServer + Specifies the DNS server to connect to, or use 'localhost' for the current + node. + + .PARAMETER DisjointNets + Indicates whether the default port binding for a socket used to send queries + to remote DNS Servers can be overridden. + + .PARAMETER NoForwarderRecursion + TRUE if the DNS server does not use recursion when name-resolution through + forwarders fails. + + .PARAMETER LogLevel + Indicates which policies are activated in the Event Viewer system log. +#> +function Test-TargetResource +{ + [CmdletBinding()] + [OutputType([bool])] + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $DnsServer, + + [Parameter()] + [System.Boolean] + $DisjointNets, + + [Parameter()] + [System.Boolean] + $NoForwarderRecursion, + + [Parameter()] + [System.UInt32] + $LogLevel + ) + + Write-Verbose -Message $script:localizedData.EvaluatingDnsServerSettings + + $currentState = Get-TargetResource -DnsServer $DnsServer + + $null = $PSBoundParameters.Remove('DnsServer') + + $result = $true + + # Returns an item for each property that is not in desired state. + if (Compare-DscParameterState -CurrentValues $currentState -DesiredValues $PSBoundParameters -Verbose:$VerbosePreference) + { + $result = $false + } + + return $result +} + +function Get-CimClassMicrosoftDnsServer +{ + [CmdletBinding()] + [OutputType([Microsoft.Management.Infrastructure.CimInstance])] + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $DnsServer + ) + + $getCimInstanceParameters = @{ + NameSpace = 'root\MicrosoftDNS' + ClassName = 'MicrosoftDNS_Server' + ErrorAction = 'Stop' + } + + if ($DnsServer -ne 'localhost') + { + $getCimInstanceParameters['ComputerName'] = $DnsServer + } + + $dnsServerInstance = Get-CimInstance @getCimInstanceParameters + + return $dnsServerInstance +} diff --git a/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/DSCResources/DSC_DnsServerSettingLegacy/DSC_DnsServerSettingLegacy.schema.mof b/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/DSCResources/DSC_DnsServerSettingLegacy/DSC_DnsServerSettingLegacy.schema.mof new file mode 100644 index 0000000..e9496bd --- /dev/null +++ b/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/DSCResources/DSC_DnsServerSettingLegacy/DSC_DnsServerSettingLegacy.schema.mof @@ -0,0 +1,8 @@ +[ClassVersion("1.0.0.0"), FriendlyName("DnsServerSettingLegacy")] +class DSC_DnsServerSettingLegacy : OMI_BaseResource +{ + [Key, Description("Specifies the DNS server to connect to, or use 'localhost' for the current node.")] String DnsServer; + [Write, Description("Indicates whether the default port binding for a socket used to send queries to remote DNS Servers can be overridden.")] Boolean DisjointNets; + [Write, Description("TRUE if the DNS server does not use recursion when name-resolution through forwarders fails.")] Boolean NoForwarderRecursion; + [Write, Description("Indicates which policies are activated in the Event Viewer system log.")] UInt32 LogLevel; +}; diff --git a/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/DSCResources/DSC_DnsServerSettingLegacy/README.md b/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/DSCResources/DSC_DnsServerSettingLegacy/README.md new file mode 100644 index 0000000..ac21bb7 --- /dev/null +++ b/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/DSCResources/DSC_DnsServerSettingLegacy/README.md @@ -0,0 +1,12 @@ +# Description + +The DnsServerSettingLegacy DSC resource manages the Domain Name System (DNS) server +legacy settings. + +If the parameter **DnsServer** is set to `'localhost'` then the resource +can normally use the default credentials (SYSTEM) to configure the DNS server +settings. If using any other value for the parameter **DnsServer** make sure +that the credential the resource is run as have the correct permissions +at the target node and the necessary network traffic is permitted (_WsMan_ +protocol). It is possible to run the resource with specific credentials using the +built-in parameter **PsDscRunAsCredential**. diff --git a/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/DSCResources/DSC_DnsServerSettingLegacy/en-US/DSC_DnsServerSettingLegacy.strings.psd1 b/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/DSCResources/DSC_DnsServerSettingLegacy/en-US/DSC_DnsServerSettingLegacy.strings.psd1 new file mode 100644 index 0000000..2977ce4 --- /dev/null +++ b/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/DSCResources/DSC_DnsServerSettingLegacy/en-US/DSC_DnsServerSettingLegacy.strings.psd1 @@ -0,0 +1,8 @@ +# culture="en-US" +ConvertFrom-StringData @' + GettingDnsServerSettings = Getting DNS Server legacy settings. (DSSL0001) + SetDnsServerSetting = Setting DNS Server legacy setting '{0}' to value '{1}'. (DSSL0002) + EvaluatingDnsServerSettings = Evaluating the DNS Server legacy settings. (DSSL0003) + PropertyInDesiredState = The property '{0}' is already in desired state. (DSSL0004) + LegacySettingsInDesiredState = The DNS Server legacy settings are in desired state. (DSSL0005) +'@ diff --git a/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/DSCResources/DSC_DnsServerSettingLegacy/en-US/about_DnsServerSettingLegacy.help.txt b/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/DSCResources/DSC_DnsServerSettingLegacy/en-US/about_DnsServerSettingLegacy.help.txt new file mode 100644 index 0000000..20896d3 --- /dev/null +++ b/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/DSCResources/DSC_DnsServerSettingLegacy/en-US/about_DnsServerSettingLegacy.help.txt @@ -0,0 +1,73 @@ +.NAME + DnsServerSettingLegacy + +.DESCRIPTION + The DnsServerSettingLegacy DSC resource manages the Domain Name System (DNS) server + legacy settings. + + If the parameter DnsServer is set to 'localhost' then the resource + can normally use the default credentials (SYSTEM) to configure the DNS server + settings. If using any other value for the parameter DnsServer make sure + that the credential the resource is run as have the correct permissions + at the target node and the necessary network traffic is permitted (WsMan + protocol). It is possible to run the resource with specific credentials using the + built-in parameter PsDscRunAsCredential. + +.PARAMETER DnsServer + Key - String + Specifies the DNS server to connect to, or use 'localhost' for the current node. + +.PARAMETER DisjointNets + Write - Boolean + Indicates whether the default port binding for a socket used to send queries to remote DNS Servers can be overridden. + +.PARAMETER NoForwarderRecursion + Write - Boolean + TRUE if the DNS server does not use recursion when name-resolution through forwarders fails. + +.PARAMETER LogLevel + Write - UInt32 + Indicates which policies are activated in the Event Viewer system log. + +.EXAMPLE 1 + +This configuration will manage the DNS server legacy settings on the current +node. + +Configuration DnsServerSettingLegacy_CurrentNode_Config +{ + Import-DscResource -ModuleName 'DnsServerDsc' + + Node localhost + { + DnsServerSettingLegacy 'DnsServerLegacyProperties' + { + DnsServer = 'localhost' + DisjointNets = $false + NoForwarderRecursion = $true + LogLevel = 50393905 + } + } +} + +.EXAMPLE 2 + +This configuration will manage the DNS server legacy settings on the current +node. + +Configuration DnsServerSettingLegacy_RemoteNode_Config +{ + Import-DscResource -ModuleName 'DnsServerDsc' + + Node localhost + { + DnsServerSettingLegacy 'DnsServerLegacyProperties' + { + DnsServer = 'dns1.company.local' + DisjointNets = $false + NoForwarderRecursion = $true + LogLevel = 50393905 + } + } +} + diff --git a/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/DSCResources/DSC_DnsServerZoneAging/DSC_DnsServerZoneAging.psm1 b/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/DSCResources/DSC_DnsServerZoneAging/DSC_DnsServerZoneAging.psm1 new file mode 100644 index 0000000..9cf80c7 --- /dev/null +++ b/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/DSCResources/DSC_DnsServerZoneAging/DSC_DnsServerZoneAging.psm1 @@ -0,0 +1,196 @@ +$script:dscResourceCommonPath = Join-Path -Path $PSScriptRoot -ChildPath '..\..\Modules\DscResource.Common' +$script:dnsServerDscCommonPath = Join-Path -Path $PSScriptRoot -ChildPath '..\..\Modules\DnsServerDsc.Common' + +Import-Module -Name $script:dscResourceCommonPath +Import-Module -Name $script:dnsServerDscCommonPath + +$script:localizedData = Get-LocalizedData -DefaultUICulture 'en-US' + +<# + .SYNOPSIS + Get the DNS zone aging settings. + + .PARAMETER Name + Name of the DNS forward or reverse loookup zone. + + .PARAMETER Enabled + Option to enable scavenge stale resource records on the zone. +#> +function Get-TargetResource +{ + [CmdletBinding()] + [OutputType([System.Collections.Hashtable])] + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $Name, + + [Parameter(Mandatory = $true)] + [System.Boolean] + $Enabled + ) + + Write-Verbose -Message ($script:localizedData.GettingDnsZoneAgingMessage -f $Name) + + # Get the current zone aging from the local DNS server + $zoneAging = Get-DnsServerZoneAging -Name $Name + + return @{ + Name = $Name + Enabled = $zoneAging.AgingEnabled + RefreshInterval = $zoneAging.RefreshInterval.TotalHours + NoRefreshInterval = $zoneAging.NoRefreshInterval.TotalHours + } +} + +<# + .SYNOPSIS + Set the DNS zone aging settings. + + .PARAMETER Name + Name of the DNS forward or reverse loookup zone. + + .PARAMETER Enabled + Option to enable scavenge stale resource records on the zone. + + .PARAMETER RefreshInterval + Refresh interval for record scavencing in hours. Default value is 7 days. + + .PARAMETER NoRefreshInterval + No-refresh interval for record scavencing in hours. Default value is 7 days. +#> +function Set-TargetResource +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $Name, + + [Parameter(Mandatory = $true)] + [System.Boolean] + $Enabled, + + [Parameter()] + [System.UInt32] + $RefreshInterval = 168, + + [Parameter()] + [System.UInt32] + $NoRefreshInterval = 168 + ) + + $currentConfiguration = Get-TargetResource -Name $Name -Enabled $Enabled + + # Enable or disable zone aging + if ($currentConfiguration.Enabled -ne $Enabled) + { + if ($Enabled) + { + Write-Verbose -Message ($script:localizedData.EnableDnsZoneAgingMessage -f $Name) + } + else + { + Write-Verbose -Message ($script:localizedData.DisableDnsZoneAgingMessage -f $Name) + } + + Set-DnsServerZoneAging -Name $Name -Aging $Enabled -WarningAction 'SilentlyContinue' + } + + # Update the refresh interval + if ($PSBoundParameters.ContainsKey('RefreshInterval')) + { + if ($currentConfiguration.RefreshInterval -ne $RefreshInterval) + { + Write-Verbose -Message ($script:localizedData.SetDnsZoneRefreshIntervalMessage -f $RefreshInterval) + + $refreshIntervalTimespan = [System.TimeSpan]::FromHours($RefreshInterval) + + <# + Hide the following warning if aging is not enabled: Specified + parameters related to aging of records have been set. However, + aging was not enabled and hence the settings are ineffective. + #> + Set-DnsServerZoneAging -Name $Name -RefreshInterval $refreshIntervalTimespan -WarningAction 'SilentlyContinue' + } + } + + # Update the no refresh interval + if ($PSBoundParameters.ContainsKey('NoRefreshInterval')) + { + if ($currentConfiguration.NoRefreshInterval -ne $NoRefreshInterval) + { + Write-Verbose -Message ($script:localizedData.SetDnsZoneNoRefreshIntervalMessage -f $NoRefreshInterval) + + $noRefreshIntervalTimespan = [System.TimeSpan]::FromHours($NoRefreshInterval) + + <# + Hide the following warning if aging is not enabled: Specified + parameters related to aging of records have been set. However, + aging was not enabled and hence the settings are ineffective. + #> + Set-DnsServerZoneAging -Name $Name -NoRefreshInterval $noRefreshIntervalTimespan -WarningAction 'SilentlyContinue' + } + } +} + +<# + .SYNOPSIS + Test the DNS zone aging settings. + + .PARAMETER Name + Name of the DNS forward or reverse loookup zone. + + .PARAMETER Enabled + Option to enable scavenge stale resource records on the zone. + + .PARAMETER RefreshInterval + Refresh interval for record scavencing in hours. Default value is 7 days. + + .PARAMETER NoRefreshInterval + No-refresh interval for record scavencing in hours. Default value is 7 days. +#> +function Test-TargetResource +{ + [CmdletBinding()] + [OutputType([System.Boolean])] + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $Name, + + [Parameter(Mandatory = $true)] + [System.Boolean] + $Enabled, + + [Parameter()] + [System.UInt32] + $RefreshInterval = 168, + + [Parameter()] + [System.UInt32] + $NoRefreshInterval = 168 + ) + + Write-Verbose -Message ($script:localizedData.TestingDnsZoneAgingMessage -f $Name) + + $currentConfiguration = Get-TargetResource -Name $Name -Enabled $Enabled + + $isDesiredState = $currentConfiguration.Enabled -eq $Enabled + + if ($PSBoundParameters.ContainsKey('RefreshInterval')) + { + $isDesiredState = $isDesiredState -and $currentConfiguration.RefreshInterval -eq $RefreshInterval + } + + if ($PSBoundParameters.ContainsKey('NoRefreshInterval')) + { + $isDesiredState = $isDesiredState -and $currentConfiguration.NoRefreshInterval -eq $NoRefreshInterval + } + + return $isDesiredState +} diff --git a/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/DSCResources/DSC_DnsServerZoneAging/DSC_DnsServerZoneAging.schema.mof b/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/DSCResources/DSC_DnsServerZoneAging/DSC_DnsServerZoneAging.schema.mof new file mode 100644 index 0000000..6914f4d --- /dev/null +++ b/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/DSCResources/DSC_DnsServerZoneAging/DSC_DnsServerZoneAging.schema.mof @@ -0,0 +1,8 @@ +[ClassVersion("1.0.0.0"), FriendlyName("DnsServerZoneAging")] +class DSC_DnsServerZoneAging : OMI_BaseResource +{ + [Key, Description("Name of the DNS forward or reverse lookup zone.")] String Name; + [Required, Description("Option to enable scavenge stale resource records on the zone.")] Boolean Enabled; + [Write, Description("Refresh interval for record scavenging in hours. Default value is `168`, 7 days.")] UInt32 RefreshInterval; + [Write, Description("No-refresh interval for record scavenging in hours. Default value is `168`, 7 days.")] UInt32 NoRefreshInterval; +}; diff --git a/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/DSCResources/DSC_DnsServerZoneAging/README.md b/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/DSCResources/DSC_DnsServerZoneAging/README.md new file mode 100644 index 0000000..89130ae --- /dev/null +++ b/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/DSCResources/DSC_DnsServerZoneAging/README.md @@ -0,0 +1,5 @@ +# Description + +The DnsServerZoneAging DSC resource manages aging settings for a Domain Name System (DNS) server zone. + +A resource record can remain on a DNS server after the resource is no longer part of the network. Aging settings determine when a record can be removed, or scavenged, as a stale record. diff --git a/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/DSCResources/DSC_DnsServerZoneAging/en-US/DSC_DnsServerZoneAging.strings.psd1 b/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/DSCResources/DSC_DnsServerZoneAging/en-US/DSC_DnsServerZoneAging.strings.psd1 new file mode 100644 index 0000000..b68f180 --- /dev/null +++ b/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/DSCResources/DSC_DnsServerZoneAging/en-US/DSC_DnsServerZoneAging.strings.psd1 @@ -0,0 +1,9 @@ +# culture="en-US" +ConvertFrom-StringData @' + GettingDnsZoneAgingMessage = Getting the DNS zone aging for {0}. + EnableDnsZoneAgingMessage = Enable DNS zone aging on {0}. + DisableDnsZoneAgingMessage = Disable DNS zone aging on {0}. + SetDnsZoneRefreshIntervalMessage = Set DNS zone refresh interval to {0} hours. + SetDnsZoneNoRefreshIntervalMessage = Set DNS zone no refresh interval to {0} hours. + TestingDnsZoneAgingMessage = Testing the DNS zone aging for {0}. +'@ diff --git a/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/DSCResources/DSC_DnsServerZoneAging/en-US/about_DnsServerZoneAging.help.txt b/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/DSCResources/DSC_DnsServerZoneAging/en-US/about_DnsServerZoneAging.help.txt new file mode 100644 index 0000000..f1441fc --- /dev/null +++ b/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/DSCResources/DSC_DnsServerZoneAging/en-US/about_DnsServerZoneAging.help.txt @@ -0,0 +1,64 @@ +.NAME + DnsServerZoneAging + +.DESCRIPTION + The DnsServerZoneAging DSC resource manages aging settings for a Domain Name System (DNS) server zone. + + A resource record can remain on a DNS server after the resource is no longer part of the network. Aging settings determine when a record can be removed, or scavenged, as a stale record. + +.PARAMETER Name + Key - String + Name of the DNS forward or reverse lookup zone. + +.PARAMETER Enabled + Required - Boolean + Option to enable scavenge stale resource records on the zone. + +.PARAMETER RefreshInterval + Write - UInt32 + Refresh interval for record scavenging in hours. Default value is 168, 7 days. + +.PARAMETER NoRefreshInterval + Write - UInt32 + No-refresh interval for record scavenging in hours. Default value is 168, 7 days. + +.EXAMPLE 1 + +This configuration will manage aging of a DNS forward zone + +Configuration DnsServerZoneAging_forward_config +{ + Import-DscResource -ModuleName 'DnsServerDsc' + + Node localhost + { + DnsServerZoneAging 'DnsServerZoneAging' + { + Name = 'contoso.com' + Enabled = $true + RefreshInterval = 120 # 5 days + NoRefreshInterval = 240 # 10 days + } + } +} + +.EXAMPLE 2 + +This configuration will manage aging of a DNS reverse zone + +Configuration DnsServerZoneAging_reverse_config +{ + Import-DscResource -ModuleName 'DnsServerDsc' + + Node localhost + { + DnsServerZoneAging 'DnsServerReverseZoneAging' + { + Name = '168.192.in-addr-arpa' + Enabled = $true + RefreshInterval = 168 # 7 days + NoRefreshInterval = 168 # 7 days + } + } +} + diff --git a/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/DSCResources/DSC_DnsServerZoneScope/DSC_DnsServerZoneScope.psm1 b/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/DSCResources/DSC_DnsServerZoneScope/DSC_DnsServerZoneScope.psm1 new file mode 100644 index 0000000..2825348 --- /dev/null +++ b/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/DSCResources/DSC_DnsServerZoneScope/DSC_DnsServerZoneScope.psm1 @@ -0,0 +1,144 @@ +$script:dscResourceCommonPath = Join-Path -Path $PSScriptRoot -ChildPath '..\..\Modules\DscResource.Common' +$script:dnsServerDscCommonPath = Join-Path -Path $PSScriptRoot -ChildPath '..\..\Modules\DnsServerDsc.Common' + +Import-Module -Name $script:dscResourceCommonPath +Import-Module -Name $script:dnsServerDscCommonPath + +$script:localizedData = Get-LocalizedData -DefaultUICulture 'en-US' + +<# + .SYNOPSIS + This will return the current state of the resource. + + .PARAMETER Name + Specifies the name of the Zone Scope. + + .PARAMETER ZoneName + Specify the existing DNS Zone to add a scope to. +#> +function Get-TargetResource +{ + [CmdletBinding()] + [OutputType([System.Collections.Hashtable])] + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $Name, + + [Parameter(Mandatory = $true)] + [System.String] + $ZoneName + + ) + + Write-Verbose -Message ($script:localizedData.GettingDnsServerZoneScopeMessage -f $Name, $ZoneName) + $record = Get-DnsServerZoneScope -Name $Name -ZoneName $ZoneName -ErrorAction SilentlyContinue + + if ($null -eq $record) + { + return @{ + Name = $Name + ZoneName = $ZoneName + ZoneFile = $null + Ensure = 'Absent' + } + } + + return @{ + Name = $record.ZoneScope + ZoneName = $record.ZoneName + ZoneFile = $record.FileName + Ensure = 'Present' + } +} #end function Get-TargetResource + +<# + .SYNOPSIS + This will configure the resource. + + .PARAMETER Name + Specifies the name of the Zone Scope. + + .PARAMETER ZoneName + Specify the existing DNS Zone to add a scope to. +#> +function Set-TargetResource +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $Name, + + [Parameter(Mandatory = $true)] + [System.String] + $ZoneName, + + [Parameter()] + [ValidateSet('Present','Absent')] + [System.String] + $Ensure = 'Present' + ) + + $clientSubnet = Get-DnsServerZoneScope -Name $Name -ZoneName $ZoneName -ErrorAction SilentlyContinue + if ($Ensure -eq 'Present') + { + if (!$clientSubnet) + { + Write-Verbose -Message ($script:localizedData.CreatingDnsServerZoneScopeMessage -f $Name, $ZoneName) + Add-DnsServerZoneScope -ZoneName $ZoneName -Name $Name + } + } + elseif ($Ensure -eq 'Absent') + { + Write-Verbose -Message ($script:localizedData.RemovingDnsServerZoneScopeMessage -f $Name, $ZoneName) + Remove-DnsServerZoneScope -Name $Name -ZoneName $ZoneName + } +} #end function Set-TargetResource + +<# + .SYNOPSIS + This will return whether the resource is in desired state. + + .PARAMETER Name + Specifies the name of the Zone Scope. + + .PARAMETER ZoneName + Specify the existing DNS Zone to add a scope to. +#> +function Test-TargetResource +{ + [CmdletBinding()] + [OutputType([System.Boolean])] + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $Name, + + [Parameter(Mandatory = $true)] + [System.String] + $ZoneName, + + [Parameter()] + [ValidateSet('Present','Absent')] + [System.String] + $Ensure = 'Present' + ) + + $result = Get-TargetResource -Name $Name -ZoneName $ZoneName + + if ($Ensure -ne $result.Ensure) + { + Write-Verbose -Message ($script:localizedData.NotDesiredPropertyMessage -f 'Ensure', $Ensure, $result.Ensure) + Write-Verbose -Message ($script:localizedData.NotInDesiredStateMessage -f $Name) + return $false + } + + Write-Verbose -Message ($script:localizedData.InDesiredStateMessage -f $Name) + return $true +} #end function Test-TargetResource + +Export-ModuleMember -Function *-TargetResource diff --git a/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/DSCResources/DSC_DnsServerZoneScope/DSC_DnsServerZoneScope.schema.mof b/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/DSCResources/DSC_DnsServerZoneScope/DSC_DnsServerZoneScope.schema.mof new file mode 100644 index 0000000..6c1b735 --- /dev/null +++ b/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/DSCResources/DSC_DnsServerZoneScope/DSC_DnsServerZoneScope.schema.mof @@ -0,0 +1,8 @@ +[ClassVersion("1.0.0.0"), FriendlyName("DnsServerZoneScope")] +class DSC_DnsServerZoneScope : OMI_BaseResource +{ + [Key, Description("Specifies the name of the Zone Scope.")] string Name; + [Key, Description("Specify the existing DNS Zone to add a scope to.")] string ZoneName; + [Write, Description("Should this DNS Server Zone Scope be present or absent"), ValueMap{"Present","Absent"}, Values{"Present","Absent"}] String Ensure; + [Read, Description("Returns the zone scope filename.")] string ZoneFile; +}; diff --git a/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/DSCResources/DSC_DnsServerZoneScope/README.md b/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/DSCResources/DSC_DnsServerZoneScope/README.md new file mode 100644 index 0000000..394a94f --- /dev/null +++ b/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/DSCResources/DSC_DnsServerZoneScope/README.md @@ -0,0 +1,9 @@ +# Description + +The DnsServerZoneScope DSC resource manages the zone scope on an existing zone on the Domain Name System (DNS) server + +The name of the scope should adhere to the same conventions as the zone name. The scope name cannot be same as the zone name to which it is attached. + +## Requirements + +- Target machine must be running Windows Server 2016 or later. diff --git a/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/DSCResources/DSC_DnsServerZoneScope/en-US/DSC_DnsServerZoneScope.strings.psd1 b/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/DSCResources/DSC_DnsServerZoneScope/en-US/DSC_DnsServerZoneScope.strings.psd1 new file mode 100644 index 0000000..37ddd46 --- /dev/null +++ b/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/DSCResources/DSC_DnsServerZoneScope/en-US/DSC_DnsServerZoneScope.strings.psd1 @@ -0,0 +1,9 @@ +# culture="en-US" +ConvertFrom-StringData @' + GettingDnsServerZoneScopeMessage = Getting DNS Server Zone Scope '{0}' in '{1}'. + CreatingDnsServerZoneScopeMessage = Creating DNS Server Zone Scope '{0}' in '{1}'. + RemovingDnsServerZoneScopeMessage = Removing DNS Server Zone Scope '{0}' from '{1}'. + NotDesiredPropertyMessage = DNS Server Zone Scope property '{0}' is not correct. Expected '{1}', actual '{2}' + InDesiredStateMessage = DNS Server Zone Scope '{0}' is in the desired state. + NotInDesiredStateMessage = DNS Server Zone Scope '{0}' is NOT in the desired state. +'@ diff --git a/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/DSCResources/DSC_DnsServerZoneScope/en-US/about_DnsServerZoneScope.help.txt b/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/DSCResources/DSC_DnsServerZoneScope/en-US/about_DnsServerZoneScope.help.txt new file mode 100644 index 0000000..d2f52df --- /dev/null +++ b/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/DSCResources/DSC_DnsServerZoneScope/en-US/about_DnsServerZoneScope.help.txt @@ -0,0 +1,45 @@ +.NAME + DnsServerZoneScope + +.DESCRIPTION + The DnsServerZoneScope DSC resource manages the zone scope on an existing zone on the Domain Name System (DNS) server + + The name of the scope should adhere to the same conventions as the zone name. The scope name cannot be same as the zone name to which it is attached. + + ## Requirements + + - Target machine must be running Windows Server 2016 or later. + +.PARAMETER Name + Key - String + Specifies the name of the Zone Scope. + +.PARAMETER ZoneName + Key - String + Specify the existing DNS Zone to add a scope to. + +.PARAMETER Ensure + Write - String + Allowed values: Present, Absent + Should this DNS Server Zone Scope be present or absent + +.PARAMETER ZoneFile + Read - String + Returns the zone scope filename. + +.EXAMPLE 1 + +This configuration will manage a DNS zone scope + +Configuration DnsServerZoneScope_config +{ + Import-DscResource -ModuleName 'DnsServerDsc' + + DnsServerZoneScope 'ZoneScope1' + { + Name = 'contoso_NorthAmerica' + ZoneName = 'contoso.com' + Ensure = 'Present' + } +} + diff --git a/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/DSCResources/DSC_DnsServerZoneTransfer/DSC_DnsServerZoneTransfer.psm1 b/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/DSCResources/DSC_DnsServerZoneTransfer/DSC_DnsServerZoneTransfer.psm1 new file mode 100644 index 0000000..1237f1f --- /dev/null +++ b/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/DSCResources/DSC_DnsServerZoneTransfer/DSC_DnsServerZoneTransfer.psm1 @@ -0,0 +1,238 @@ +$script:dscResourceCommonPath = Join-Path -Path $PSScriptRoot -ChildPath '..\..\Modules\DscResource.Common' +$script:dnsServerDscCommonPath = Join-Path -Path $PSScriptRoot -ChildPath '..\..\Modules\DnsServerDsc.Common' + +Import-Module -Name $script:dscResourceCommonPath +Import-Module -Name $script:dnsServerDscCommonPath + +$script:localizedData = Get-LocalizedData -DefaultUICulture 'en-US' + +# Allow transfer to any server use 0, to one in name tab 1, specific one 2, no transfer 3 +$XferId2Name= @('Any','Named','Specific','None') + +function Get-TargetResource +{ + [CmdletBinding()] + [OutputType([System.Collections.Hashtable])] + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $Name, + + [Parameter(Mandatory = $true)] + [ValidateSet("None","Any","Named","Specific")] + [System.String] + $Type + ) + +#region Input Validation + + # Check for DnsServer module/role + Assert-Module -ModuleName 'DnsServer' + +#endregion + Write-Verbose -Message 'Getting DNS zone.' + $currentZone = Get-CimInstance ` + -ClassName MicrosoftDNS_Zone ` + -Namespace root\MicrosoftDNS ` + -Verbose:$false | Where-Object -FilterScript {$_.Name -eq $Name} + + @{ + Name = $Name + Type = $XferId2Name[$currentZone.SecureSecondaries] + SecondaryServer = $currentZone.SecondaryServers + } +} + +function Set-TargetResource +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $Name, + + [Parameter(Mandatory = $true)] + [ValidateSet("None","Any","Named","Specific")] + [System.String] + $Type, + + [Parameter()] + [String[]] + $SecondaryServer + ) + Write-Verbose -Message 'Setting DNS zone.' + if ($PSBoundParameters.ContainsKey('Debug')) + { + $null = $PSBoundParameters.Remove('Debug') + } + Test-ResourceProperties @PSBoundParameters -Apply + + # Restart the DNS service + Restart-Service -Name DNS +} + + +function Test-TargetResource +{ + [CmdletBinding()] + [OutputType([System.Boolean])] + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $Name, + + [Parameter(Mandatory = $true)] + [ValidateSet("None","Any","Named","Specific")] + [System.String] + $Type, + + [Parameter()] + [String[]] + $SecondaryServer + ) + +#region Input Validation + + # Check for DnsServer module/role + Assert-Module -ModuleName 'DnsServer' + +#endregion + Write-Verbose -Message 'Validating DNS zone.' + if ($PSBoundParameters.ContainsKey('Debug')) + { + $null = $PSBoundParameters.Remove('Debug') + } + Test-ResourceProperties @PSBoundParameters +} + +function Test-ResourceProperties +{ + [CmdletBinding()] + [OutputType([System.Boolean])] + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $Name, + + [Parameter(Mandatory = $true)] + [ValidateSet("None","Any","Named","Specific")] + [System.String] + $Type, + + [Parameter()] + [String[]] + $SecondaryServer, + + [Parameter()] + [Switch] + $Apply + ) + + $checkZoneMessage = $($script:localizedData.CheckingZoneMessage) ` + -f $Name + Write-Verbose -Message $checkZoneMessage + + # Get the current value of transfer zone + $currentZone = Get-CimInstance ` + -ClassName MicrosoftDNS_Zone ` + -Namespace root\MicrosoftDNS ` + -Verbose:$false | Where-Object -FilterScript {$_.Name -eq $Name} + $currentZoneTransfer = $currentZone.SecureSecondaries + + # Hashtable with 2 keys: SecureSecondaries,SecondaryServers + $Arguments = @{} + + switch ($Type) + { + 'None' + { + $Arguments['SecureSecondaries'] = 3 + } + 'Any' + { + $Arguments['SecureSecondaries'] = 0 + } + 'Named' + { + $Arguments['SecureSecondaries'] = 1 + } + 'Specific' + { + $Arguments['SecureSecondaries'] = 2 + $Arguments['SecondaryServers']=$SecondaryServer + } + } + + # Check the current value against expected value + if ($currentZoneTransfer -eq $Arguments.SecureSecondaries) + { + $desiredZoneMessage = ($script:localizedData.DesiredZoneMessage) ` + -f $XferId2Name[$currentZoneTransfer] + Write-Verbose -Message $desiredZoneMessage + + # If the Type is specific, and SecondaryServer doesn't match + if (($currentZoneTransfer -eq 2) ` + -and (Compare-Object $currentZone.SecondaryServers $SecondaryServer)) + { + $notDesiredPropertyMessage = ($script:localizedData.NotDesiredPropertyMessage) ` + -f ($SecondaryServer -join ','),($currentZone.SecondaryServers -join ',') + Write-Verbose -Message $notDesiredPropertyMessage + + # Set the SecondaryServer property + if ($Apply) + { + $settingPropertyMessage = ($script:localizedData.SettingPropertyMessage) ` + -f ($SecondaryServer -join ',') + Write-Verbose -Message $settingPropertyMessage + + $null = Invoke-CimMethod ` + -InputObject $currentZone ` + -MethodName ResetSecondaries ` + -Arguments $Arguments ` + -Verbose:$false + + $setPropertyMessage = $script:localizedData.SetPropertyMessage + Write-Verbose -Message $setPropertyMessage + } + else + { + return $false + } + } # end SecondaryServer match + + if (-not $Apply) + { + return $true + } + } # end currentZoneTransfer -eq ExpectedZoneTransfer + else + { + $notDesiredZoneMessage = $($script:localizedData.NotDesiredZoneMessage) ` + -f $XferId2Name[$Arguments.SecureSecondaries], ` + $XferId2Name[$currentZoneTransfer] + Write-Verbose -Message $notDesiredZoneMessage + + if ($Apply) + { + $null = Invoke-CimMethod ` + -InputObject $currentZone ` + -MethodName ResetSecondaries ` + -Arguments $Arguments ` + -Verbose:$false + + $setZoneMessage = $($script:localizedData.SetZoneMessage) ` + -f $Name,$XferId2Name[$Arguments.SecureSecondaries] + Write-Verbose -Message $setZoneMessage + } + else + { + return $false + } + } # end currentZoneTransfer -ne ExpectedZoneTransfer +} + +Export-ModuleMember -Function *-TargetResource diff --git a/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/DSCResources/DSC_DnsServerZoneTransfer/DSC_DnsServerZoneTransfer.schema.mof b/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/DSCResources/DSC_DnsServerZoneTransfer/DSC_DnsServerZoneTransfer.schema.mof new file mode 100644 index 0000000..cedda4b --- /dev/null +++ b/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/DSCResources/DSC_DnsServerZoneTransfer/DSC_DnsServerZoneTransfer.schema.mof @@ -0,0 +1,7 @@ +[ClassVersion("1.0.0.0"), FriendlyName("DnsServerZoneTransfer")] +class DSC_DnsServerZoneTransfer : OMI_BaseResource +{ + [Key, Description("Name of the DNS zone")] String Name; + [Required, Description("Type of transfer allowed"), ValueMap{"None","Any","Named","Specific"}, Values{"None","Any","Named","Specific"}] String Type; + [Write, Description("IP address or DNS name of DNS servers where zone information can be transferred")] String SecondaryServer[]; +}; diff --git a/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/DSCResources/DSC_DnsServerZoneTransfer/README.md b/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/DSCResources/DSC_DnsServerZoneTransfer/README.md new file mode 100644 index 0000000..137fb2c --- /dev/null +++ b/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/DSCResources/DSC_DnsServerZoneTransfer/README.md @@ -0,0 +1,3 @@ +# Description + +The DnsServerZoneTransfer DSC resource manages the replication settings of DNS Server zone data between servers. diff --git a/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/DSCResources/DSC_DnsServerZoneTransfer/en-US/DSC_DnsServerZoneTransfer.strings.psd1 b/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/DSCResources/DSC_DnsServerZoneTransfer/en-US/DSC_DnsServerZoneTransfer.strings.psd1 new file mode 100644 index 0000000..2f634c2 --- /dev/null +++ b/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/DSCResources/DSC_DnsServerZoneTransfer/en-US/DSC_DnsServerZoneTransfer.strings.psd1 @@ -0,0 +1,10 @@ +# culture="en-US" +ConvertFrom-StringData @' + CheckingZoneMessage = Checking the current zone transfer for DNS server zone {0} ... + DesiredZoneMessage = Current zone transfer settings for the given DNS server zone is correctly set to {0} + NotDesiredZoneMessage = DNS server zone transfer settings is not correct. Expected {0}, actual {1} + SetZoneMessage = Current zone transfer setting for DNS server zone {0} is set to {1} + NotDesiredPropertyMessage = DNS server zone transfer secondary servers are not correct. Expected {0}, actual {1} + SettingPropertyMessage = Setting DNS server zone transfer secondary servers to {0} ... + SetPropertyMessage = DNS server zone transfer secondary servers are set +'@ diff --git a/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/DSCResources/DSC_DnsServerZoneTransfer/en-US/about_DnsServerZoneTransfer.help.txt b/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/DSCResources/DSC_DnsServerZoneTransfer/en-US/about_DnsServerZoneTransfer.help.txt new file mode 100644 index 0000000..7013912 --- /dev/null +++ b/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/DSCResources/DSC_DnsServerZoneTransfer/en-US/about_DnsServerZoneTransfer.help.txt @@ -0,0 +1,34 @@ +.NAME + DnsServerZoneTransfer + +.DESCRIPTION + The DnsServerZoneTransfer DSC resource manages the replication settings of DNS Server zone data between servers. + +.PARAMETER Name + Key - String + Name of the DNS zone + +.PARAMETER Type + Required - String + Allowed values: None, Any, Named, Specific + Type of transfer allowed + +.PARAMETER SecondaryServer + Write - StringArray + IP address or DNS name of DNS servers where zone information can be transferred + +.EXAMPLE 1 + +This configuration will manage a DNS zone transfer + +Configuration DnsServerZoneTransfer_config +{ + Import-DscResource -ModuleName 'DnsServerDsc' + + DnsServerZoneTransfer 'TransferToAnyServer' + { + Name = 'demo.contoso.com' + Type = 'Any' + } +} + diff --git a/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/DnsServerDsc.psd1 b/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/DnsServerDsc.psd1 new file mode 100644 index 0000000..f035581 --- /dev/null +++ b/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/DnsServerDsc.psd1 @@ -0,0 +1,192 @@ +@{ + # Version number of this module. + moduleVersion = '3.0.0' + + # ID used to uniquely identify this module + GUID = '5f70e6a1-f1b2-4ba0-8276-8967d43a7ec2' + + # Author of this module + Author = 'DSC Community' + + # Company or vendor of this module + CompanyName = 'DSC Community' + + # Copyright statement for this module + Copyright = 'Copyright the DSC Community contributors. All rights reserved.' + + # Description of the functionality provided by this module + Description = 'This module contains DSC resources for the management and configuration of Windows Server DNS Server.' + + # Minimum version of the Windows PowerShell engine required by this module + PowerShellVersion = '5.0' + + # Script module or binary module file associated with this manifest. + RootModule = 'DnsServerDsc.psm1' + + # Functions to export from this module + FunctionsToExport = @() + + # Cmdlets to export from this module + CmdletsToExport = @() + + # Variables to export from this module + VariablesToExport = @() + + # Aliases to export from this module + AliasesToExport = @() + + DscResourcesToExport = @('DnsRecordCname','DnsRecordPtr','DnsRecordA','DnsRecordAaaa','DnsRecordCnameScoped','DnsRecordMx','DnsRecordNs','DnsRecordSrv','DnsServerCache','DnsServerDsSetting','DnsServerEDns','DnsServerRecursion','DnsServerScavenging','DnsRecordAaaaScoped','DnsRecordAScoped','DnsRecordMxScoped','DnsRecordNsScoped','DnsRecordSrvScoped','DnsServerADZone','DnsServerClientSubnet','DnsServerConditionalForwarder','DnsServerDiagnostics','DnsServerForwarder','DnsServerPrimaryZone','DnsServerRootHint','DnsServerSecondaryZone','DnsServerSetting','DnsServerSettingLegacy','DnsServerZoneAging','DnsServerZoneScope','DnsServerZoneTransfer') + + <# + Private data to pass to the module specified in RootModule/ModuleToProcess. + This may also contain a PSData hashtable with additional module metadata used by PowerShell. + #> + PrivateData = @{ + PSData = @{ + # Set to a prerelease string value if the release should be a prerelease. + Prerelease = '' + + # Tags applied to this module. These help with module discovery in online galleries. + Tags = @('DesiredStateConfiguration', 'DSC', 'DSCResourceKit', 'DSCResource') + + # A URL to the license for this module. + LicenseUri = 'https://github.com/dsccommunity/DnsServerDsc/blob/main/LICENSE' + + # A URL to the main website for this project. + ProjectUri = 'https://github.com/dsccommunity/DnsServerDsc' + + # A URL to an icon representing this module. + IconUri = 'https://dsccommunity.org/images/DSC_Logo_300p.png' + + # ReleaseNotes of this module + ReleaseNotes = '## [3.0.0] - 2021-05-26 + +### Removed + +- xDnsRecord + - BREAKING CHANGE: The resource has been replaced by _DnsServerA_, _DnsServerPtr_, + and _DnsServerCName_ ([issue #221](https://github.com/dsccommunity/DnsServerDsc/issues/221)). +- xDnsServerMx + - BREAKING CHANGE: The resource has been replaced by _DnsServerMx_ ([issue #228](https://github.com/dsccommunity/DnsServerDsc/issues/228)). +- DnsServerSetting + - BREAKING CHANGE: The properties `Forwarders` and `ForwardingTimeout` has + been removed ([issue #192](https://github.com/dsccommunity/DnsServerDsc/issues/192)). + Use the resource _DnsServerForwarder_ to enforce these properties. + - BREAKING CHANGE: The properties `EnableEDnsProbes` and `EDnsCacheTimeout` has + been removed ([issue #195](https://github.com/dsccommunity/DnsServerDsc/issues/195)). + Use the resource _DnsServerEDns_ to enforce these properties. + - BREAKING CHANGE: The properties `SecureResponses`, `MaxCacheTTL`, and + `MaxNegativeCacheTTL` has been removed ([issue #197](https://github.com/dsccommunity/DnsServerDsc/issues/197)). + To enforce theses properties, use resource _DnsServerEDns_ using the + properties `EnablePollutionProtection`, `MaxTtl`, and `MaxNegativeTtl` + respectively. + - BREAKING CHANGE: The properties `DefaultAgingState`, `ScavengingInterval`, + `DefaultNoRefreshInterval`, and `DefaultRefreshInterval` have been removed. + Use the resource _DnsServerScavenging_ to enforce this properties ([issue #193](https://github.com/dsccommunity/DnsServerDsc/issues/193)). + - BREAKING CHANGE: The properties `NoRecursion`, `RecursionRetry`, and + `RecursionTimeout` has been removed ([issue #200](https://github.com/dsccommunity/DnsServerDsc/issues/200)). + To enforce theses properties, use resource _DnsServerRecursion_ using the + properties `Enable`, `RetryInterval`, and `Timeout` respectively. + - BREAKING CHANGE: A few properties that are not supported by any DNS + Server PowerShell cmdlet was moved to the new resource _DnsServerSettingLegacy_. + - BREAKING CHANGE: The properties `DsPollingInterval` and `DsTombstoneInterval` + has been removed ([issue #252](https://github.com/dsccommunity/DnsServerDsc/issues/252)). + Use the resource _DnsServerDsSetting_ to enforce these properties. + +- ResourceBase + - For the method `Get()` the overload that took a `[Microsoft.Management.Infrastructure.CimInstance]` + was removed as it is not the correct pattern going forward. + +### Added + +- DnsServerDsc + - Added new resource + - _DnsServerCache_ - resource to enforce cache settings ([issue #196](https://github.com/dsccommunity/DnsServerDsc/issues/196)). + - _DnsServerRecursion_ - resource to enforce recursion settings ([issue #198](https://github.com/dsccommunity/DnsServerDsc/issues/198)). + - Added new private function `Get-ClassName` that returns the class name + or optionally an array with the class name and all inherited base class + named. + - Added new private function `Get-LocalizedDataRecursive` that gathers + all localization strings from an array of class names. This can be used + in classes to be able to inherit localization strings from one or more + base class. If a localization string key exist in a parent class''s + localization string file it will override the localization string key + in any base class. + - Fixed code coverage in the pipeline ([issue #246](https://github.com/dsccommunity/DnsServerDsc/issues/246)). +- ResourceBase + - Added new method `Assert()` tha calls `Assert-Module` and `AssertProperties()`. +- DnsRecordNs + - Added new resource to manage NS records +- DnsRecordNsScoped + - Added new resource to manage scoped NS records +- DnsServerDsSetting + - Added new resource to manage AD-integrated DNS settings +- DnsServerSettingLegacy + - A new resource to manage legacy DNS Server settings that are not supported + by any DNS Server PowerShell cmdlet. + +### Changed + +- DnsServerDsc + - BREAKING CHANGE: Renamed the module to DnsServerDsc ([issue #179](https://github.com/dsccommunity/DnsServerDsc/issues/179)). + - BREAKING CHANGE: Removed the prefix ''x'' from all MOF-based resources + ([issue #179](https://github.com/dsccommunity/DnsServerDsc/issues/179)). + - Renamed a MOF-based resource to use the prefix ''DSC'' ([issue #225](https://github.com/dsccommunity/DnsServerDsc/issues/225)). + - Fix stub `Get-DnsServerResourceRecord` so it throws if it is not mocked + correctly ([issue #204](https://github.com/dsccommunity/DnsServerDsc/issues/204)). + - Switch the order in the deploy pipeline so that creating the GitHub release + is made after a successful release. + - Updated stub functions to throw if they are used (when missing a mock in + unit test) ([issue #235](https://github.com/dsccommunity/DnsServerDsc/issues/235)). +- ResourceBase + - Added support for inherit localization strings and also able to override + a localization string that exist in a base class. + - Moved more logic from the resources into the base class for the method + `Test()`, `Get()`, and `Set()`. The base class now have three methods + `AssertProperties()`, `Modify()`, and `GetCurrentState()` where the + two latter ones must be overridden by a resource if calling the base + methods `Set()` and `Get()`. + - Moved the `Assert-Module` from the constructor to a new method `Assert()` + that is called from `Get()`, `Test()`, and `Set()`. The method `Assert()` + also calls the method `AssertProperties()`. The method `Assert()` is not + meant to be overridden, but can if there is a reason not to run + `Assert-Module` and or `AssertProperties()`. +- Integration tests + - Added commands in the DnsRecord* integration tests to wait for the LCM + before moving to the next test. +- DnsServerCache + - Moved to the same coding pattern as _DnsServerRecursion_. +- DnsServerEDns + - Moved to the same coding pattern as _DnsServerRecursion_. +- DnsServerScavenging + - Moved to the same coding pattern as _DnsServerRecursion_. +- DnsServerSetting + - Changed to use `Get-DnsServerSetting` and `Set-DnsServerSetting` + ([issue #185](https://github.com/dsccommunity/xDnsServer/issues/185)). + - BREAKING CHANGE: The property `DisableAutoReverseZones` have been renamed + to `DisableAutoReverseZone`. + - BREAKING CHANGE: The property `ListenAddresses` have been renamed + to `ListeningIPAddress`. + - BREAKING CHANGE: The property `AllowUpdate` was changed to a boolean + value (`$true` or `$false`) since that is what the cmdlet `Set-DnsServerSetting` + is expecting (related to [issue #101](https://github.com/dsccommunity/xDnsServer/issues/101)). + - BREAKING CHANGE: The property `EnableDnsSec` was changed to a boolean + value (`$true` or `$false`) since that is what the cmdlet `Set-DnsServerSetting` + is expecting. + - BREAKING CHANGE: The property `ForwardDelegations` was changed to a boolean + value (`$true` or `$false`) since that is what the cmdlet `Set-DnsServerSetting` + is expecting. + +### Fixed + +- Logic bug in DnsRecordPtr.expandIPv6String($string) (#255) + - Supporting tests added + +' + } # End of PSData hashtable + } # End of PrivateData hashtable +} + + + + diff --git a/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/DnsServerDsc.psm1 b/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/DnsServerDsc.psm1 new file mode 100644 index 0000000..98e405f --- /dev/null +++ b/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/DnsServerDsc.psm1 @@ -0,0 +1,3044 @@ +#Region './prefix.ps1' 0 +# Import nested, 'DscResource.Common' module +$script:dscResourceCommonModulePath = Join-Path -Path $PSScriptRoot -ChildPath 'Modules\DscResource.Common' +Import-Module -Name $script:dscResourceCommonModulePath + +$script:localizedData = Get-LocalizedData -DefaultUICulture 'en-US' +#EndRegion './prefix.ps1' 6 +#Region './Enum/1.Ensure.ps1' 0 +enum Ensure +{ + Present + Absent +} +#EndRegion './Enum/1.Ensure.ps1' 6 +#Region './Classes/001.ResourceBase.ps1' 0 +<# + .SYNOPSIS + A class with methods that are equal for all class-based resources. + + .DESCRIPTION + A class with methods that are equal for all class-based resources. + + .NOTES + This class should not contain any DSC properties. +#> + +class ResourceBase +{ + # Hidden property for holding localization strings + hidden [System.Collections.Hashtable] $localizedData = @{} + + # Default constructor + ResourceBase() + { + $this.localizedData = Get-LocalizedDataRecursive -ClassName ($this | Get-ClassName -Recurse) + } + + [ResourceBase] Get() + { + $this.Assert() + + Write-Verbose -Message ($this.localizedData.GetCurrentState -f $this.DnsServer, $this.GetType().Name) + + # Get all key properties. + $keyProperty = $this | + Get-Member -MemberType 'Property' | + Select-Object -ExpandProperty Name | + Where-Object -FilterScript { + $this.GetType().GetMember($_).CustomAttributes.Where( { $_.NamedArguments.MemberName -eq 'Key' }).NamedArguments.TypedValue.Value -eq $true + } + + $getParameters = @{} + + # Set each key property to its value (property DnsServer is handled below). + $keyProperty | + Where-Object -FilterScript { + $_ -ne 'DnsServer' + } | + ForEach-Object -Process { + $getParameters[$_] = $this.$_ + } + + # Set ComputerName depending on value of DnsServer. + if ($this.DnsServer -ne 'localhost') + { + $getParameters['ComputerName'] = $this.DnsServer + } + + $getCurrentStateResult = $this.GetCurrentState($getParameters) + + $dscResourceObject = [System.Activator]::CreateInstance($this.GetType()) + + foreach ($propertyName in $this.PSObject.Properties.Name) + { + if ($propertyName -in @($getCurrentStateResult.PSObject.Properties.Name)) + { + $dscResourceObject.$propertyName = $getCurrentStateResult.$propertyName + } + } + + # Always set this as it won't be in the $getCurrentStateResult + $dscResourceObject.DnsServer = $this.DnsServer + + # Return properties. + return $dscResourceObject + } + + [void] Set() + { + $this.Assert() + + Write-Verbose -Message ($this.localizedData.SetDesiredState -f $this.DnsServer, $this.GetType().Name) + + # Call the Compare method to get enforced properties that are not in desired state. + $propertiesNotInDesiredState = $this.Compare() + + if ($propertiesNotInDesiredState) + { + $propertiesToModify = $this.GetDesiredStateForSplatting($propertiesNotInDesiredState) + + $propertiesToModify.Keys | ForEach-Object -Process { + Write-Verbose -Message ($this.localizedData.SetProperty -f $_, $propertiesToModify.$_, $this.GetType().Name) + } + + if ($this.DnsServer -ne 'localhost') + { + $propertiesToModify['ComputerName'] = $this.DnsServer + } + + <# + Call the Modify() method with the properties that should be enforced + and was not in desired state. + #> + $this.Modify($propertiesToModify) + } + else + { + Write-Verbose -Message $this.localizedData.NoPropertiesToSet + } + } + + [System.Boolean] Test() + { + Write-Verbose -Message ($this.localizedData.TestDesiredState -f $this.DnsServer, $this.GetType().Name) + + $this.Assert() + + $isInDesiredState = $true + + <# + Returns all enforced properties not in desires state, or $null if + all enforced properties are in desired state. + #> + $propertiesNotInDesiredState = $this.Compare() + + if ($propertiesNotInDesiredState) + { + $isInDesiredState = $false + } + + if ($isInDesiredState) + { + Write-Verbose -Message ($this.localizedData.InDesiredState -f $this.DnsServer, $this.GetType().Name) + } + else + { + Write-Verbose -Message ($this.localizedData.NotInDesiredState -f $this.DnsServer, $this.GetType().Name) + } + + return $isInDesiredState + } + + <# + Returns a hashtable containing all properties that should be enforced. + This method should normally not be overridden. + #> + hidden [System.Collections.Hashtable[]] Compare() + { + $currentState = $this.Get() | ConvertTo-HashTableFromObject + $desiredState = $this | ConvertTo-HashTableFromObject + + <# + Remove properties that have $null as the value, and remove read + properties so that there is no chance to compare those. + #> + @($desiredState.Keys) | ForEach-Object -Process { + $isReadProperty = $this.GetType().GetMember($_).CustomAttributes.Where( { $_.NamedArguments.MemberName -eq 'NotConfigurable' }).NamedArguments.TypedValue.Value -eq $true + + if ($isReadProperty -or $null -eq $desiredState[$_]) + { + $desiredState.Remove($_) + } + } + + $CompareDscParameterState = @{ + CurrentValues = $currentState + DesiredValues = $desiredState + Properties = $desiredState.Keys + ExcludeProperties = @('DnsServer') + IncludeValue = $true + } + + <# + Returns all enforced properties not in desires state, or $null if + all enforced properties are in desired state. + #> + return (Compare-DscParameterState @CompareDscParameterState) + } + + # Returns a hashtable containing all properties that should be enforced. + hidden [System.Collections.Hashtable] GetDesiredStateForSplatting([System.Collections.Hashtable[]] $Properties) + { + $desiredState = @{} + + $Properties | ForEach-Object -Process { + $desiredState[$_.Property] = $_.ExpectedValue + } + + return $desiredState + } + + # This method should normally not be overridden. + hidden [void] Assert() + { + Assert-Module -ModuleName 'DnsServer' + + $this.AssertProperties() + } + + # This method can be overridden if resource specific asserts are needed. + hidden [void] AssertProperties() + { + } + + # This method must be overridden by a resource. + hidden [void] Modify([System.Collections.Hashtable] $properties) + { + throw $this.localizedData.ModifyMethodNotImplemented + } + + # This method must be overridden by a resource. + hidden [Microsoft.Management.Infrastructure.CimInstance] GetCurrentState([System.Collections.Hashtable] $properties) + { + throw $this.localizedData.GetCurrentStateMethodNotImplemented + } +} +#EndRegion './Classes/001.ResourceBase.ps1' 212 +#Region './Classes/001.ResourcePropertiesBase.ps1' 0 +<# + .SYNOPSIS + A class with DSC properties that are equal for all class-based resources. + + .DESCRIPTION + A class with DSC properties that are equal for all class-based resources. + + .PARAMETER DnsServer + The host name of the Domain Name System (DNS) server, or use 'localhost' + for the current node. Defaults to `'localhost'`. +#> + +class ResourcePropertiesBase +{ + [DscProperty()] + [System.String] + $DnsServer = 'localhost' +} +#EndRegion './Classes/001.ResourcePropertiesBase.ps1' 19 +#Region './Classes/002.DnsRecordBase.ps1' 0 +<# + .SYNOPSIS + A DSC Resource for MS DNS Server that is not exposed to end users representing the common fields available to all resource records. + + .DESCRIPTION + A DSC Resource for MS DNS Server that is not exposed to end users representing the common fields available to all resource records. + + .PARAMETER ZoneName + Specifies the name of a DNS zone. (Key Parameter) + + .PARAMETER TimeToLive + Specifies the TimeToLive value of the SRV record. Value must be in valid TimeSpan string format (i.e.: Days.Hours:Minutes:Seconds.Miliseconds or 30.23:59:59.999). + + .PARAMETER Ensure + Whether the host record should be present or removed. +#> + +class DnsRecordBase : ResourcePropertiesBase +{ + [DscProperty(Key)] + [System.String] + $ZoneName + + [DscProperty()] + [System.String] + $TimeToLive + + [DscProperty()] + [Ensure] + $Ensure = [Ensure]::Present + + # Hidden property to determine whether the class is a scoped version + hidden [System.Boolean] $isScoped + + # Hidden property for holding localization strings + hidden [System.Collections.Hashtable] $localizedData + + # Hidden method to integrate localized strings from classes up the inheritance stack + hidden [void] SetLocalizedData() + { + # Create a list of the inherited class names + $inheritedClasses = @(,$this.GetType().Name) + $parentClass = $this.GetType().BaseType + while ($parentClass -ne [System.Object]) + { + $inheritedClasses += $parentClass.Name + $parentClass = $parentClass.BaseType + } + + $this.localizedData = @{} + + foreach ($className in $inheritedClasses) + { + # Get localized data for the class + $localizationFile = "$($className).strings.psd1" + + try + { + $tmpData = Get-LocalizedData -DefaultUICulture 'en-US' -FileName $localizationFile -ErrorAction Stop + + # Append only previously unspecified keys in the localization data + foreach ($key in $tmpData.Keys) + { + if (-not $this.localizedData.ContainsKey($key)) + { + $this.localizedData[$key] = $tmpData[$key] + } + } + } + catch + { + if ($_.CategoryInfo.Category.ToString() -eq 'ObjectNotFound') + { + Write-Warning $_.Exception.Message + } + else + { + throw $_ + } + } + } + + Write-Debug ($this.localizedData | ConvertTo-JSON) + } + + # Default constructor sets the $isScoped variable and loads the localization strings + DnsRecordBase() + { + # Determine scope + $this.isScoped = $this.PSObject.Properties.Name -contains 'ZoneScope' + + # Import the localization strings + $this.SetLocalizedData() + } + + #region Generic DSC methods -- DO NOT OVERRIDE + + [DnsRecordBase] Get() + { + Write-Verbose -Message ($this.localizedData.GettingDscResourceObject -f $this.GetType().Name) + + $dscResourceObject = $null + + $record = $this.GetResourceRecord() + + if ($null -eq $record) + { + Write-Verbose -Message $this.localizedData.RecordNotFound + + <# + Create an object of the correct type (i.e.: the subclassed resource type) + and set its values to those specified in the object, but set Ensure to Absent + #> + $dscResourceObject = [System.Activator]::CreateInstance($this.GetType()) + + foreach ($propertyName in $this.PSObject.Properties.Name) + { + $dscResourceObject.$propertyName = $this.$propertyName + } + + $dscResourceObject.Ensure = 'Absent' + } + else + { + Write-Verbose -Message $this.localizedData.RecordFound + + # Build an object reflecting the current state based on the record found + $dscResourceObject = $this.NewDscResourceObjectFromRecord($record) + } + + return $dscResourceObject + } + + [void] Set() + { + # Initialize dns cmdlet Parameters for removing a record + $dnsParameters = @{ + ZoneName = $this.ZoneName + ComputerName = $this.DnsServer + } + + # Accomodate for scoped records as well + if ($this.isScoped) + { + $dnsParameters['ZoneScope'] = ($this.PSObject.Properties | Where-Object -FilterScript { $_.Name -eq 'ZoneScope' }).Value + } + + $existingRecord = $this.GetResourceRecord() + + if ($this.Ensure -eq 'Present') + { + if ($null -ne $existingRecord) + { + $currentState = $this.Get() | ConvertTo-HashTableFromObject + $desiredState = $this | ConvertTo-HashTableFromObject + + # Remove properties that have $null as the value + @($desiredState.Keys) | ForEach-Object -Process { + if ($null -eq $desiredState[$_]) + { + $desiredState.Remove($_) + } + } + + # Returns all enforced properties not in desires state, or $null if all enforced properties are in desired state + $propertiesNotInDesiredState = Compare-DscParameterState -CurrentValues $currentState -DesiredValues $desiredState -Properties $desiredState.Keys -IncludeValue + + if ($null -ne $propertiesNotInDesiredState) + { + Write-Verbose -Message $this.localizedData.ModifyingExistingRecord + + $this.ModifyResourceRecord($existingRecord, $propertiesNotInDesiredState) + } + } + else + { + Write-Verbose -Message ($this.localizedData.AddingNewRecord -f $this.GetType().Name) + + # Adding record + $this.AddResourceRecord() + } + } + elseif ($this.Ensure -eq 'Absent') + { + if ($null -ne $existingRecord) + { + Write-Verbose -Message $this.localizedData.RemovingExistingRecord + + # Removing existing record + $existingRecord | Remove-DnsServerResourceRecord @dnsParameters -Force + } + } + } + + [System.Boolean] Test() + { + $isInDesiredState = $true + + $currentState = $this.Get() | ConvertTo-HashTableFromObject + $desiredState = $this | ConvertTo-HashTableFromObject + + if ($this.Ensure -eq 'Present') + { + if ($currentState.Ensure -eq 'Present') + { + # Remove properties that have $null as the value + @($desiredState.Keys) | ForEach-Object -Process { + if ($null -eq $desiredState[$_]) + { + $desiredState.Remove($_) + } + } + + # Returns all enforced properties not in desires state, or $null if all enforced properties are in desired state + $propertiesNotInDesiredState = Compare-DscParameterState -CurrentValues $currentState -DesiredValues $desiredState -Properties $desiredState.Keys -ExcludeProperties @('Ensure') + + if ($propertiesNotInDesiredState) + { + $isInDesiredState = $false + } + } + else + { + Write-Verbose -Message ($this.localizedData.PropertyIsNotInDesiredState -f 'Ensure', $desiredState['Ensure'], $currentState['Ensure']) + + $isInDesiredState = $false + } + } + + if ($this.Ensure -eq 'Absent') + { + if ($currentState['Ensure'] -eq 'Present') + { + Write-Verbose -Message ($this.localizedData.PropertyIsNotInDesiredState -f 'Ensure', $desiredState['Ensure'], $currentState['Ensure']) + + $isInDesiredState = $false + } + } + + if ($isInDesiredState) + { + Write-Verbose -Message $this.localizedData.ObjectInDesiredState + } + else + { + Write-Verbose -Message $this.localizedData.ObjectNotInDesiredState + } + + return $isInDesiredState + } + + #endregion + + #region Methods to override + + # Using the values supplied to $this, query the DNS server for a resource record and return it + hidden [Microsoft.Management.Infrastructure.CimInstance] GetResourceRecord() + { + throw $this.localizedData.GetResourceRecordNotImplemented + } + + # Add a resource record using the properties of this object. + hidden [void] AddResourceRecord() + { + throw $this.localizedData.AddResourceRecordNotImplemented + } + + <# + Modifies a resource record using the properties of this object. + + The data in each hashtable will contain the following properties: + + - ActualType (System.RuntimeType) + - ExpectedType (System.RuntimeType) + - Property (String) + - ExpectedValue (the property's type) + - ActualValue (the property's type) + - InDesiredState (System.Boolean) + #> + hidden [void] ModifyResourceRecord([Microsoft.Management.Infrastructure.CimInstance] $existingRecord, [System.Collections.Hashtable[]] $propertiesNotInDesiredState) + { + throw $this.localizedData.ModifyResourceRecordNotImplemented + } + + # Given a resource record object, create an instance of this class with the appropriate data + hidden [DnsRecordBase] NewDscResourceObjectFromRecord($record) + { + throw $this.localizedData.NewResourceObjectFromRecordNotImplemented + } + + #endregion +} +#EndRegion './Classes/002.DnsRecordBase.ps1' 293 +#Region './Classes/002.DnsRecordCname.ps1' 0 +<# + .SYNOPSIS + The DnsRecordCname DSC resource manages CNAME DNS records against a specific zone on a Domain Name System (DNS) server. + + .DESCRIPTION + The DnsRecordCname DSC resource manages CNAME DNS records against a specific zone on a Domain Name System (DNS) server. + + .PARAMETER Name + Specifies the name of a DNS server resource record object. (Key Parameter) + + .PARAMETER HostNameAlias + Specifies a a canonical name target for a CNAME record. This must be a fully qualified domain name (FQDN). (Key Parameter) +#> + +[DscResource()] +class DnsRecordCname : DnsRecordBase +{ + [DscProperty(Key)] + [System.String] + $Name + + [DscProperty(Key)] + [System.String] + $HostNameAlias + + [DnsRecordCname] Get() + { + return ([DnsRecordBase] $this).Get() + } + + [void] Set() + { + ([DnsRecordBase] $this).Set() + } + + [System.Boolean] Test() + { + return ([DnsRecordBase] $this).Test() + } + + hidden [Microsoft.Management.Infrastructure.CimInstance] GetResourceRecord() + { + Write-Verbose -Message ($this.localizedData.GettingDnsRecordMessage -f 'CNAME', $this.ZoneName, $this.ZoneScope, $this.DnsServer) + + $dnsParameters = @{ + ZoneName = $this.ZoneName + ComputerName = $this.DnsServer + RRType = 'CNAME' + Name = $this.Name + } + + if ($this.isScoped) + { + $dnsParameters['ZoneScope'] = $this.ZoneScope + } + + $record = Get-DnsServerResourceRecord @dnsParameters -ErrorAction SilentlyContinue | Where-Object -FilterScript { + $_.RecordData.HostNameAlias -eq "$($this.HostnameAlias)." + } + + return $record + } + + hidden [DnsRecordCname] NewDscResourceObjectFromRecord([Microsoft.Management.Infrastructure.CimInstance] $record) + { + $dscResourceObject = [DnsRecordCname] @{ + ZoneName = $this.ZoneName + Name = $this.Name + HostNameAlias = $this.HostNameAlias + TimeToLive = $record.TimeToLive.ToString() + DnsServer = $this.DnsServer + Ensure = 'Present' + } + + return $dscResourceObject + } + + hidden [void] AddResourceRecord() + { + $dnsParameters = @{ + ZoneName = $this.ZoneName + ComputerName = $this.DnsServer + CNAME = $true + Name = $this.Name + HostNameAlias = $this.HostNameAlias + } + + if ($this.isScoped) + { + $dnsParameters['ZoneScope'] = $this.ZoneScope + } + + if ($null -ne $this.TimeToLive) + { + $dnsParameters.Add('TimeToLive', $this.TimeToLive) + } + + Write-Verbose -Message ($this.localizedData.CreatingDnsRecordMessage -f 'CNAME', $this.ZoneName, $this.ZoneScope, $this.DnsServer) + + Add-DnsServerResourceRecord @dnsParameters + } + + hidden [void] ModifyResourceRecord([Microsoft.Management.Infrastructure.CimInstance] $existingRecord, [System.Collections.Hashtable[]] $propertiesNotInDesiredState) + { + $dnsParameters = @{ + ZoneName = $this.ZoneName + ComputerName = $this.DnsServer + } + + if ($this.isScoped) + { + $dnsParameters['ZoneScope'] = $this.ZoneScope + } + + # Copy the existing record and modify values as appropriate + $newRecord = [Microsoft.Management.Infrastructure.CimInstance]::new($existingRecord) + + foreach ($propertyToChange in $propertiesNotInDesiredState) + { + switch ($propertyToChange.Property) + { + # Key parameters will never be affected, so only include Mandatory and Optional values in the switch statement + 'TimeToLive' + { + $newRecord.TimeToLive = [System.TimeSpan] $propertyToChange.ExpectedValue + } + + } + } + + Set-DnsServerResourceRecord @dnsParameters -OldInputObject $existingRecord -NewInputObject $newRecord -Verbose + } +} +#EndRegion './Classes/002.DnsRecordCname.ps1' 134 +#Region './Classes/002.DnsRecordPtr.ps1' 0 +<# + .SYNOPSIS + The DnsRecordPtr DSC resource manages PTR DNS records against a specific zone on a Domain Name System (DNS) server. + + .DESCRIPTION + The DnsRecordPtr DSC resource manages PTR DNS records against a specific zone on a Domain Name System (DNS) server. + + .PARAMETER IpAddress + Specifies the IP address to which the record is associated (Can be either IPv4 or IPv6. (Key Parameter) + + .PARAMETER Name + Specifies the FQDN of the host when you add a PTR resource record. (Key Parameter) + + .NOTES + Reverse lookup zones do not support scopes, so there should be no DnsRecordPtrScoped subclass created. +#> + +[DscResource()] +class DnsRecordPtr : DnsRecordBase +{ + [DscProperty(Key)] + [System.String] + $IpAddress + + [DscProperty(Key)] + [System.String] + $Name + + hidden [System.String] $recordHostName + + [DnsRecordPtr] Get() + { + # Ensure $recordHostName is set + $this.recordHostName = $this.getRecordHostName($this.IpAddress) + + return ([DnsRecordBase] $this).Get() + } + + [void] Set() + { + # Ensure $recordHostName is set + $this.recordHostName = $this.getRecordHostName($this.IpAddress) + + ([DnsRecordBase] $this).Set() + } + + [System.Boolean] Test() + { + # Ensure $recordHostName is set + $this.recordHostName = $this.getRecordHostName($this.IpAddress) + + return ([DnsRecordBase] $this).Test() + } + + hidden [Microsoft.Management.Infrastructure.CimInstance] GetResourceRecord() + { + Write-Verbose -Message ($this.localizedData.GettingDnsRecordMessage -f 'Ptr', $this.ZoneName, $this.ZoneScope, $this.DnsServer) + + $dnsParameters = @{ + ZoneName = $this.ZoneName + ComputerName = $this.DnsServer + RRType = 'PTR' + Name = $this.recordHostName + } + + $record = Get-DnsServerResourceRecord @dnsParameters -ErrorAction SilentlyContinue | Where-Object -FilterScript { + $_.RecordData.PtrDomainName -eq "$($this.Name)." + } + + return $record + } + + hidden [DnsRecordPtr] NewDscResourceObjectFromRecord([Microsoft.Management.Infrastructure.CimInstance] $record) + { + $dscResourceObject = [DnsRecordPtr] @{ + ZoneName = $this.ZoneName + IpAddress = $this.IpAddress + Name = $this.Name + TimeToLive = $record.TimeToLive.ToString() + DnsServer = $this.DnsServer + Ensure = 'Present' + } + + return $dscResourceObject + } + + hidden [void] AddResourceRecord() + { + $dnsParameters = @{ + ZoneName = $this.ZoneName + ComputerName = $this.DnsServer + PTR = $true + Name = $this.recordHostName + PtrDomainName = $this.Name + } + + if ($null -ne $this.TimeToLive) + { + $dnsParameters.Add('TimeToLive', $this.TimeToLive) + } + + Write-Verbose -Message ($this.localizedData.CreatingDnsRecordMessage -f 'PTR', $this.ZoneName, $this.ZoneScope, $this.DnsServer) + + Add-DnsServerResourceRecord @dnsParameters + } + + hidden [void] ModifyResourceRecord([Microsoft.Management.Infrastructure.CimInstance] $existingRecord, [System.Collections.Hashtable[]] $propertiesNotInDesiredState) + { + $dnsParameters = @{ + ZoneName = $this.ZoneName + ComputerName = $this.DnsServer + } + + # Copy the existing record and modify values as appropriate + $newRecord = [Microsoft.Management.Infrastructure.CimInstance]::new($existingRecord) + + foreach ($propertyToChange in $propertiesNotInDesiredState) + { + switch ($propertyToChange.Property) + { + # Key parameters will never be affected, so only include Mandatory and Optional values in the switch statement + 'TimeToLive' + { + $newRecord.TimeToLive = [System.TimeSpan] $propertyToChange.ExpectedValue + } + + } + } + + Set-DnsServerResourceRecord @dnsParameters -OldInputObject $existingRecord -NewInputObject $newRecord -Verbose + } + + # Take a compressed IPv6 string (i.e.: fd00::1) and expand it out to the full notation (i.e.: fd00:0000:0000:0000:0000:0000:0000:0001) + hidden [System.String] expandIPv6String($string) + { + # Split the string on the colons + $segments = [System.Collections.ArrayList]::new(($string -split ':')) + + # Determine how many segments need to be added to reach the 8 required + $blankSegmentCount = 8 - $segments.count + + # Hold the expanded segments + $newSegments = [System.Collections.ArrayList]::new() + + # Insert missing segments + foreach ($segment in $segments) + { + if ([System.String]::IsNullOrEmpty($segment)) + { + for ($i = 0; $i -le $blankSegmentCount; $i++) + { + $newSegments.Add('0000') + } + } else + { + $newSegments.Add($segment) + } + } + + # Pad out all segments with leading zeros + $paddedSegments = $newSegments | ForEach-Object { + $_.PadLeft(4, '0') + } + return ($paddedSegments -join ':') + } + + # Translate the IP address to the reverse notation used by the DNS server + hidden [System.String] getReverseNotation([System.Net.IpAddress] $ipAddressObj) + { + $significantData = [System.Collections.ArrayList]::New() + + switch ($ipAddressObj.AddressFamily) + { + 'InterNetwork' + { + $significantData.AddRange(($ipAddressObj.IPAddressToString -split '\.')) + break + } + + 'InterNetworkV6' + { + # Get the hex values into an ArrayList + $significantData.AddRange(($this.expandIPv6String($ipAddressObj.IPAddressToString) -replace ':', '' -split '')) + break + } + } + + $significantData.Reverse() + + # The reverse lookup notation puts a '.' between each hex value + return ($significantData -join '.').Trim('.') + } + + # Determine the record host name + hidden [System.String] getRecordHostName([System.Net.IpAddress] $ipAddressObj) + { + $reverseLookupAddressComponent = "" + + switch ($ipAddressObj.AddressFamily) + { + 'InterNetwork' + { + if (-not $this.ZoneName.ToLower().EndsWith('.in-addr.arpa')) + { + throw ($this.localizedData.NotAnIPv4Zone -f $this.ZoneName) + } + $reverseLookupAddressComponent = $this.ZoneName.Replace('.in-addr.arpa', '') + break + } + + 'InterNetworkV6' + { + if (-not $this.ZoneName.ToLower().EndsWith('.ip6.arpa')) + { + throw ($this.localizedData.NotAnIPv6Zone -f $this.ZoneName) + } + $reverseLookupAddressComponent = $this.ZoneName.Replace('.ip6.arpa', '') + break + } + } + + $reverseNotation = $this.getReverseNotation($ipAddressObj) + + # Check to make sure that the ip address actually belongs in this zone + if ($reverseNotation -notmatch "$($reverseLookupAddressComponent)`$") + { + throw $this.localizedData.WrongZone -f $ipAddressObj.IPAddressToString, $this.ZoneName + } + + # Strip the zone name from the reversed IP using a regular expression + $ptrRecordHostName = $reverseNotation -replace "\.$([System.Text.RegularExpressions.Regex]::Escape($reverseLookupAddressComponent))`$", "" + + return $ptrRecordHostName + } +} +#EndRegion './Classes/002.DnsRecordPtr.ps1' 236 +#Region './Classes/003.DnsRecordA.ps1' 0 +<# + .SYNOPSIS + The DnsRecordA DSC resource manages A DNS records against a specific zone on a Domain Name System (DNS) server. + + .DESCRIPTION + The DnsRecordA DSC resource manages A DNS records against a specific zone on a Domain Name System (DNS) server. + + .PARAMETER Name + Specifies the name of a DNS server resource record object. (Key Parameter) + + .PARAMETER IPv4Address + Specifies the IPv4 address of a host. (Key Parameter) +#> + +[DscResource()] +class DnsRecordA : DnsRecordBase +{ + [DscProperty(Key)] + [System.String] + $Name + + [DscProperty(Key)] + [System.String] + $IPv4Address + + [DnsRecordA] Get() + { + return ([DnsRecordBase] $this).Get() + } + + [void] Set() + { + ([DnsRecordBase] $this).Set() + } + + [System.Boolean] Test() + { + return ([DnsRecordBase] $this).Test() + } + + hidden [Microsoft.Management.Infrastructure.CimInstance] GetResourceRecord() + { + Write-Verbose -Message ($this.localizedData.GettingDnsRecordMessage -f 'A', $this.ZoneName, $this.ZoneScope, $this.DnsServer) + + $dnsParameters = @{ + ZoneName = $this.ZoneName + ComputerName = $this.DnsServer + RRType = 'A' + Name = $this.Name + } + + if ($this.isScoped) + { + $dnsParameters['ZoneScope'] = $this.ZoneScope + } + + $record = Get-DnsServerResourceRecord @dnsParameters -ErrorAction SilentlyContinue | Where-Object { + $_.RecordData.IPv4Address -eq $this.IPv4Address + } + + return $record + } + + hidden [DnsRecordA] NewDscResourceObjectFromRecord([Microsoft.Management.Infrastructure.CimInstance] $record) + { + $dscResourceObject = [DnsRecordA] @{ + ZoneName = $this.ZoneName + Name = $this.Name + IPv4Address = $this.IPv4Address + TimeToLive = $record.TimeToLive.ToString() + DnsServer = $this.DnsServer + Ensure = 'Present' + } + + return $dscResourceObject + } + + hidden [void] AddResourceRecord() + { + $dnsParameters = @{ + ZoneName = $this.ZoneName + ComputerName = $this.DnsServer + A = $true + Name = $this.Name + IPv4Address = $this.IPv4Address + } + + if ($this.isScoped) + { + $dnsParameters['ZoneScope'] = $this.ZoneScope + } + + if ($null -ne $this.TimeToLive) + { + $dnsParameters.Add('TimeToLive', $this.TimeToLive) + } + + Write-Verbose -Message ($this.localizedData.CreatingDnsRecordMessage -f 'A', $this.ZoneName, $this.ZoneScope, $this.DnsServer) + + Add-DnsServerResourceRecord @dnsParameters + } + + hidden [void] ModifyResourceRecord([Microsoft.Management.Infrastructure.CimInstance] $existingRecord, [System.Collections.Hashtable[]] $propertiesNotInDesiredState) + { + $dnsParameters = @{ + ZoneName = $this.ZoneName + ComputerName = $this.DnsServer + } + + if ($this.isScoped) + { + $dnsParameters['ZoneScope'] = $this.ZoneScope + } + + # Copy the existing record and modify values as appropriate + $newRecord = [Microsoft.Management.Infrastructure.CimInstance]::new($existingRecord) + + foreach ($propertyToChange in $propertiesNotInDesiredState) + { + switch ($propertyToChange.Property) + { + # Key parameters will never be affected, so only include Mandatory and Optional values in the switch statement + 'TimeToLive' + { + $newRecord.TimeToLive = [System.TimeSpan] $propertyToChange.ExpectedValue + } + + } + } + + Set-DnsServerResourceRecord @dnsParameters -OldInputObject $existingRecord -NewInputObject $newRecord -Verbose + } +} +#EndRegion './Classes/003.DnsRecordA.ps1' 134 +#Region './Classes/003.DnsRecordAaaa.ps1' 0 +<# + .SYNOPSIS + The DnsRecordAaaa DSC resource manages AAAA DNS records against a specific zone on a Domain Name System (DNS) server. + + .DESCRIPTION + The DnsRecordAaaa DSC resource manages AAAA DNS records against a specific zone on a Domain Name System (DNS) server. + + .PARAMETER Name + Specifies the name of a DNS server resource record object. (Key Parameter) + + .PARAMETER IPv6Address + Specifies the IPv6 address of a host. (Key Parameter) +#> + +[DscResource()] +class DnsRecordAaaa : DnsRecordBase +{ + [DscProperty(Key)] + [System.String] + $Name + + [DscProperty(Key)] + [System.String] + $IPv6Address + + [DnsRecordAaaa] Get() + { + return ([DnsRecordBase] $this).Get() + } + + [void] Set() + { + ([DnsRecordBase] $this).Set() + } + + [System.Boolean] Test() + { + return ([DnsRecordBase] $this).Test() + } + + hidden [Microsoft.Management.Infrastructure.CimInstance] GetResourceRecord() + { + Write-Verbose -Message ($this.localizedData.GettingDnsRecordMessage -f 'Aaaa', $this.ZoneName, $this.ZoneScope, $this.DnsServer) + + $dnsParameters = @{ + ZoneName = $this.ZoneName + ComputerName = $this.DnsServer + RRType = 'AAAA' + Name = $this.Name + } + + if ($this.isScoped) + { + $dnsParameters['ZoneScope'] = $this.ZoneScope + } + + $record = Get-DnsServerResourceRecord @dnsParameters -ErrorAction SilentlyContinue | Where-Object -FilterScript { + $_.RecordData.IPv6Address -eq $this.IPv6Address + } + + return $record + } + + hidden [DnsRecordAaaa] NewDscResourceObjectFromRecord([Microsoft.Management.Infrastructure.CimInstance] $record) + { + $dscResourceObject = [DnsRecordAaaa] @{ + ZoneName = $this.ZoneName + Name = $this.Name + IPv6Address = $this.IPv6Address + TimeToLive = $record.TimeToLive.ToString() + DnsServer = $this.DnsServer + Ensure = 'Present' + } + + return $dscResourceObject + } + + hidden [void] AddResourceRecord() + { + $dnsParameters = @{ + ZoneName = $this.ZoneName + ComputerName = $this.DnsServer + AAAA = $true + Name = $this.name + IPv6Address = $this.IPv6Address + } + + if ($this.isScoped) + { + $dnsParameters['ZoneScope'] = $this.ZoneScope + } + + if ($null -ne $this.TimeToLive) + { + $dnsParameters.Add('TimeToLive', $this.TimeToLive) + } + + Write-Verbose -Message ($this.localizedData.CreatingDnsRecordMessage -f 'AAAA', $this.ZoneName, $this.ZoneScope, $this.DnsServer) + + Add-DnsServerResourceRecord @dnsParameters + } + + hidden [void] ModifyResourceRecord([Microsoft.Management.Infrastructure.CimInstance] $existingRecord, [System.Collections.Hashtable[]] $propertiesNotInDesiredState) + { + $dnsParameters = @{ + ZoneName = $this.ZoneName + ComputerName = $this.DnsServer + } + + if ($this.isScoped) + { + $dnsParameters['ZoneScope'] = $this.ZoneScope + } + + # Copy the existing record and modify values as appropriate + $newRecord = [Microsoft.Management.Infrastructure.CimInstance]::new($existingRecord) + + foreach ($propertyToChange in $propertiesNotInDesiredState) + { + switch ($propertyToChange.Property) + { + # Key parameters will never be affected, so only include Mandatory and Optional values in the switch statement + 'TimeToLive' + { + $newRecord.TimeToLive = [System.TimeSpan] $propertyToChange.ExpectedValue + } + + } + } + + Set-DnsServerResourceRecord @dnsParameters -OldInputObject $existingRecord -NewInputObject $newRecord -Verbose + } +} +#EndRegion './Classes/003.DnsRecordAaaa.ps1' 134 +#Region './Classes/003.DnsRecordCnameScoped.ps1' 0 +<# + .SYNOPSIS + The DnsRecordCnameScoped DSC resource manages CNAME DNS records against a specific zone and zone scope on a Domain Name System (DNS) server. + + .DESCRIPTION + The DnsRecordCnameScoped DSC resource manages CNAME DNS records against a specific zone and zone scope on a Domain Name System (DNS) server. + + .PARAMETER ZoneScope + Specifies the name of a zone scope. (Key Parameter) +#> + +[DscResource()] +class DnsRecordCnameScoped : DnsRecordCname +{ + [DscProperty(Key)] + [System.String] + $ZoneScope + + [DnsRecordCnameScoped] Get() + { + return ([DnsRecordBase] $this).Get() + } + + [void] Set() + { + ([DnsRecordBase] $this).Set() + } + + [System.Boolean] Test() + { + return ([DnsRecordBase] $this).Test() + } + + hidden [Microsoft.Management.Infrastructure.CimInstance] GetResourceRecord() + { + return ([DnsRecordCname] $this).GetResourceRecord() + } + + hidden [DnsRecordCnameScoped] NewDscResourceObjectFromRecord([Microsoft.Management.Infrastructure.CimInstance] $record) + { + $dscResourceObject = [DnsRecordCnameScoped] @{ + ZoneName = $this.ZoneName + ZoneScope = $this.ZoneScope + Name = $this.Name + HostNameAlias = $this.HostNameAlias + TimeToLive = $record.TimeToLive.ToString() + DnsServer = $this.DnsServer + Ensure = 'Present' + } + + return $dscResourceObject + } + + hidden [void] AddResourceRecord() + { + ([DnsRecordCname] $this).AddResourceRecord() + } + + hidden [void] ModifyResourceRecord([Microsoft.Management.Infrastructure.CimInstance] $existingRecord, [System.Collections.Hashtable[]] $propertiesNotInDesiredState) + { + ([DnsRecordCname] $this).ModifyResourceRecord($existingRecord, $propertiesNotInDesiredState) + } +} +#EndRegion './Classes/003.DnsRecordCnameScoped.ps1' 64 +#Region './Classes/003.DnsRecordMx.ps1' 0 +<# + .SYNOPSIS + The DnsRecordMx DSC resource manages MX DNS records against a specific zone on a Domain Name System (DNS) server. + + .DESCRIPTION + The DnsRecordMx DSC resource manages MX DNS records against a specific zone on a Domain Name System (DNS) server. + + .PARAMETER EmailDomain + Everything after the '@' in the email addresses supported by this mail exchanger. It must be a subdomain the zone or the zone itself. To specify all subdomains, use the '*' character (i.e.: *.contoso.com). (Key Parameter) + + .PARAMETER MailExchange + FQDN of the server handling email for the specified email domain. When setting the value, this FQDN must resolve to an IP address and cannot reference a CNAME record. (Key Parameter) + + .PARAMETER Priority + Specifies the priority for this MX record among other MX records that belong to the same email domain, where a lower value has a higher priority. (Mandatory Parameter) +#> + +[DscResource()] +class DnsRecordMx : DnsRecordBase +{ + [DscProperty(Key)] + [System.String] + $EmailDomain + + [DscProperty(Key)] + [System.String] + $MailExchange + + [DscProperty(Mandatory)] + [System.UInt16] + $Priority + + hidden [System.String] $recordName + + [DnsRecordMx] Get() + { + $this.recordName = $this.getRecordName() + return ([DnsRecordBase] $this).Get() + } + + [void] Set() + { + $this.recordName = $this.getRecordName() + ([DnsRecordBase] $this).Set() + } + + [System.Boolean] Test() + { + $this.recordName = $this.getRecordName() + return ([DnsRecordBase] $this).Test() + } + + [System.String] getRecordName() + { + $aRecordName = $null + $regexMatch = $this.EmailDomain | Select-String -Pattern "^((.*?)\.){0,1}$($this.ZoneName)`$" + if ($null -eq $regexMatch) + { + throw ($this.localizedData.DomainZoneMismatch -f $this.EmailDomain, $this.ZoneName) + } + else + { + # Match group 2 contains the value in which we are interested. + $aRecordName = $regexMatch.Matches.Groups[2].Value + if ($aRecordName -eq '') + { + $aRecordName = '.' + } + } + return $aRecordName + } + + hidden [Microsoft.Management.Infrastructure.CimInstance] GetResourceRecord() + { + Write-Verbose -Message ($this.localizedData.GettingDnsRecordMessage -f 'Mx', $this.ZoneName, $this.ZoneScope, $this.DnsServer) + + $dnsParameters = @{ + ZoneName = $this.ZoneName + ComputerName = $this.DnsServer + RRType = 'MX' + } + + if ($this.isScoped) + { + $dnsParameters['ZoneScope'] = $this.ZoneScope + } + + $record = Get-DnsServerResourceRecord @dnsParameters -ErrorAction SilentlyContinue | Where-Object -FilterScript { + $translatedRecordName = $this.getRecordName() + if ($translatedRecordName -eq '.') + { + $translatedRecordName = '@' + } + $_.HostName -eq $translatedRecordName -and + $_.RecordData.MailExchange -eq "$($this.MailExchange)." + } + + <# + It is technically possible, outside of this resource to have more than one record with the same target, but + different priorities. So, although the idea of doing so is nonsensical, we have to ensure we are selecting + only one record in this method. It doesn't matter which one. + #> + return $record | Select-Object -First 1 + } + + hidden [DnsRecordMx] NewDscResourceObjectFromRecord([Microsoft.Management.Infrastructure.CimInstance] $record) + { + $dscResourceObject = [DnsRecordMx] @{ + ZoneName = $this.ZoneName + EmailDomain = $this.EmailDomain + MailExchange = $this.MailExchange + Priority = $record.RecordData.Preference + TimeToLive = $record.TimeToLive.ToString() + DnsServer = $this.DnsServer + Ensure = 'Present' + } + + return $dscResourceObject + } + + hidden [void] AddResourceRecord() + { + $dnsParameters = @{ + ZoneName = $this.ZoneName + ComputerName = $this.DnsServer + MX = $true + Name = $this.getRecordName() + MailExchange = $this.MailExchange + Preference = $this.Priority + } + + if ($this.isScoped) + { + $dnsParameters['ZoneScope'] = $this.ZoneScope + } + + if ($null -ne $this.TimeToLive) + { + $dnsParameters.Add('TimeToLive', $this.TimeToLive) + } + + Write-Verbose -Message ($this.localizedData.CreatingDnsRecordMessage -f 'MX', $this.ZoneName, $this.ZoneScope, $this.DnsServer) + + Add-DnsServerResourceRecord @dnsParameters + } + + hidden [void] ModifyResourceRecord([Microsoft.Management.Infrastructure.CimInstance] $existingRecord, [System.Collections.Hashtable[]] $propertiesNotInDesiredState) + { + $dnsParameters = @{ + ZoneName = $this.ZoneName + ComputerName = $this.DnsServer + } + + if ($this.isScoped) + { + $dnsParameters['ZoneScope'] = $this.ZoneScope + } + + # Copy the existing record and modify values as appropriate + $newRecord = [Microsoft.Management.Infrastructure.CimInstance]::new($existingRecord) + + foreach ($propertyToChange in $propertiesNotInDesiredState) + { + switch ($propertyToChange.Property) + { + # Key parameters will never be affected, so only include Mandatory and Optional values in the switch statement + + 'Priority' + { + $newRecord.RecordData.Preference = $propertyToChange.ExpectedValue + } + + 'TimeToLive' + { + $newRecord.TimeToLive = [System.TimeSpan] $propertyToChange.ExpectedValue + } + + } + } + + Set-DnsServerResourceRecord @dnsParameters -OldInputObject $existingRecord -NewInputObject $newRecord -Verbose + } +} +#EndRegion './Classes/003.DnsRecordMx.ps1' 184 +#Region './Classes/003.DnsRecordNs.ps1' 0 +<# + .SYNOPSIS + The DnsRecordNs DSC resource manages NS DNS records against a specific zone on a Domain Name System (DNS) server. + + .DESCRIPTION + The DnsRecordNs DSC resource manages NS DNS records against a specific zone on a Domain Name System (DNS) server. + + .PARAMETER DomainName + Specifies the fully qualified DNS domain name for which the NameServer is authoritative. It must be a subdomain the zone or the zone itself. To specify all subdomains, use the '*' character (i.e.: *.contoso.com). (Key Parameter) + + .PARAMETER NameServer + Specifies the name server of a domain. This should be a fully qualified domain name, not an IP address (Key Parameter) +#> + +[DscResource()] +class DnsRecordNs : DnsRecordBase +{ + [DscProperty(Key)] + [System.String] + $DomainName + + [DscProperty(Key)] + [System.String] + $NameServer + + [DnsRecordNs] Get() + { + return ([DnsRecordBase] $this).Get() + } + + [void] Set() + { + ([DnsRecordBase] $this).Set() + } + + [System.Boolean] Test() + { + return ([DnsRecordBase] $this).Test() + } + + [System.String] getRecordName() + { + $aRecordName = $null + + # Use regex matching to determine if the domain name provided is a subdomain of the ZoneName (ends in ZoneName). + $regexMatch = $this.DomainName | Select-String -Pattern "^((.*?)\.){0,1}$($this.ZoneName)`$" + + if ($null -eq $regexMatch) + { + throw ($this.localizedData.DomainZoneMismatch -f $this.DomainName, $this.ZoneName) + } + else + { + # Match group 2 contains the value in which we are interested. + $aRecordName = $regexMatch.Matches.Groups[2].Value + if ($aRecordName -eq '') + { + $aRecordName = '.' + } + } + return $aRecordName + } + + hidden [Microsoft.Management.Infrastructure.CimInstance] GetResourceRecord() + { + Write-Verbose -Message ($this.localizedData.GettingDnsRecordMessage -f 'Ns', $this.ZoneName, $this.ZoneScope, $this.DnsServer) + + $dnsParameters = @{ + ZoneName = $this.ZoneName + ComputerName = $this.DnsServer + RRType = 'NS' + } + + if ($this.isScoped) + { + $dnsParameters['ZoneScope'] = $this.ZoneScope + } + + $record = Get-DnsServerResourceRecord @dnsParameters -ErrorAction SilentlyContinue | Where-Object -FilterScript { + $translatedRecordName = $this.getRecordName() + if ($translatedRecordName -eq '.') + { + $translatedRecordName = '@' + } + $_.HostName -eq $translatedRecordName -and + $_.RecordData.NameServer -eq "$($this.NameServer)." + } + + return $record + } + + hidden [DnsRecordNs] NewDscResourceObjectFromRecord([Microsoft.Management.Infrastructure.CimInstance] $record) + { + $dscResourceObject = [DnsRecordNs] @{ + ZoneName = $this.ZoneName + DomainName = $this.DomainName + NameServer = $this.NameServer + TimeToLive = $record.TimeToLive.ToString() + DnsServer = $this.DnsServer + Ensure = 'Present' + } + + return $dscResourceObject + } + + hidden [void] AddResourceRecord() + { + $dnsParameters = @{ + ZoneName = $this.ZoneName + ComputerName = $this.DnsServer + NS = $true + Name = $this.getRecordName() + NameServer = $this.NameServer + } + + if ($this.isScoped) + { + $dnsParameters['ZoneScope'] = $this.ZoneScope + } + + if ($null -ne $this.TimeToLive) + { + $dnsParameters.Add('TimeToLive', $this.TimeToLive) + } + + Write-Verbose -Message ($this.localizedData.CreatingDnsRecordMessage -f 'NS', $this.ZoneName, $this.ZoneScope, $this.DnsServer) + + Add-DnsServerResourceRecord @dnsParameters + } + + hidden [void] ModifyResourceRecord([Microsoft.Management.Infrastructure.CimInstance] $existingRecord, [System.Collections.Hashtable[]] $propertiesNotInDesiredState) + { + $dnsParameters = @{ + ZoneName = $this.ZoneName + ComputerName = $this.DnsServer + } + + if ($this.isScoped) + { + $dnsParameters['ZoneScope'] = $this.ZoneScope + } + + # Copy the existing record and modify values as appropriate + $newRecord = [Microsoft.Management.Infrastructure.CimInstance]::new($existingRecord) + + foreach ($propertyToChange in $propertiesNotInDesiredState) + { + switch ($propertyToChange.Property) + { + # Key parameters will never be affected, so only include Mandatory and Optional values in the switch statement + + 'TimeToLive' + { + $newRecord.TimeToLive = [System.TimeSpan] $propertyToChange.ExpectedValue + } + + } + } + + Set-DnsServerResourceRecord @dnsParameters -OldInputObject $existingRecord -NewInputObject $newRecord -Verbose + } +} +#EndRegion './Classes/003.DnsRecordNs.ps1' 163 +#Region './Classes/003.DnsRecordSrv.ps1' 0 +<# + .SYNOPSIS + The DnsRecordSrv DSC resource manages SRV DNS records against a specific zone on a Domain Name System (DNS) server. + + .DESCRIPTION + The DnsRecordSrv DSC resource manages SRV DNS records against a specific zone on a Domain Name System (DNS) server. + + .PARAMETER SymbolicName + Service name for the SRV record. eg: xmpp, ldap, etc. (Key Parameter) + + .PARAMETER Protocol + Service transmission protocol ('TCP' or 'UDP') (Key Parameter) + + .PARAMETER Port + The TCP or UDP port on which the service is found (Key Parameter) + + .PARAMETER Target + Specifies the Target Hostname or IP Address. (Key Parameter) + + .PARAMETER Priority + Specifies the Priority value of the SRV record. (Mandatory Parameter) + + .PARAMETER Weight + Specifies the weight of the SRV record. (Mandatory Parameter) +#> + +[DscResource()] +class DnsRecordSrv : DnsRecordBase +{ + [DscProperty(Key)] + [System.String] + $SymbolicName + + [DscProperty(Key)] + [ValidateSet('TCP', 'UDP')] + [System.String] + $Protocol + + [DscProperty(Key)] + [ValidateRange(1, 65535)] + [System.UInt16] + $Port + + [DscProperty(Key)] + [System.String] + $Target + + [DscProperty(Mandatory)] + [System.UInt16] + $Priority + + [DscProperty(Mandatory)] + [System.UInt16] + $Weight + + hidden [System.String] getRecordHostName() + { + return $this.getRecordHostName($this.SymbolicName, $this.Protocol) + } + + hidden [System.String] getRecordHostName($aSymbolicName, $aProtocol) + { + return "_$($aSymbolicName)._$($aProtocol)".ToLower() + } + + [DnsRecordSrv] Get() + { + return ([DnsRecordBase] $this).Get() + } + + [void] Set() + { + ([DnsRecordBase] $this).Set() + } + + [System.Boolean] Test() + { + return ([DnsRecordBase] $this).Test() + } + + hidden [Microsoft.Management.Infrastructure.CimInstance] GetResourceRecord() + { + $recordHostName = $this.getRecordHostName() + + Write-Verbose -Message ($this.localizedData.GettingDnsRecordMessage -f $recordHostName, $this.target, 'SRV', $this.ZoneName, $this.ZoneScope, $this.DnsServer) + + $dnsParameters = @{ + Name = $recordHostName + ZoneName = $this.ZoneName + ComputerName = $this.DnsServer + RRType = 'SRV' + } + + if ($this.isScoped) + { + $dnsParameters['ZoneScope'] = $this.ZoneScope + } + + $record = Get-DnsServerResourceRecord @dnsParameters -ErrorAction SilentlyContinue | Where-Object -FilterScript { + $_.HostName -eq $recordHostName -and + $_.RecordData.Port -eq $this.Port -and + $_.RecordData.DomainName -eq "$($this.Target)." + } + + return $record + } + + hidden [DnsRecordSrv] NewDscResourceObjectFromRecord([Microsoft.Management.Infrastructure.CimInstance] $record) + { + $dscResourceObject = [DnsRecordSrv] @{ + ZoneName = $this.ZoneName + SymbolicName = $this.SymbolicName + Protocol = $this.Protocol.ToLower() + Port = $this.Port + Target = ($record.RecordData.DomainName).TrimEnd('.') + Priority = $record.RecordData.Priority + Weight = $record.RecordData.Weight + TimeToLive = $record.TimeToLive.ToString() + DnsServer = $this.DnsServer + Ensure = 'Present' + } + + return $dscResourceObject + } + + hidden [void] AddResourceRecord() + { + $recordHostName = $this.getRecordHostName() + + $dnsParameters = @{ + ZoneName = $this.ZoneName + ComputerName = $this.DnsServer + Name = $recordHostName + Srv = $true + DomainName = $this.Target + Port = $this.Port + Priority = $this.Priority + Weight = $this.Weight + } + + if ($this.isScoped) + { + $dnsParameters['ZoneScope'] = $this.ZoneScope + } + + if ($null -ne $this.TimeToLive) + { + $dnsParameters.Add('TimeToLive', $this.TimeToLive) + } + + Write-Verbose -Message ($this.localizedData.CreatingDnsRecordMessage -f 'SRV', $recordHostName, $this.Target, $this.ZoneName, $this.ZoneScope, $this.DnsServer) + + Add-DnsServerResourceRecord @dnsParameters + } + + hidden [void] ModifyResourceRecord([Microsoft.Management.Infrastructure.CimInstance] $existingRecord, [System.Collections.Hashtable[]] $propertiesNotInDesiredState) + { + $recordHostName = $this.getRecordHostName() + + $dnsParameters = @{ + ZoneName = $this.ZoneName + ComputerName = $this.DnsServer + } + + if ($this.isScoped) + { + $dnsParameters['ZoneScope'] = $this.ZoneScope + } + + # Copy the existing record and modify values as appropriate + $newRecord = [Microsoft.Management.Infrastructure.CimInstance]::new($existingRecord) + + foreach ($propertyToChange in $propertiesNotInDesiredState) + { + switch ($propertyToChange.Property) + { + # Key parameters will never be affected, so only include Mandatory and Optional values in the switch statement + 'Priority' + { + $newRecord.RecordData.Priority = $propertyToChange.ExpectedValue + } + + 'Weight' + { + $newRecord.RecordData.Weight = $propertyToChange.ExpectedValue + } + + 'TimeToLive' + { + $newRecord.TimeToLive = [System.TimeSpan] $propertyToChange.ExpectedValue + } + + } + } + + Set-DnsServerResourceRecord @dnsParameters -OldInputObject $existingRecord -NewInputObject $newRecord -Verbose + } +} +#EndRegion './Classes/003.DnsRecordSrv.ps1' 199 +#Region './Classes/003.DnsServerCache.ps1' 0 +<# + .SYNOPSIS + The DnsServerCache DSC resource manages cache settings on a Microsoft Domain + Name System (DNS) server. + + .DESCRIPTION + The DnsServerCache DSC resource manages cache settings on a Microsoft Domain + Name System (DNS) server. + + .PARAMETER DnsServer + The host name of the Domain Name System (DNS) server, or use `'localhost'` + for the current node. + + .PARAMETER IgnorePolicies + Specifies whether to ignore policies for this cache. + + .PARAMETER LockingPercent + Specifies a percentage of the original Time to Live (TTL) value that caching + can consume. Cache locking is configured as a percent value. For example, if + the cache locking value is set to `50`, the DNS server does not overwrite a + cached entry for half of the duration of the TTL. If the cache locking percent + is set to `100` that means the DNS server will not overwrite cached entries + for the entire duration of the TTL. + + .PARAMETER MaxKBSize + Specifies the maximum size, in kilobytes, of the memory cache of a DNS server. + If set to `0` there is no limit. + + .PARAMETER MaxNegativeTtl + Specifies how long an entry that records a negative answer to a query remains + stored in the DNS cache. Minimum value is `'00:00:01'` and maximum value is + `'30.00:00:00'` + + .PARAMETER MaxTtl + Specifies how long a record is saved in cache. Minimum value is `'00:00:00'` + and maximum value is `'30.00:00:00'`. If the TimeSpan is set to `'00:00:00'` + (0 seconds), the DNS server does not cache records. + + .PARAMETER EnablePollutionProtection + Specifies whether DNS filters name service (NS) resource records that are + cached. Valid values are False (`$false`), which caches all responses to name + queries; and True (`$true`), which caches only the records that belong to the + same DNS subtree. + + When you set this parameter value to False (`$false`), cache pollution + protection is disabled. A DNS server caches the Host (A) record and all queried + NS resources that are in the DNS server zone. In this case, DNS can also cache + the NS record of an unauthorized DNS server. This event causes name resolution + to fail or to be appropriated for subsequent queries in the specified domain. + + When you set the value for this parameter to True (`$true`), the DNS server + enables cache pollution protection and ignores the Host (A) record. The DNS + server performs a cache update query to resolve the address of the NS if the + NS is outside the zone of the DNS server. The additional query minimally + affects DNS server performance. + + .PARAMETER StoreEmptyAuthenticationResponse + Specifies whether a DNS server stores empty authoritative responses in the + cache (RFC-2308). +#> + +[DscResource()] +class DnsServerCache : ResourceBase +{ + [DscProperty(Key)] + [System.String] + $DnsServer + + [DscProperty()] + [Nullable[System.Boolean]] + $IgnorePolicies + + [DscProperty()] + [Nullable[System.UInt32]] + $LockingPercent + + [DscProperty()] + [Nullable[System.UInt32]] + $MaxKBSize + + [DscProperty()] + [System.String] + $MaxNegativeTtl + + [DscProperty()] + [System.String] + $MaxTtl + + [DscProperty()] + [Nullable[System.Boolean]] + $EnablePollutionProtection + + [DscProperty()] + [Nullable[System.Boolean]] + $StoreEmptyAuthenticationResponse + + [DnsServerCache] Get() + { + # Call the base method to return the properties. + return ([ResourceBase] $this).Get() + } + + # Base method Get() call this method to get the current state as a CimInstance. + [Microsoft.Management.Infrastructure.CimInstance] GetCurrentState([System.Collections.Hashtable] $properties) + { + return (Get-DnsServerCache @properties) + } + + [void] Set() + { + # Call the base method to enforce the properties. + ([ResourceBase] $this).Set() + } + + <# + Base method Set() call this method with the properties that should be + enforced and that are not in desired state. + #> + [void] Modify([System.Collections.Hashtable] $properties) + { + <# + If the property 'EnablePollutionProtection' was present and not in desired state, + then the property name must be change for the cmdlet Set-DnsServerCache. In the + cmdlet Get-DnsServerCache the property name is 'EnablePollutionProtection', but + in the cmdlet Set-DnsServerCache the parameter is 'PollutionProtection'. + #> + if ($properties.ContainsKey('EnablePollutionProtection')) + { + $properties['PollutionProtection'] = $properties.EnablePollutionProtection + + $properties.Remove('EnablePollutionProtection') + } + + Set-DnsServerCache @properties + } + + [System.Boolean] Test() + { + # Call the base method to test all of the properties that should be enforced. + return ([ResourceBase] $this).Test() + } + + hidden [void] AssertProperties() + { + if ($null -ne $this.MaxNegativeTtl) + { + Assert-TimeSpan -PropertyName 'MaxNegativeTtl' -Value $this.MaxNegativeTtl -Minimum '0.00:00:01' -Maximum '30.00:00:00' + } + + if ($null -ne $this.MaxTtl) + { + Assert-TimeSpan -PropertyName 'MaxTtl' -Value $this.MaxTtl -Minimum '0.00:00:00' -Maximum '30.00:00:00' + } + } +} +#EndRegion './Classes/003.DnsServerCache.ps1' 156 +#Region './Classes/003.DnsServerDsSetting.ps1' 0 +<# + .SYNOPSIS + The DnsServerDsSetting DSC resource manages DNS Active Directory settings + on a Microsoft Domain Name System (DNS) server. + + .DESCRIPTION + The DnsServerDsSetting DSC resource manages DNS Active Directory settings + on a Microsoft Domain Name System (DNS) server. + + .PARAMETER DnsServer + The host name of the Domain Name System (DNS) server, or use `'localhost'` + for the current node. + + .PARAMETER DirectoryPartitionAutoEnlistInterval + Specifies the interval, during which a DNS server tries to enlist itself + in a DNS domain partition and DNS forest partition, if it is not already + enlisted. We recommend that you limit this value to the range one hour to + 180 days, inclusive, but you can use any value. We recommend that you set + the default value to one day. You must set the value 0 (zero) as a flag + value for the default value. However, you can allow zero and treat it + literally. + + .PARAMETER LazyUpdateInterval + Specifies a value, in seconds, to determine how frequently the DNS server + submits updates to the directory server without specifying the + LDAP_SERVER_LAZY_COMMIT_OID control ([MS-ADTS] section 3.1.1.3.4.1.7) at + the same time that it processes DNS dynamic update requests. We recommend + that you limit this value to the range 0x00000000 to 0x0000003c. You must + set the default value to 0x00000003. You must set the value zero to + indicate that the DNS server does not specify the + LDAP_SERVER_LAZY_COMMIT_OID control at the same time that it processes + DNS dynamic update requests. For more information about + LDAP_SERVER_LAZY_COMMIT_OID, see LDAP_SERVER_LAZY_COMMIT_OID control + code. The LDAP_SERVER_LAZY_COMMIT_OID control instructs the DNS server + to return the results of a directory service modification command after + it is completed in memory but before it is committed to disk. In this + way, the server can return results quickly and save data to disk without + sacrificing performance. The DNS server must send this control only to + the directory server that is attached to an LDAP update that the DNS + server initiates in response to a DNS dynamic update request. If the + value is nonzero, LDAP updates that occur during the processing of DNS + dynamic update requests must not specify the LDAP_SERVER_LAZY_COMMIT_OID + control if a period of less than DsLazyUpdateInterval seconds has passed + since the last LDAP update that specifies this control. If a period that + is greater than DsLazyUpdateInterval seconds passes, during which time + the DNS server does not perform an LDAP update that specifies this + control, the DNS server must specify this control on the next update. + + .PARAMETER MinimumBackgroundLoadThreads + Specifies the minimum number of background threads that the DNS server + uses to load zone data from the directory service. You must limit this + value to the range 0x00000000 to 0x00000005, inclusive. You must set the + default value to 0x00000001, and you must treat the value zero as a flag + value for the default value. + + .PARAMETER PollingInterval + Specifies how frequently the DNS server polls Active Directory Domain + Services (AD DS) for changes in Active Directory-integrated zones. You + must limit the value to the range 30 seconds to 3,600 seconds, inclusive. + + .PARAMETER RemoteReplicationDelay + Specifies the minimum interval, in seconds, that the DNS server waits + between the time that it determines that a single object has changed on + a remote directory server, to the time that it tries to replicate a + single object change. You must limit the value to the range 0x00000005 + to 0x00000E10, inclusive. You must set the default value to 0x0000001E, + and you must treat the value zero as a flag value for the default value. + + .PARAMETER TombstoneInterval + Specifies the amount of time that DNS keeps tombstoned records alive in + Active Directory. We recommend that you limit this value to the range + three days to eight weeks, inclusive, but you can set it to any value in + the range 82 hours to 8 weeks. We recommend that you set the default + value to 14 days and treat the value zero as a flag value for the + default. However, you can allow the value zero and treat it literally. + At 2:00 A.M. local time every day, the DNS server must search all + directory service zones for nodes that have the Active Directory + dnsTombstoned attribute set to True, and for a directory service + EntombedTime (section 2.2.2.2.3.23 of MS-DNSP) value that is greater + than previous directory service DSTombstoneInterval seconds. You must + permanently delete all such nodes from the directory server. +#> + +[DscResource()] +class DnsServerDsSetting : ResourceBase +{ + [DscProperty(Key)] + [System.String] + $DnsServer + + [DscProperty()] + [System.String] + $DirectoryPartitionAutoEnlistInterval + + [DscProperty()] + [Nullable[System.UInt32]] + $LazyUpdateInterval + + [DscProperty()] + [Nullable[System.UInt32]] + $MinimumBackgroundLoadThreads + + [DscProperty()] + [System.String] + $PollingInterval + + [DscProperty()] + [Nullable[System.UInt32]] + $RemoteReplicationDelay + + [DscProperty()] + [System.String] + $TombstoneInterval + + [DnsServerDsSetting] Get() + { + # Call the base method to return the properties. + return ([ResourceBase] $this).Get() + } + + # Base method Get() call this method to get the current state as a CimInstance. + [Microsoft.Management.Infrastructure.CimInstance] GetCurrentState([System.Collections.Hashtable] $properties) + { + return (Get-DnsServerDsSetting @properties) + } + + [void] Set() + { + # Call the base method to enforce the properties. + ([ResourceBase] $this).Set() + } + + <# + Base method Set() call this method with the properties that should be + enforced and that are not in desired state. + #> + [void] Modify([System.Collections.Hashtable] $properties) + { + Set-DnsServerDsSetting @properties + } + + [System.Boolean] Test() + { + # Call the base method to test all of the properties that should be enforced. + return ([ResourceBase] $this).Test() + } + + hidden [void] AssertProperties() + { + @( + 'DirectoryPartitionAutoEnlistInterval', + 'TombstoneInterval' + ) | ForEach-Object -Process { + $valueToConvert = $this.$_ + + # Only evaluate properties that have a value. + if ($null -ne $valueToConvert) + { + Assert-TimeSpan -PropertyName $_ -Value $valueToConvert -Minimum '0.00:00:00' + } + } + } +} +#EndRegion './Classes/003.DnsServerDsSetting.ps1' 164 +#Region './Classes/003.DnsServerEDns.ps1' 0 +<# + .SYNOPSIS + The DnsServerEDns DSC resource manages _extension mechanisms for DNS (EDNS)_ + on a Microsoft Domain Name System (DNS) server. + + .DESCRIPTION + The DnsServerEDns DSC resource manages _extension mechanisms for DNS (EDNS)_ + on a Microsoft Domain Name System (DNS) server. + + .PARAMETER DnsServer + The host name of the Domain Name System (DNS) server, or use `'localhost'` + for the current node. + + .PARAMETER CacheTimeout + Specifies the number of seconds that the DNS server caches EDNS information. + + .PARAMETER EnableProbes + Specifies whether to enable the server to probe other servers to determine + whether they support EDNS. + + .PARAMETER EnableReception + Specifies whether the DNS server accepts queries that contain an EDNS record. +#> + +[DscResource()] +class DnsServerEDns : ResourceBase +{ + [DscProperty(Key)] + [System.String] + $DnsServer + + [DscProperty()] + [System.String] + $CacheTimeout + + [DscProperty()] + [Nullable[System.Boolean]] + $EnableProbes + + [DscProperty()] + [Nullable[System.Boolean]] + $EnableReception + + [DnsServerEDns] Get() + { + # Call the base method to return the properties. + return ([ResourceBase] $this).Get() + } + + # Base method Get() call this method to get the current state as a CimInstance. + [Microsoft.Management.Infrastructure.CimInstance] GetCurrentState([System.Collections.Hashtable] $properties) + { + return (Get-DnsServerEDns @properties) + } + + [void] Set() + { + # Call the base method to enforce the properties. + ([ResourceBase] $this).Set() + } + + <# + Base method Set() call this method with the properties that should be + enforced and that are not in desired state. + #> + [void] Modify([System.Collections.Hashtable] $properties) + { + Set-DnsServerEDns @properties + } + + [System.Boolean] Test() + { + # Call the base method to test all of the properties that should be enforced. + return ([ResourceBase] $this).Test() + } + + hidden [void] AssertProperties() + { + @( + 'CacheTimeout' + ) | ForEach-Object -Process { + $valueToConvert = $this.$_ + + # Only evaluate properties that have a value. + if ($null -ne $valueToConvert) + { + Assert-TimeSpan -PropertyName $_ -Value $valueToConvert -Minimum '0.00:00:00' + } + } + } +} +#EndRegion './Classes/003.DnsServerEDns.ps1' 92 +#Region './Classes/003.DnsServerRecursion.ps1' 0 +<# + .SYNOPSIS + The DnsServerRecursion DSC resource manages recursion settings on a Microsoft + Domain Name System (DNS) server. + + .DESCRIPTION + The DnsServerRecursion DSC resource manages recursion settings on a Microsoft + Domain Name System (DNS) server. Recursion occurs when a DNS server queries + other DNS servers on behalf of a requesting client, and then sends the answer + back to the client. + + The property `SecureResponse` that can be set by the cmdlet `Set-DnsServerRecursion` + changes the same value as `EnablePollutionProtection` in the resource _DnsServerCache_ + does. Use the property `EnablePollutionProtection` in the resource _DnsServerCache_ + to enforce pollution protection. + + .PARAMETER DnsServer + The host name of the Domain Name System (DNS) server, or use `'localhost'` + for the current node. + + .PARAMETER Enable + Specifies whether the server enables recursion. + + .PARAMETER AdditionalTimeout + Specifies the time interval, in seconds, that a DNS server waits as it uses + recursion to get resource records from a remote DNS server. Valid values are + in the range of `1` second to `15` seconds. See recommendation in the documentation + of [Set-DnsServerRecursion](https://docs.microsoft.com/en-us/powershell/module/dnsserver/set-dnsserverrecursion). + + .PARAMETER RetryInterval + Specifies elapsed seconds before a DNS server retries a recursive lookup. + Valid values are in the range of `1` second to `15` seconds. The + recommendation is that in general this value should not be change. However, + under a few circumstances it can be considered changing the value. For + example, if a DNS server contacts a remote DNS server over a slow link and + retries the lookup before it gets a response, it could help to raise the + retry interval to be slightly longer than the observed response time. + See recommendation in the documentation of [Set-DnsServerRecursion](https://docs.microsoft.com/en-us/powershell/module/dnsserver/set-dnsserverrecursion). + + .PARAMETER Timeout + Specifies the number of seconds that a DNS server waits before it stops + trying to contact a remote server. The valid value is in the range of `1` + second to `15` seconds. Recommendation is to increase this value when + recursion occurs over a slow link. See recommendation in the documentation + of [Set-DnsServerRecursion](https://docs.microsoft.com/en-us/powershell/module/dnsserver/set-dnsserverrecursion). + + .NOTES + The cmdlet Set-DsnServerRecursion allows to set the value 0 (zero) for the + properties AdditionalTimeout, RetryInterval, and Timeout, but setting the + value 0 reverts the property to its respectively default value. The default + value for the properties on Windows Server 2016 is 4 seconds for property + AdditionalTimeout, 3 seconds for RetryInterval, and 8 seconds for property + Timeout. If it was allowed to set 0 (zero) as the value in this resource + for these properties then the state would never become in desired state. +#> + +[DscResource()] +class DnsServerRecursion : ResourceBase +{ + [DscProperty(Key)] + [System.String] + $DnsServer + + [DscProperty()] + [Nullable[System.Boolean]] + $Enable + + [DscProperty()] + [Nullable[System.UInt32]] + $AdditionalTimeout + + [DscProperty()] + [Nullable[System.UInt32]] + $RetryInterval + + [DscProperty()] + [Nullable[System.UInt32]] + $Timeout + + [DnsServerRecursion] Get() + { + # Call the base method to return the properties. + return ([ResourceBase] $this).Get() + } + + # Base method Get() call this method to get the current state as a CimInstance. + [Microsoft.Management.Infrastructure.CimInstance] GetCurrentState([System.Collections.Hashtable] $properties) + { + return (Get-DnsServerRecursion @properties) + } + + [void] Set() + { + # Call the base method to enforce the properties. + ([ResourceBase] $this).Set() + } + + <# + Base method Set() call this method with the properties that should be + enforced and that are not in desired state. + #> + [void] Modify([System.Collections.Hashtable] $properties) + { + Set-DnsServerRecursion @properties + } + + [System.Boolean] Test() + { + # Call the base method to test all of the properties that should be enforced. + return ([ResourceBase] $this).Test() + } + + # Called by the base method Set() and Test() to assert that all properties are valid. + hidden [void] AssertProperties() + { + @( + 'AdditionalTimeout' + 'RetryInterval' + 'Timeout' + ) | ForEach-Object -Process { + $propertyValue = $this.$_ + + # Only evaluate properties that have a value. + if ($null -ne $propertyValue -and $propertyValue -notin (1..15)) + { + $errorMessage = $this.localizedData.PropertyIsNotInValidRange -f $_, $propertyValue + + New-InvalidOperationException -Message $errorMessage + } + } + } +} +#EndRegion './Classes/003.DnsServerRecursion.ps1' 133 +#Region './Classes/003.DnsServerScavenging.ps1' 0 +<# + .SYNOPSIS + The DnsServerScavenging DSC resource manages scavenging on a Microsoft + Domain Name System (DNS) server. + + .DESCRIPTION + The DnsServerScavenging DSC resource manages scavenging on a Microsoft + Domain Name System (DNS) server. + + .PARAMETER DnsServer + The host name of the Domain Name System (DNS) server, or use 'localhost' + for the current node. + + .PARAMETER ScavengingState + Specifies whether to Enable automatic scavenging of stale records. + `ScavengingState` determines whether the DNS scavenging feature is enabled + by default on newly created zones. + + .PARAMETER ScavengingInterval + Specifies a length of time as a value that can be converted to a `[TimeSpan]` + object. `ScavengingInterval` determines whether the scavenging feature for + the DNS server is enabled and sets the number of hours between scavenging + cycles. The value `0` disables scavenging for the DNS server. A setting + greater than `0` enables scavenging for the server and sets the number of + days, hours, minutes, and seconds (formatted as dd.hh:mm:ss) between + scavenging cycles. The minimum value is 0. The maximum value is 365.00:00:00 + (1 year). + + .PARAMETER RefreshInterval + Specifies the refresh interval as a value that can be converted to a `[TimeSpan]` + object (formatted as dd.hh:mm:ss). During this interval, a DNS server can + refresh a resource record that has a non-zero time stamp. Zones on the server + inherit this value automatically. If a DNS server does not refresh a resource + record that has a non-zero time stamp, the DNS server can remove that record + during the next scavenging. Do not select a value smaller than the longest + refresh period of a resource record registered in the zone. The minimum value + is `0`. The maximum value is 365.00:00:00 (1 year). + + .PARAMETER NoRefreshInterval + Specifies a length of time as a value that can be converted to a `[TimeSpan]` + object (formatted as dd.hh:mm:ss). `NoRefreshInterval` sets a period of time + in which no refreshes are accepted for dynamically updated records. Zones on + the server inherit this value automatically. This value is the interval between + the last update of a timestamp for a record and the earliest time when the + timestamp can be refreshed. The minimum value is 0. The maximum value is + 365.00:00:00 (1 year). + + .PARAMETER LastScavengeTime + The time when the last scavenging cycle was executed. +#> + +[DscResource()] +class DnsServerScavenging : ResourceBase +{ + [DscProperty(Key)] + [System.String] + $DnsServer + + [DscProperty()] + [Nullable[System.Boolean]] + $ScavengingState + + [DscProperty()] + [System.String] + $ScavengingInterval + + [DscProperty()] + [System.String] + $RefreshInterval + + [DscProperty()] + [System.String] + $NoRefreshInterval + + [DscProperty(NotConfigurable)] + [Nullable[System.DateTime]] + $LastScavengeTime + + [DnsServerScavenging] Get() + { + # Call the base method to return the properties. + return ([ResourceBase] $this).Get() + } + + # Base method Get() call this method to get the current state as a CimInstance. + [Microsoft.Management.Infrastructure.CimInstance] GetCurrentState([System.Collections.Hashtable] $properties) + { + return (Get-DnsServerScavenging @properties) + } + + [void] Set() + { + # Call the base method to enforce the properties. + ([ResourceBase] $this).Set() + } + + <# + Base method Set() call this method with the properties that should be + enforced and that are not in desired state. + #> + [void] Modify([System.Collections.Hashtable] $properties) + { + Set-DnsServerScavenging @properties + } + + [System.Boolean] Test() + { + # Call the base method to test all of the properties that should be enforced. + return ([ResourceBase] $this).Test() + } + + hidden [void] AssertProperties() + { + @( + 'ScavengingInterval' + 'RefreshInterval' + 'NoRefreshInterval' + ) | ForEach-Object -Process { + $valueToConvert = $this.$_ + + # Only evaluate properties that have a value. + if ($null -ne $valueToConvert) + { + Assert-TimeSpan -PropertyName $_ -Value $valueToConvert -Maximum '365.00:00:00' -Minimum '0.00:00:00' + } + } + } +} +#EndRegion './Classes/003.DnsServerScavenging.ps1' 129 +#Region './Classes/004.DnsRecordAaaaScoped.ps1' 0 +<# + .SYNOPSIS + The DnsRecordAaaaScoped DSC resource manages AAAA DNS records against a specific zone and zone scope on a Domain Name System (DNS) server. + + .DESCRIPTION + The DnsRecordAaaaScoped DSC resource manages AAAA DNS records against a specific zone and zone scope on a Domain Name System (DNS) server. + + .PARAMETER ZoneScope + Specifies the name of a zone scope. (Key Parameter) +#> + +[DscResource()] +class DnsRecordAaaaScoped : DnsRecordAaaa +{ + [DscProperty(Key)] + [System.String] + $ZoneScope + + [DnsRecordAaaaScoped] Get() + { + return ([DnsRecordBase] $this).Get() + } + + [void] Set() + { + ([DnsRecordBase] $this).Set() + } + + [System.Boolean] Test() + { + return ([DnsRecordBase] $this).Test() + } + + hidden [Microsoft.Management.Infrastructure.CimInstance] GetResourceRecord() + { + return ([DnsRecordAaaa] $this).GetResourceRecord() + } + + hidden [DnsRecordAaaaScoped] NewDscResourceObjectFromRecord([Microsoft.Management.Infrastructure.CimInstance] $record) + { + $dscResourceObject = [DnsRecordAaaaScoped] @{ + ZoneName = $this.ZoneName + ZoneScope = $this.ZoneScope + Name = $this.Name + IPv6Address = $this.IPv6Address + TimeToLive = $record.TimeToLive.ToString() + DnsServer = $this.DnsServer + Ensure = 'Present' + } + + return $dscResourceObject + } + + hidden [void] AddResourceRecord() + { + ([DnsRecordAaaa] $this).AddResourceRecord() + } + + hidden [void] ModifyResourceRecord([Microsoft.Management.Infrastructure.CimInstance] $existingRecord, [System.Collections.Hashtable[]] $propertiesNotInDesiredState) + { + ([DnsRecordAaaa] $this).ModifyResourceRecord($existingRecord, $propertiesNotInDesiredState) + } +} +#EndRegion './Classes/004.DnsRecordAaaaScoped.ps1' 64 +#Region './Classes/004.DnsRecordAScoped.ps1' 0 +<# + .SYNOPSIS + The DnsRecordAScoped DSC resource manages A DNS records against a specific zone and zone scope on a Domain Name System (DNS) server. + + .DESCRIPTION + The DnsRecordAScoped DSC resource manages A DNS records against a specific zone and zone scope on a Domain Name System (DNS) server. + + .PARAMETER ZoneScope + Specifies the name of a zone scope. (Key Parameter) +#> + +[DscResource()] +class DnsRecordAScoped : DnsRecordA +{ + [DscProperty(Key)] + [System.String] + $ZoneScope + + [DnsRecordAScoped] Get() + { + return ([DnsRecordBase] $this).Get() + } + + [void] Set() + { + ([DnsRecordBase] $this).Set() + } + + [System.Boolean] Test() + { + return ([DnsRecordBase] $this).Test() + } + + hidden [Microsoft.Management.Infrastructure.CimInstance] GetResourceRecord() + { + return ([DnsRecordA] $this).GetResourceRecord() + } + + hidden [DnsRecordAScoped] NewDscResourceObjectFromRecord([Microsoft.Management.Infrastructure.CimInstance] $record) + { + $dscResourceObject = [DnsRecordAScoped] @{ + ZoneName = $this.ZoneName + ZoneScope = $this.ZoneScope + Name = $this.Name + IPv4Address = $this.IPv4Address + TimeToLive = $record.TimeToLive.ToString() + DnsServer = $this.DnsServer + Ensure = 'Present' + } + + return $dscResourceObject + } + + hidden [void] AddResourceRecord() + { + ([DnsRecordA] $this).AddResourceRecord() + } + + hidden [void] ModifyResourceRecord([Microsoft.Management.Infrastructure.CimInstance] $existingRecord, [System.Collections.Hashtable[]] $propertiesNotInDesiredState) + { + ([DnsRecordA] $this).ModifyResourceRecord($existingRecord, $propertiesNotInDesiredState) + } +} +#EndRegion './Classes/004.DnsRecordAScoped.ps1' 64 +#Region './Classes/004.DnsRecordMxScoped.ps1' 0 +<# + .SYNOPSIS + The DnsRecordMxScoped DSC resource manages MX DNS records against a specific zone and zone scope on a Domain Name System (DNS) server. + + .DESCRIPTION + The DnsRecordMxScoped DSC resource manages MX DNS records against a specific zone and zone scope on a Domain Name System (DNS) server. + + .PARAMETER ZoneScope + Specifies the name of a zone scope. (Key Parameter) +#> + +[DscResource()] +class DnsRecordMxScoped : DnsRecordMx +{ + [DscProperty(Key)] + [System.String] + $ZoneScope + + [DnsRecordMxScoped] Get() + { + return ([DnsRecordBase] $this).Get() + } + + [void] Set() + { + ([DnsRecordBase] $this).Set() + } + + [System.Boolean] Test() + { + return ([DnsRecordBase] $this).Test() + } + + hidden [Microsoft.Management.Infrastructure.CimInstance] GetResourceRecord() + { + return ([DnsRecordMx] $this).GetResourceRecord() + } + + hidden [DnsRecordMxScoped] NewDscResourceObjectFromRecord([Microsoft.Management.Infrastructure.CimInstance] $record) + { + $dscResourceObject = [DnsRecordMxScoped] @{ + ZoneName = $this.ZoneName + ZoneScope = $this.ZoneScope + EmailDomain = $this.EmailDomain + MailExchange = $this.MailExchange + Priority = $record.RecordData.Preference + TimeToLive = $record.TimeToLive.ToString() + DnsServer = $this.DnsServer + Ensure = 'Present' + } + + return $dscResourceObject + } + + hidden [void] AddResourceRecord() + { + ([DnsRecordMx] $this).AddResourceRecord() + } + + hidden [void] ModifyResourceRecord([Microsoft.Management.Infrastructure.CimInstance] $existingRecord, [System.Collections.Hashtable[]] $propertiesNotInDesiredState) + { + ([DnsRecordMx] $this).ModifyResourceRecord($existingRecord, $propertiesNotInDesiredState) + } +} +#EndRegion './Classes/004.DnsRecordMxScoped.ps1' 65 +#Region './Classes/004.DnsRecordNsScoped.ps1' 0 +<# + .SYNOPSIS + The DnsRecordNsScoped DSC resource manages NS DNS records against a specific zone and zone scope on a Domain Name System (DNS) server. + + .DESCRIPTION + The DnsRecordNsScoped DSC resource manages NS DNS records against a specific zone and zone scope on a Domain Name System (DNS) server. + + .PARAMETER ZoneScope + Specifies the name of a zone scope. (Key Parameter) +#> + +[DscResource()] +class DnsRecordNsScoped : DnsRecordNs +{ + [DscProperty(Key)] + [System.String] + $ZoneScope + + [DnsRecordNsScoped] Get() + { + return ([DnsRecordBase] $this).Get() + } + + [void] Set() + { + ([DnsRecordBase] $this).Set() + } + + [System.Boolean] Test() + { + return ([DnsRecordBase] $this).Test() + } + + hidden [Microsoft.Management.Infrastructure.CimInstance] GetResourceRecord() + { + return ([DnsRecordNs] $this).GetResourceRecord() + } + + hidden [DnsRecordNsScoped] NewDscResourceObjectFromRecord([Microsoft.Management.Infrastructure.CimInstance] $record) + { + $dscResourceObject = [DnsRecordNsScoped] @{ + ZoneName = $this.ZoneName + ZoneScope = $this.ZoneScope + DomainName = $this.DomainName + NameServer = $this.NameServer + TimeToLive = $record.TimeToLive.ToString() + DnsServer = $this.DnsServer + Ensure = 'Present' + } + + return $dscResourceObject + } + + hidden [void] AddResourceRecord() + { + ([DnsRecordNs] $this).AddResourceRecord() + } + + hidden [void] ModifyResourceRecord([Microsoft.Management.Infrastructure.CimInstance] $existingRecord, [System.Collections.Hashtable[]] $propertiesNotInDesiredState) + { + ([DnsRecordNs] $this).ModifyResourceRecord($existingRecord, $propertiesNotInDesiredState) + } +} +#EndRegion './Classes/004.DnsRecordNsScoped.ps1' 64 +#Region './Classes/004.DnsRecordSrvScoped.ps1' 0 +<# + .SYNOPSIS + The DnsRecordSrvScoped DSC resource manages SRV DNS records against a specific zone and zone scope on a Domain Name System (DNS) server. + + .DESCRIPTION + The DnsRecordSrvScoped DSC resource manages SRV DNS records against a specific zone and zone scope on a Domain Name System (DNS) server. + + .PARAMETER ZoneScope + Specifies the name of a zone scope. (Key Parameter) +#> + +[DscResource()] +class DnsRecordSrvScoped : DnsRecordSrv +{ + [DscProperty(Key)] + [System.String] + $ZoneScope + + [DnsRecordSrvScoped] Get() + { + return ([DnsRecordBase] $this).Get() + } + + [void] Set() + { + ([DnsRecordBase] $this).Set() + } + + [System.Boolean] Test() + { + return ([DnsRecordBase] $this).Test() + } + + hidden [Microsoft.Management.Infrastructure.CimInstance] GetResourceRecord() + { + return ([DnsRecordSrv] $this).GetResourceRecord() + } + + hidden [DnsRecordSrvScoped] NewDscResourceObjectFromRecord([Microsoft.Management.Infrastructure.CimInstance] $record) + { + $dscResourceObject = [DnsRecordSrvScoped] @{ + ZoneName = $this.ZoneName + ZoneScope = $this.ZoneScope + SymbolicName = $this.SymbolicName + Protocol = $this.Protocol.ToLower() + Port = $this.Port + Target = ($record.RecordData.DomainName).TrimEnd('.') + Priority = $record.RecordData.Priority + Weight = $record.RecordData.Weight + TimeToLive = $record.TimeToLive.ToString() + DnsServer = $this.DnsServer + Ensure = 'Present' + } + + return $dscResourceObject + } + + hidden [void] AddResourceRecord() + { + ([DnsRecordSrv] $this).AddResourceRecord() + } + + hidden [void] ModifyResourceRecord([Microsoft.Management.Infrastructure.CimInstance] $existingRecord, [System.Collections.Hashtable[]] $propertiesNotInDesiredState) + { + ([DnsRecordSrv] $this).ModifyResourceRecord($existingRecord, $propertiesNotInDesiredState) + } +} +#EndRegion './Classes/004.DnsRecordSrvScoped.ps1' 68 +#Region './Private/Assert-TimeSpan.ps1' 0 +<# + .SYNOPSIS + Assert that the value provided can be converted to a TimeSpan object. + + .PARAMETER Value + The time value as a string that should be converted. +#> +function Assert-TimeSpan +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true, ValueFromPipeline = $true)] + [System.String] + $Value, + + [Parameter(Mandatory = $true)] + [System.String] + $PropertyName, + + [Parameter()] + [System.TimeSpan] + $Maximum, + + [Parameter()] + [System.TimeSpan] + $Minimum + ) + + $timeSpanObject = $Value | ConvertTo-TimeSpan + + # If the conversion fails $null is returned. + if ($null -eq $timeSpanObject) + { + $errorMessage = $script:localizedData.PropertyHasWrongFormat -f $PropertyName, $Value + + New-InvalidOperationException -Message $errorMessage + } + + if ($PSBoundParameters.ContainsKey('Maximum') -and $timeSpanObject -gt $Maximum) + { + $errorMessage = $script:localizedData.TimeSpanExceedMaximumValue -f $PropertyName, $timeSpanObject.ToString(), $Maximum + + New-InvalidOperationException -Message $errorMessage + } + + if ($PSBoundParameters.ContainsKey('Minimum') -and $timeSpanObject -lt $Minimum) + { + $errorMessage = $script:localizedData.TimeSpanBelowMinimumValue -f $PropertyName, $timeSpanObject.ToString(), $Minimum + + New-InvalidOperationException -Message $errorMessage + } +} +#EndRegion './Private/Assert-TimeSpan.ps1' 54 +#Region './Private/ConvertTo-HashtableFromObject.ps1' 0 +<# + .SYNOPSIS + Convert any object to hashtable + + .PARAMETER InputObject + The object that should convert to hashtable. +#> +function ConvertTo-HashtableFromObject +{ + [CmdletBinding()] + [OutputType([System.Collections.Hashtable])] + param + ( + [Parameter(Mandatory = $true, ValueFromPipeline = $true)] + [PSObject] + $InputObject + ) + + $hashResult = @{} + + $InputObject.psobject.Properties | Foreach-Object { + $hashResult[$_.Name] = $_.Value + } + + return $hashResult +} +#EndRegion './Private/ConvertTo-HashtableFromObject.ps1' 27 +#Region './Private/ConvertTo-TimeSpan.ps1' 0 +<# + .SYNOPSIS + Converts a string value to a TimeSpan object. + + .PARAMETER Value + The time value as a string that should be converted. + + .OUTPUTS + Returns an TimeSpan object containing the converted value, or $null if + conversion was not possible. +#> +function ConvertTo-TimeSpan +{ + [CmdletBinding()] + [OutputType([System.TimeSpan])] + param + ( + [Parameter(Mandatory = $true, ValueFromPipeline = $true)] + [System.String] + $Value + ) + + $timeSpan = New-TimeSpan + + if (-not [System.TimeSpan]::TryParse($Value, [ref] $timeSpan)) + { + $timeSpan = $null + } + + return $timeSpan +} +#EndRegion './Private/ConvertTo-TimeSpan.ps1' 32 +#Region './Private/Get-ClassName.ps1' 0 +<# + .SYNOPSIS + Get the class name of the passed object, and optional an array with + all inherited classes. + + .PARAMETER InputObject + The object to be evaluated. + + .OUTPUTS + Returns a string array with at least one item. +#> +function Get-ClassName +{ + [CmdletBinding()] + [OutputType([System.Object[]])] + param + ( + [Parameter(Mandatory = $true, ValueFromPipeline = $true)] + [PSObject] + $InputObject, + + [Parameter()] + [System.Management.Automation.SwitchParameter] + $Recurse + ) + + # Create a list of the inherited class names + $class = @($InputObject.GetType().FullName) + + if ($Recurse.IsPresent) + { + $parentClass = $InputObject.GetType().BaseType + + while ($parentClass -ne [System.Object]) + { + $class += $parentClass.FullName + + $parentClass = $parentClass.BaseType + } + } + + return ,$class +} +#EndRegion './Private/Get-ClassName.ps1' 44 +#Region './Private/Get-LocalizedDataRecursive.ps1' 0 +<# + .SYNOPSIS + Get the localization strings data from one or more localization string files. + This can be used in classes to be able to inherit localization strings + from one or more parent (base) classes. + + The order of class names passed to parameter `ClassName` determines the order + of importing localization string files. First entry's localization string file + will be imported first, then next entry's localization string file, and so on. + If the second (or any consecutive) entry's localization string file contain a + localization string key that existed in a previous imported localization string + file that localization string key will be ignored. Making it possible for a + child class to override localization strings from one or more parent (base) + classes. + + .PARAMETER ClassName + An array of class names, normally provided by `Get-ClassName -Recurse`. + + .OUTPUTS + Returns a string array with at least one item. +#> +function Get-LocalizedDataRecursive +{ + [CmdletBinding()] + [OutputType([System.Collections.Hashtable])] + param + ( + [Parameter(Mandatory = $true, ValueFromPipeline = $true)] + [System.String[]] + $ClassName + ) + + begin + { + $localizedData = @{} + } + + process + { + foreach ($name in $ClassName) + { + if ($name -match '\.psd1') + { + # Assume we got full file name. + $localizationFileName = $name + } + else + { + # Assume we only got class name. + $localizationFileName = '{0}.strings.psd1' -f $name + } + + Write-Debug -Message ('Importing localization data from {0}' -f $localizationFileName) + + # Get localized data for the class + $classLocalizationStrings = Get-LocalizedData -DefaultUICulture 'en-US' -FileName $localizationFileName -ErrorAction 'Stop' + + # Append only previously unspecified keys in the localization data + foreach ($key in $classLocalizationStrings.Keys) + { + if (-not $localizedData.ContainsKey($key)) + { + $localizedData[$key] = $classLocalizationStrings[$key] + } + } + } + } + + end + { + Write-Debug -Message ('Localization data: {0}' -f ($localizedData | ConvertTo-JSON)) + + return $localizedData + } +} +#EndRegion './Private/Get-LocalizedDataRecursive.ps1' 76 diff --git a/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/Modules/DnsServerDsc.Common/DnsServerDsc.Common.psd1 b/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/Modules/DnsServerDsc.Common/DnsServerDsc.Common.psd1 new file mode 100644 index 0000000..1e8f9c9 --- /dev/null +++ b/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/Modules/DnsServerDsc.Common/DnsServerDsc.Common.psd1 @@ -0,0 +1,47 @@ +@{ + # Script module or binary module file associated with this manifest. + RootModule = 'DnsServerDsc.Common.psm1' + + # Version number of this module. + ModuleVersion = '1.0.0' + + # ID used to uniquely identify this module + GUID = 'df2cccf3-f8bd-4142-9539-ed5486caebe1' + + # Author of this module + Author = 'DSC Community' + + # Company or vendor of this module + CompanyName = 'DSC Community' + + # Copyright statement for this module + Copyright = 'Copyright the DSC Community contributors. All rights reserved.' + + # Description of the functionality provided by this module + Description = 'Functions used by the DSC resources in DnsServerDsc.' + + # Functions to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no functions to export. + FunctionsToExport = @( + 'Remove-CommonParameter' + 'ConvertTo-CimInstance' + 'ConvertTo-FollowRfc1034' + 'ConvertTo-HashTable' + 'Convert-RootHintsToHashtable' + 'Test-DscDnsParameterState' + ) + + # Cmdlets to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no cmdlets to export. + CmdletsToExport = @() + + # Variables to export from this module + VariablesToExport = @() + + # Aliases to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no aliases to export. + AliasesToExport = @() + + # Private data to pass to the module specified in RootModule/ModuleToProcess. This may also contain a PSData hashtable with additional module metadata used by PowerShell. + PrivateData = @{ + PSData = @{ + } # End of PSData hashtable + } # End of PrivateData hashtable +} diff --git a/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/Modules/DnsServerDsc.Common/DnsServerDsc.Common.psm1 b/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/Modules/DnsServerDsc.Common/DnsServerDsc.Common.psm1 new file mode 100644 index 0000000..d3e3232 --- /dev/null +++ b/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/Modules/DnsServerDsc.Common/DnsServerDsc.Common.psm1 @@ -0,0 +1,416 @@ +$script:resourceHelperModulePath = Join-Path -Path $PSScriptRoot -ChildPath '..\..\Modules\DscResource.Common' + +Import-Module -Name $script:resourceHelperModulePath + +$script:localizedData = Get-LocalizedData -DefaultUICulture 'en-US' + +<# + .SYNOPSIS + Converts a string to a fully qualified DNS domain name, if its not already. + + .DESCRIPTION + This function is used to convert a string into a fully qualified DNS domain name by appending a '.' to the end. + + .PARAMETER Name + A string with the value to convert. + + .OUTPUTS + System.String +#> +function ConvertTo-FollowRfc1034 +{ + [CmdletBinding()] + [OutputType([System.String])] + param + ( + [Parameter(Mandatory = $true, ValueFromPipeline = $true)] + [System.String] + $Name + ) + + if (-not $Name.EndsWith('.')) + { + return "$Name." + } + + return $Name +} + +<# + .SYNOPSIS + Converts root hints like the DNS cmdlets are run. + + .DESCRIPTION + This function is used to convert a CimInstance array containing MSFT_KeyValuePair objects into a hashtable. + + .PARAMETER CimInstance + An array of CimInstances or a single CimInstance object to convert. + + .OUTPUTS + System.Collections.Hashtable +#> +function Convert-RootHintsToHashtable +{ + [CmdletBinding()] + [OutputType([System.Collections.Hashtable])] + param + ( + [Parameter(Mandatory = $true)] + [System.Object[]] + [AllowEmptyCollection()] + $RootHints + ) + + $r = @{ } + + foreach ($rootHint in $RootHints) + { + if (-not $rootHint.IPAddress) + { + continue + } + + $ip = if ($rootHint.IPAddress.RecordData.IPv4Address) + { + $rootHint.IPAddress.RecordData.IPv4Address.IPAddressToString -join ',' + } + else + { + $rootHint.IPAddress.RecordData.IPv6Address.IPAddressToString -join ',' + } + + $r.Add($rootHint.NameServer.RecordData.NameServer, $ip) + } + + return $r +} + +<# + .SYNOPSIS + Tests the status of DSC resource parameters. + + .DESCRIPTION + This function tests the parameter status of DSC resource parameters against the current values present on the system. + + .PARAMETER CurrentValues + A hashtable with the current values on the system, obtained by e.g. Get-TargetResource. + + .PARAMETER DesiredValues + The hashtable of desired values. + + .PARAMETER ValuesToCheck + The values to check if not all values should be checked. + + .PARAMETER TurnOffTypeChecking + Indicates that the type of the parameter should not be checked. + + .PARAMETER ReverseCheck + Indicates that a reverse check should be done. The current and desired state are swapped for another test. + + .PARAMETER SortArrayValues + If the sorting of array values does not matter, values are sorted internally before doing the comparison. + + .NOTES + This function is enhanced with additional parameters compared to the function + Test-DscParameterState that is available in the module DscResource.Common. + These enhancements should be merged into DscResource.Common; + https://github.com/dsccommunity/DscResource.Common/blob/master/source/Public/Test-DscParameterState.ps1. +#> +function Test-DscDnsParameterState +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [System.Object] + $CurrentValues, + + [Parameter(Mandatory = $true)] + [System.Object] + $DesiredValues, + + [Parameter()] + [System.String[]] + $ValuesToCheck, + + [Parameter()] + [switch] + $TurnOffTypeChecking, + + [Parameter()] + [switch] + $ReverseCheck, + + [Parameter()] + [switch] + $SortArrayValues + ) + + $returnValue = $true + + if ($CurrentValues -is [Microsoft.Management.Infrastructure.CimInstance] -or + $CurrentValues -is [Microsoft.Management.Infrastructure.CimInstance[]]) + { + $CurrentValues = ConvertTo-HashTable -CimInstance $CurrentValues + } + + if ($DesiredValues -is [Microsoft.Management.Infrastructure.CimInstance] -or + $DesiredValues -is [Microsoft.Management.Infrastructure.CimInstance[]]) + { + $DesiredValues = ConvertTo-HashTable -CimInstance $DesiredValues + } + + $types = 'System.Management.Automation.PSBoundParametersDictionary', 'System.Collections.Hashtable', 'Microsoft.Management.Infrastructure.CimInstance' + + if ($DesiredValues.GetType().FullName -notin $types) + { + New-InvalidArgumentException ` + -Message ($script:localizedData.InvalidDesiredValuesError -f $DesiredValues.GetType().FullName) ` + -ArgumentName 'DesiredValues' + } + + if ($CurrentValues.GetType().FullName -notin $types) + { + New-InvalidArgumentException ` + -Message ($script:localizedData.InvalidCurrentValuesError -f $CurrentValues.GetType().FullName) ` + -ArgumentName 'CurrentValues' + } + + if ($DesiredValues -is [Microsoft.Management.Infrastructure.CimInstance] -and -not $ValuesToCheck) + { + New-InvalidArgumentException ` + -Message $script:localizedData.InvalidValuesToCheckError ` + -ArgumentName 'ValuesToCheck' + } + + $desiredValuesClean = Remove-CommonParameter -Hashtable $DesiredValues + + if (-not $ValuesToCheck) + { + $keyList = $desiredValuesClean.Keys + } + else + { + $keyList = $ValuesToCheck + } + + foreach ($key in $keyList) + { + $desiredValue = $desiredValuesClean.$key + $currentValue = $CurrentValues.$key + + if ($desiredValue -is [Microsoft.Management.Infrastructure.CimInstance] -or + $desiredValue -is [Microsoft.Management.Infrastructure.CimInstance[]]) + { + $desiredValue = ConvertTo-HashTable -CimInstance $desiredValue + } + if ($currentValue -is [Microsoft.Management.Infrastructure.CimInstance] -or + $currentValue -is [Microsoft.Management.Infrastructure.CimInstance[]]) + { + $currentValue = ConvertTo-HashTable -CimInstance $currentValue + } + + if ($null -ne $desiredValue) + { + $desiredType = $desiredValue.GetType() + } + else + { + $desiredType = @{ + Name = 'Unknown' + } + } + + if ($null -ne $currentValue) + { + $currentType = $currentValue.GetType() + } + else + { + $currentType = @{ + Name = 'Unknown' + } + } + + if ($currentType.Name -ne 'Unknown' -and $desiredType.Name -eq 'PSCredential') + { + # This is a credential object. Compare only the user name + if ($currentType.Name -eq 'PSCredential' -and $currentValue.UserName -eq $desiredValue.UserName) + { + Write-Verbose -Message ($script:localizedData.MatchPsCredentialUsernameMessage -f $currentValue.UserName, $desiredValue.UserName) + continue + } + else + { + Write-Verbose -Message ($script:localizedData.NoMatchPsCredentialUsernameMessage -f $currentValue.UserName, $desiredValue.UserName) + $returnValue = $false + } + + # Assume the string is our username when the matching desired value is actually a credential + if ($currentType.Name -eq 'string' -and $currentValue -eq $desiredValue.UserName) + { + Write-Verbose -Message ($script:localizedData.MatchPsCredentialUsernameMessage -f $currentValue, $desiredValue.UserName) + continue + } + else + { + Write-Verbose -Message ($script:localizedData.NoMatchPsCredentialUsernameMessage -f $currentValue, $desiredValue.UserName) + $returnValue = $false + } + } + + if (-not $TurnOffTypeChecking) + { + if (($desiredType.Name -ne 'Unknown' -and $currentType.Name -ne 'Unknown') -and + $desiredType.FullName -ne $currentType.FullName) + { + Write-Verbose -Message ($script:localizedData.NoMatchTypeMismatchMessage -f $key, $currentType.Name, $desiredType.Name) + $returnValue = $false + continue + } + } + + if ($currentValue -eq $desiredValue -and -not $desiredType.IsArray) + { + Write-Verbose -Message ($script:localizedData.MatchValueMessage -f $desiredType.Name, $key, $currentValue, $desiredValue) + continue + } + + if ($desiredValuesClean.GetType().Name -in 'HashTable', 'PSBoundParametersDictionary') + { + $checkDesiredValue = $desiredValuesClean.ContainsKey($key) + } + else + { + $checkDesiredValue = Test-DscObjectHasProperty -Object $desiredValuesClean -PropertyName $key + } + + if (-not $checkDesiredValue) + { + Write-Verbose -Message ($script:localizedData.MatchValueMessage -f $desiredType.Name, $key, $currentValue, $desiredValue) + continue + } + + if ($desiredType.IsArray) + { + Write-Verbose -Message ($script:localizedData.TestDscParameterCompareMessage -f $key) + + if (-not $currentValue) + { + Write-Verbose -Message ($script:localizedData.NoMatchValueMessage -f $desiredType.Name, $key, $currentValue, $desiredValue) + $returnValue = $false + continue + } + elseif ($currentValue.Count -ne $desiredValue.Count) + { + Write-Verbose -Message ($script:localizedData.NoMatchValueDifferentCountMessage -f $desiredType.Name, $key, $currentValue.Count, $desiredValue.Count) + $returnValue = $false + continue + } + else + { + $desiredArrayValues = $desiredValue + $currentArrayValues = $currentValue + + if ($SortArrayValues) + { + $desiredArrayValues = $desiredArrayValues | Sort-Object + $currentArrayValues = $currentArrayValues | Sort-Object + } + + for ($i = 0; $i -lt $desiredArrayValues.Count; $i++) + { + if ($null -ne $desiredArrayValues[$i]) + { + $desiredType = $desiredArrayValues[$i].GetType() + } + else + { + $desiredType = @{ + Name = 'Unknown' + } + } + + if ($null -ne $currentArrayValues[$i]) + { + $currentType = $currentArrayValues[$i].GetType() + } + else + { + $currentType = @{ + Name = 'Unknown' + } + } + + if (-not $TurnOffTypeChecking) + { + if (($desiredType.Name -ne 'Unknown' -and $currentType.Name -ne 'Unknown') -and + $desiredType.FullName -ne $currentType.FullName) + { + Write-Verbose -Message ($script:localizedData.NoMatchElementTypeMismatchMessage -f $key, $i, $currentType.Name, $desiredType.Name) + $returnValue = $false + continue + } + } + + if ($desiredArrayValues[$i] -ne $currentArrayValues[$i]) + { + Write-Verbose -Message ($script:localizedData.NoMatchElementValueMismatchMessage -f $i, $desiredType.Name, $key, $currentArrayValues[$i], $desiredArrayValues[$i]) + $returnValue = $false + continue + } + else + { + Write-Verbose -Message ($script:localizedData.MatchElementValueMessage -f $i, $desiredType.Name, $key, $currentArrayValues[$i], $desiredArrayValues[$i]) + continue + } + } + + } + } + elseif ($desiredType -eq [System.Collections.Hashtable] -and $currentType -eq [System.Collections.Hashtable]) + { + $param = $PSBoundParameters + $param.CurrentValues = $currentValue + $param.DesiredValues = $desiredValue + [void]$param.Remove('ValuesToCheck') + if ($returnValue) + { + $returnValue = Test-DscDnsParameterState @param + } + else + { + Test-DscDnsParameterState @param | Out-Null + } + continue + } + else + { + if ($desiredValue -ne $currentValue) + { + Write-Verbose -Message ($script:localizedData.NoMatchValueMessage -f $desiredType.Name, $key, $currentValue, $desiredValue) + $returnValue = $false + } + } + } + + if ($ReverseCheck) + { + Write-Verbose -Message $script:localizedData.StartingReverseCheck + $reverseCheckParameters = $PSBoundParameters + $reverseCheckParameters.CurrentValues = $DesiredValues + $reverseCheckParameters.DesiredValues = $CurrentValues + [void] $reverseCheckParameters.Remove('ReverseCheck') + if ($returnValue) + { + $returnValue = Test-DscDnsParameterState @reverseCheckParameters + } + else + { + Test-DscDnsParameterState @reverseCheckParameters | Out-Null + } + } + + Write-Verbose -Message ($script:localizedData.TestDscParameterResultMessage -f $returnValue) + return $returnValue +} diff --git a/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/Modules/DnsServerDsc.Common/en-US/DnsServerDsc.Common.strings.psd1 b/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/Modules/DnsServerDsc.Common/en-US/DnsServerDsc.Common.strings.psd1 new file mode 100644 index 0000000..81f9d66 --- /dev/null +++ b/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/Modules/DnsServerDsc.Common/en-US/DnsServerDsc.Common.strings.psd1 @@ -0,0 +1,17 @@ +ConvertFrom-StringData @' + InvalidCurrentValuesError = Property 'CurrentValues' in Test-DscDnsParameterState must be either a Hashtable, CimInstance or CimIntance[]. Type detected was '{0}'. + InvalidDesiredValuesError = Property 'DesiredValues' in Test-DscDnsParameterState must be either a Hashtable or CimInstance. Type detected was '{0}'. + InvalidValuesToCheckError = If 'DesiredValues' is a CimInstance then property 'ValuesToCheck' must contain a value. + TestDscParameterCompareMessage = Comparing values in property '{0}'. + MatchPsCredentialUsernameMessage = MATCH: PSCredential username match. Current state is '{0}' and desired state is '{1}'. + NoMatchPsCredentialUsernameMessage = NOTMATCH: PSCredential username mismatch. Current state is '{0}' and desired state is '{1}'. + NoMatchTypeMismatchMessage = NOTMATCH: Type mismatch for property '{0}' Current state type is '{1}' and desired type is '{2}'. + MatchValueMessage = MATCH: Value (type '{0}') for property '{1}' does match. Current state is '{2}' and desired state is '{3}'. + NoMatchValueMessage = NOTMATCH: Value (type '{0}') for property '{1}' does not match. Current state is '{2}' and desired state is '{3}'. + NoMatchValueDifferentCountMessage = NOTMATCH: Value (type '{0}') for property '{1}' does have a different count. Current state count is '{2}' and desired state count is '{3}'. + NoMatchElementTypeMismatchMessage = NOTMATCH: Type mismatch for property '{0}' Current state type of element [{1}] is '{2}' and desired type is '{3}'. + NoMatchElementValueMismatchMessage = NOTMATCH: Value [{0}] (type '{1}') for property '{2}' does match. Current state is '{3}' and desired state is '{4}'. + MatchElementValueMessage = MATCH: Value [{0}] (type '{1}') for property '{2}' does match. Current state is '{3}' and desired state is '{4}'. + TestDscParameterResultMessage = Test-DscParameter result is '{0}'. + StartingReverseCheck = Starting with a reverse check. +'@ diff --git a/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/Modules/DscResource.Common/0.10.2/DscResource.Common.psd1 b/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/Modules/DscResource.Common/0.10.2/DscResource.Common.psd1 new file mode 100644 index 0000000..79e1735 --- /dev/null +++ b/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/Modules/DscResource.Common/0.10.2/DscResource.Common.psd1 @@ -0,0 +1,82 @@ +@{ + # Script module or binary module file associated with this manifest. + RootModule = 'DscResource.Common.psm1' + + # Version number of this module. + ModuleVersion = '0.10.2' + + # ID used to uniquely identify this module + GUID = '9c9daa5b-5c00-472d-a588-c96e8e498450' + + # Author of this module + Author = 'DSC Community' + + # Company or vendor of this module + CompanyName = 'DSC Community' + + # Copyright statement for this module + Copyright = 'Copyright the DSC Community contributors. All rights reserved.' + + # Description of the functionality provided by this module + Description = 'Common functions used in DSC Resources' + + # Minimum version of the PowerShell engine required by this module + PowerShellVersion = '4.0' + + # Functions to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no functions to export. + FunctionsToExport = @('Assert-BoundParameter','Assert-IPAddress','Assert-Module','Compare-DscParameterState','Compare-ResourcePropertyState','ConvertTo-CimInstance','ConvertTo-HashTable','Get-ComputerName','Get-LocalizedData','Get-TemporaryFolder','New-InvalidArgumentException','New-InvalidDataException','New-InvalidOperationException','New-InvalidResultException','New-NotImplementedException','New-ObjectNotFoundException','Remove-CommonParameter','Set-DscMachineRebootRequired','Set-PSModulePath','Test-DscParameterState','Test-IsNanoServer') + + # Cmdlets to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no cmdlets to export. + CmdletsToExport = @() + + # Variables to export from this module + VariablesToExport = @() + + # Aliases to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no aliases to export. + AliasesToExport = @() + + # Private data to pass to the module specified in RootModule/ModuleToProcess. This may also contain a PSData hashtable with additional module metadata used by PowerShell. + PrivateData = @{ + + PSData = @{ + # Tags applied to this module. These help with module discovery in online galleries. + Tags = @('DSC', 'Localization') + + # A URL to the license for this module. + LicenseUri = 'https://github.com/dsccommunity/DscResource.Common/blob/main/LICENSE' + + # A URL to the main website for this project. + ProjectUri = 'https://github.com/dsccommunity/DscResource.Common' + + # A URL to an icon representing this module. + IconUri = 'https://dsccommunity.org/images/DSC_Logo_300p.png' + + # ReleaseNotes of this module + ReleaseNotes = '## [0.10.2] - 2021-03-24 + +### Changed + +- DscResource.Common + - Renamed default branch to `main` - fixes [issue #62](https://github.com/dsccommunity/DscResource.Common/issues/62). + - Changed to use the new GitHub deploy tasks. +- `Assert-Module` + - Now it possible to forcibly import a module using `-ImportModule -Force` + - It no longer outputs verbose messages that is normally generated when + using `Get-Module -ListAvailable` if the module that is asserted is + already in the session ([issue #66](https://github.com/dsccommunity/DscResource.Common/issues/66)). +- `Compare-DscParameterState` + - Fix verbose message to only show when using parameter `IncludeInDesiredState`. + Also made the verbose message more intuitive when the value being compared + was a `[System.Boolean]`. + +' + + Prerelease = '' + } # End of PSData hashtable + + } # End of PrivateData hashtable +} + + + + diff --git a/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/Modules/DscResource.Common/0.10.2/DscResource.Common.psm1 b/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/Modules/DscResource.Common/0.10.2/DscResource.Common.psm1 new file mode 100644 index 0000000..c691324 --- /dev/null +++ b/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/Modules/DscResource.Common/0.10.2/DscResource.Common.psm1 @@ -0,0 +1,2535 @@ +#Region './prefix.ps1' 0 +$script:modulesFolderPath = Split-Path -Path $PSScriptRoot -Parent +#EndRegion './prefix.ps1' 2 +#Region './Private/Test-DscObjectHasProperty.ps1' 0 +<# + .SYNOPSIS + Tests if an object has a property. + + .DESCRIPTION + Tests if the specified object has the specified property and return + $true or $false. + + .PARAMETER Object + Specifies the object to test for the specified property. + + .PARAMETER PropertyName + Specifies the property name to test for. + + .EXAMPLE + Test-DscObjectHasProperty -Object 'AnyString' -PropertyName 'Length' +#> +function Test-DscObjectHasProperty +{ + [CmdletBinding()] + [OutputType([System.Boolean])] + param + ( + [Parameter(Mandatory = $true)] + [System.Object] + $Object, + + [Parameter(Mandatory = $true)] + [System.String] + $PropertyName + ) + + if ($Object.PSObject.Properties.Name -contains $PropertyName) + { + return [System.Boolean] $Object.$PropertyName + } + + return $false +} +#EndRegion './Private/Test-DscObjectHasProperty.ps1' 40 +#Region './Private/Test-DscPropertyState.ps1' 0 +<# + .SYNOPSIS + Compares the current and the desired value of a property. + + .DESCRIPTION + This function is used to compare the current and the desired value of a + property. + + .PARAMETER Values + This is set to a hash table with the current value (the CurrentValue key) + and desired value (the DesiredValue key). + + .EXAMPLE + Test-DscPropertyState -Values @{ + CurrentValue = 'John' + DesiredValue = 'Alice' + } + + .EXAMPLE + Test-DscPropertyState -Values @{ + CurrentValue = 1 + DesiredValue = 2 + } + + .NOTES + This function is used by the cmdlet Compare-ResourcePropertyState. +#> +function Test-DscPropertyState +{ + [CmdletBinding()] + [OutputType([System.Boolean])] + param + ( + [Parameter(Mandatory = $true)] + [System.Collections.Hashtable] + $Values + ) + + if ($null -eq $Values.CurrentValue -and $null -eq $Values.DesiredValue) + { + # Both values are $null so return $true + $returnValue = $true + } + elseif ($null -eq $Values.CurrentValue -or $null -eq $Values.DesiredValue) + { + # Either CurrentValue or DesiredValue are $null so return $false + $returnValue = $false + } + elseif ( + $Values.DesiredValue -is [Microsoft.Management.Infrastructure.CimInstance[]] ` + -or $Values.DesiredValue -is [System.Array] -and $Values.DesiredValue[0] -is [Microsoft.Management.Infrastructure.CimInstance] + ) + { + if (-not $Values.ContainsKey('KeyProperties')) + { + $errorMessage = $script:localizedData.KeyPropertiesMissing + + New-InvalidOperationException -Message $errorMessage + } + + $propertyState = @() + + <# + It is a collection of CIM instances, then recursively call + Test-DscPropertyState for each CIM instance in the collection. + #> + foreach ($desiredCimInstance in $Values.DesiredValue) + { + $currentCimInstance = $Values.CurrentValue + + <# + Use the CIM instance Key properties to filter out the current + values if the exist. + #> + foreach ($keyProperty in $Values.KeyProperties) + { + $currentCimInstance = $currentCimInstance | + Where-Object -Property $keyProperty -EQ -Value $desiredCimInstance.$keyProperty + } + + if ($currentCimInstance.Count -gt 1) + { + $errorMessage = $script:localizedData.TooManyCimInstances + + New-InvalidOperationException -Message $errorMessage + } + + if ($currentCimInstance) + { + $keyCimInstanceProperties = $currentCimInstance.CimInstanceProperties | + Where-Object -FilterScript { + $_.Name -in $Values.KeyProperties + } + + <# + For each key property build a string representation of the + property name and its value. + #> + $keyPropertyValues = $keyCimInstanceProperties.ForEach({'{0}="{1}"' -f $_.Name, ($_.Value -join ',')}) + + Write-Debug -Message ( + $script:localizedData.TestingCimInstance -f @( + $currentCimInstance.CimClass.CimClassName, + ($keyPropertyValues -join ';') + ) + ) + } + else + { + $keyCimInstanceProperties = $desiredCimInstance.CimInstanceProperties | + Where-Object -FilterScript { + $_.Name -in $Values.KeyProperties + } + + <# + For each key property build a string representation of the + property name and its value. + #> + $keyPropertyValues = $keyCimInstanceProperties.ForEach({'{0}="{1}"' -f $_.Name, ($_.Value -join ',')}) + + Write-Debug -Message ( + $script:localizedData.MissingCimInstance -f @( + $desiredCimInstance.CimClass.CimClassName, + ($keyPropertyValues -join ';') + ) + ) + } + + # Recursively call Test-DscPropertyState with the CimInstance to evaluate. + $propertyState += Test-DscPropertyState -Values @{ + CurrentValue = $currentCimInstance + DesiredValue = $desiredCimInstance + } + } + + # Return $false if one property is found to not be in desired state. + $returnValue = -not ($false -in $propertyState) + } + elseif ($Values.DesiredValue -is [Microsoft.Management.Infrastructure.CimInstance]) + { + $propertyState = @() + + <# + It is a CIM instance, recursively call Test-DscPropertyState for each + CIM instance property. + #> + $desiredCimInstanceProperties = $Values.DesiredValue.CimInstanceProperties | + Select-Object -Property @('Name', 'Value') + + if ($desiredCimInstanceProperties) + { + foreach ($desiredCimInstanceProperty in $desiredCimInstanceProperties) + { + <# + Recursively call Test-DscPropertyState to evaluate each property + in the CimInstance. + #> + $propertyState += Test-DscPropertyState -Values @{ + CurrentValue = $Values.CurrentValue.($desiredCimInstanceProperty.Name) + DesiredValue = $desiredCimInstanceProperty.Value + } + } + } + else + { + if ($Values.CurrentValue.CimInstanceProperties.Count -gt 0) + { + # Current value did not have any CIM properties, but desired state has. + $propertyState += $false + } + } + + # Return $false if one property is found to not be in desired state. + $returnValue = -not ($false -in $propertyState) + } + elseif ($Values.DesiredValue -is [System.Array] -or $Values.CurrentValue -is [System.Array]) + { + $compareObjectParameters = @{ + ReferenceObject = $Values.CurrentValue + DifferenceObject = $Values.DesiredValue + } + + $arrayCompare = Compare-Object @compareObjectParameters + + if ($null -ne $arrayCompare) + { + Write-Debug -Message $script:localizedData.ArrayDoesNotMatch + + $arrayCompare | + ForEach-Object -Process { + if ($_.SideIndicator -eq '=>') + { + Write-Debug -Message ( + $script:localizedData.ArrayValueIsAbsent -f $_.InputObject + ) + } + else + { + Write-Debug -Message ( + $script:localizedData.ArrayValueIsPresent -f $_.InputObject + ) + } + } + + $returnValue = $false + } + else + { + $returnValue = $true + } + } + elseif ($Values.CurrentValue -ne $Values.DesiredValue) + { + $desiredType = $Values.DesiredValue.GetType() + + $returnValue = $false + + $supportedTypes = @( + 'String' + 'Int32' + 'UInt32' + 'Int16' + 'UInt16' + 'Single' + 'Boolean' + ) + + if ($desiredType.Name -notin $supportedTypes) + { + Write-Warning -Message ($script:localizedData.UnableToCompareType -f $desiredType.Name) + } + else + { + Write-Debug -Message ( + $script:localizedData.PropertyValueOfTypeDoesNotMatch ` + -f $desiredType.Name, $Values.CurrentValue, $Values.DesiredValue + ) + } + } + else + { + $returnValue = $true + } + + return $returnValue +} +#EndRegion './Private/Test-DscPropertyState.ps1' 247 +#Region './Public/Assert-BoundParameter.ps1' 0 +<# + .SYNOPSIS + Throws an error if there is a bound parameter that exists in both the + mutually exclusive lists. + + .DESCRIPTION + Throws an error if there is a bound parameter that exists in both the + mutually exclusive lists. + + .PARAMETER BoundParameterList + The parameters that should be evaluated against the mutually exclusive + lists MutuallyExclusiveList1 and MutuallyExclusiveList2. This parameter is + normally set to the $PSBoundParameters variable. + + .PARAMETER MutuallyExclusiveList1 + An array of parameter names that are not allowed to be bound at the + same time as those in MutuallyExclusiveList2. + + .PARAMETER MutuallyExclusiveList2 + An array of parameter names that are not allowed to be bound at the + same time as those in MutuallyExclusiveList1. + + .EXAMPLE + $assertBoundParameterParameters = @{ + BoundParameterList = $PSBoundParameters + MutuallyExclusiveList1 = @( + 'Parameter1' + ) + MutuallyExclusiveList2 = @( + 'Parameter2' + ) + } + + Assert-BoundParameter @assertBoundParameterParameters + + This example throws an exception if `$PSBoundParameters` contains both + the parameters `Parameter1` and `Parameter2`. +#> +function Assert-BoundParameter +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [AllowEmptyCollection()] + [System.Collections.Hashtable] + $BoundParameterList, + + [Parameter(Mandatory = $true)] + [System.String[]] + $MutuallyExclusiveList1, + + [Parameter(Mandatory = $true)] + [System.String[]] + $MutuallyExclusiveList2 + ) + + $itemFoundFromList1 = $BoundParameterList.Keys.Where({ $_ -in $MutuallyExclusiveList1 }) + $itemFoundFromList2 = $BoundParameterList.Keys.Where({ $_ -in $MutuallyExclusiveList2 }) + + if ($itemFoundFromList1.Count -gt 0 -and $itemFoundFromList2.Count -gt 0) + { + $errorMessage = ` + $script:localizedData.ParameterUsageWrong ` + -f ($MutuallyExclusiveList1 -join "','"), ($MutuallyExclusiveList2 -join "','") + + New-InvalidArgumentException -ArgumentName 'Parameters' -Message $errorMessage + } +} +#EndRegion './Public/Assert-BoundParameter.ps1' 70 +#Region './Public/Assert-IPAddress.ps1' 0 +<# + .SYNOPSIS + Asserts that the specified IP address is valid. + + .DESCRIPTION + Checks the IP address so that it is valid and do not conflict with address + family. If any problems are detected an exception will be thrown. + + .PARAMETER AddressFamily + IP address family that the supplied Address should be in. Valid values are + 'IPv4' or 'IPv6'. + + .PARAMETER Address + Specifies an IPv4 or IPv6 address. + + .EXAMPLE + Assert-IPAddress -Address '127.0.0.1' + + This will assert that the supplied address is a valid IPv4 address. + If it is not an exception will be thrown. + + .EXAMPLE + Assert-IPAddress -Address 'fe80:ab04:30F5:002b::1' + + This will assert that the supplied address is a valid IPv6 address. + If it is not an exception will be thrown. + + .EXAMPLE + Assert-IPAddress -Address 'fe80:ab04:30F5:002b::1' -AddressFamily 'IPv6' + + This will assert that address is valid and that it matches the + supplied address family. If the supplied address family does not match + the address an exception will be thrown. +#> +function Assert-IPAddress +{ + [CmdletBinding()] + param + ( + [Parameter()] + [ValidateSet('IPv4', 'IPv6')] + [System.String] + $AddressFamily, + + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $Address + ) + + [System.Net.IPAddress] $ipAddress = $null + + if (-not ([System.Net.IPAddress]::TryParse($Address, [ref] $ipAddress))) + { + New-InvalidArgumentException ` + -Message ($script:localizedData.AddressFormatError -f $Address) ` + -ArgumentName 'Address' + } + + if ($AddressFamily) + { + switch ($AddressFamily) + { + 'IPv4' + { + if ($ipAddress.AddressFamily -ne [System.Net.Sockets.AddressFamily]::InterNetwork.ToString()) + { + New-InvalidArgumentException ` + -Message ($script:localizedData.AddressIPv6MismatchError -f $Address, $AddressFamily) ` + -ArgumentName 'AddressFamily' + } + } + + 'IPv6' + { + if ($ipAddress.AddressFamily -ne [System.Net.Sockets.AddressFamily]::InterNetworkV6.ToString()) + { + New-InvalidArgumentException ` + -Message ($script:localizedData.AddressIPv4MismatchError -f $Address, $AddressFamily) ` + -ArgumentName 'AddressFamily' + } + } + } + } +} +#EndRegion './Public/Assert-IPAddress.ps1' 86 +#Region './Public/Assert-Module.ps1' 0 +<# + .SYNOPSIS + Assert if the specific module is available to be imported. + + .DESCRIPTION + Assert if the specific module is available to be imported. + + .PARAMETER ModuleName + Specifies the name of the module to assert. + + .PARAMETER ImportModule + Specifies to import the module if it is asserted. + + .PARAMETER Force + Specifies to forcibly import the module even if it is already in the + session. This parameter is ignored unless parameter `ImportModule` is + also used. + + .EXAMPLE + Assert-Module -ModuleName 'DhcpServer' + + This asserts that the module DhcpServer is available on the system. + + .EXAMPLE + Assert-Module -ModuleName 'DhcpServer' -ImportModule + + This asserts that the module DhcpServer is available on the system and + imports it. + + .EXAMPLE + Assert-Module -ModuleName 'DhcpServer' -ImportModule -Force + + This asserts that the module DhcpServer is available on the system and + forcibly imports it. +#> +function Assert-Module +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $ModuleName, + + [Parameter()] + [System.Management.Automation.SwitchParameter] + $ImportModule, + + [Parameter()] + [System.Management.Automation.SwitchParameter] + $Force + ) + + <# + If the module is already in the session there is no need to use -ListAvailable. + This is a fix for issue #66. + #> + if (-not (Get-Module -Name $ModuleName)) + { + if (-not (Get-Module -Name $ModuleName -ListAvailable)) + { + $errorMessage = $script:localizedData.ModuleNotFound -f $ModuleName + New-ObjectNotFoundException -Message $errorMessage + } + + # Only import it here if $Force is not set, otherwise it will be imported below. + if ($ImportModule -and -not $Force) + { + Import-Module -Name $ModuleName + } + } + + # Always import the module even if already in session. + if ($ImportModule -and $Force) + { + Import-Module -Name $ModuleName -Force + } +} +#EndRegion './Public/Assert-Module.ps1' 79 +#Region './Public/Compare-DscParameterState.ps1' 0 +<# + .SYNOPSIS + This method is used to compare current and desired values for any DSC resource. + + .DESCRIPTION + This function compare the parameter status of DSC resource parameters against + the current values present on the system, and return a hashtable with the metadata + from the comparison. + + .PARAMETER CurrentValues + A hashtable with the current values on the system, obtained by e.g. + Get-TargetResource. + + .PARAMETER DesiredValues + The hashtable of desired values. For example $PSBoundParameters with the + desired values. + + .PARAMETER Properties + This is a list of properties in the desired values list should be checked. + If this is empty then all values in DesiredValues are checked. + + .PARAMETER ExcludeProperties + This is a list of which properties in the desired values list should be checked. + If this is empty then all values in DesiredValues are checked. + + .PARAMETER TurnOffTypeChecking + Indicates that the type of the parameter should not be checked. + + .PARAMETER ReverseCheck + Indicates that a reverse check should be done. The current and desired state + are swapped for another test. + + .PARAMETER SortArrayValues + If the sorting of array values does not matter, values are sorted internally + before doing the comparison. + + .PARAMETER IncludeInDesiredState + Indicates that result adds the properties in the desired state. + By default, this command returns only the properties not in desired state. + + .PARAMETER IncludeValue + Indicates that result contains the ActualValue and ExcpectedValue properties. + + .EXAMPLE + $currentValues = @{ + String = 'This is a string' + Int = 1 + Bool = $true + } + + $desiredValues = @{ + String = 'This is a string' + Int = 99 + } + + Compare-DscParameterState -CurrentValues $currentValues -DesiredValues $desiredValues + + Name Value + ---- ----- + Property Int + InDesiredState False + ExpectedType System.Int32 + ActualType System.Int32 + ``` + + The function Compare-DscParameterState compare the value of each hashtable based + on the keys present in $desiredValues hashtable. The result indicates that Int + property is not in the desired state. + No information about Bool property, because it is not in $desiredValues hashtable. + + .EXAMPLE + $currentValues = @{ + String = 'This is a string' + Int = 1 + Bool = $true + } + + $desiredValues = @{ + String = 'This is a string' + Int = 99 + Bool = $false + } + + $excludeProperties = @('Bool') + + Compare-DscParameterState ` + -CurrentValues $currentValues ` + -DesiredValues $desiredValues ` + -ExcludeProperties $ExcludeProperties + + Name Value + ---- ----- + Property Int + InDesiredState False + ExpectedType System.Int32 + ActualType System.Int32 + ``` + + The function Compare-DscParameterState compare the value of each hashtable based + on the keys present in $desiredValues hashtable and without those in $excludeProperties. + The result indicates that Int property is not in the desired state. + No information about Bool property, because it is in $excludeProperties. + + .EXAMPLE + $serviceParameters = @{ + Name = $Name + } + + $returnValue = Compare-DscParameterState ` + -CurrentValues (Get-Service @serviceParameters) ` + -DesiredValues $PSBoundParameters ` + -Properties @( + 'Name' + 'Status' + 'StartType' + ) + + This compares the values in the current state against the desires state. + The command Get-Service is called using just the required parameters + to get the values in the current state. The parameter 'Properties' + is used to specify the properties 'Name','Status' and + 'StartType' for the comparison. + +#> +function Compare-DscParameterState +{ + [CmdletBinding()] + [OutputType([System.Object[]])] + param + ( + [Parameter(Mandatory = $true)] + [System.Object] + $CurrentValues, + + [Parameter(Mandatory = $true)] + [System.Object] + $DesiredValues, + + [Parameter()] + [System.String[]] + [Alias('ValuesToCheck')] + $Properties, + + [Parameter()] + [System.String[]] + $ExcludeProperties, + + [Parameter()] + [System.Management.Automation.SwitchParameter] + $TurnOffTypeChecking, + + [Parameter()] + [System.Management.Automation.SwitchParameter] + $ReverseCheck, + + [Parameter()] + [System.Management.Automation.SwitchParameter] + $SortArrayValues, + + [Parameter()] + [System.Management.Automation.SwitchParameter] + $IncludeInDesiredState, + + [Parameter()] + [System.Management.Automation.SwitchParameter] + $IncludeValue + ) + + $returnValue = @() + #region ConvertCIm to Hashtable + if ($CurrentValues -is [Microsoft.Management.Infrastructure.CimInstance] -or + $CurrentValues -is [Microsoft.Management.Infrastructure.CimInstance[]]) + { + $CurrentValues = ConvertTo-HashTable -CimInstance $CurrentValues + } + + if ($DesiredValues -is [Microsoft.Management.Infrastructure.CimInstance] -or + $DesiredValues -is [Microsoft.Management.Infrastructure.CimInstance[]]) + { + $DesiredValues = ConvertTo-HashTable -CimInstance $DesiredValues + } + #endregion Endofconverion + #region CheckType of object + $types = 'System.Management.Automation.PSBoundParametersDictionary', + 'System.Collections.Hashtable', + 'Microsoft.Management.Infrastructure.CimInstance' + + if ($DesiredValues.GetType().FullName -notin $types) + { + New-InvalidArgumentException ` + -Message ($script:localizedData.InvalidDesiredValuesError -f $DesiredValues.GetType().FullName) ` + -ArgumentName 'DesiredValues' + } + + if ($CurrentValues.GetType().FullName -notin $types) + { + New-InvalidArgumentException ` + -Message ($script:localizedData.InvalidCurrentValuesError -f $CurrentValues.GetType().FullName) ` + -ArgumentName 'CurrentValues' + } + #endregion checktype + #region check if CimInstance and not have properties in parameters invoke exception + if ($DesiredValues -is [Microsoft.Management.Infrastructure.CimInstance] -and -not $Properties) + { + New-InvalidArgumentException ` + -Message $script:localizedData.InvalidPropertiesError ` + -ArgumentName Properties + } + #endregion check cim and properties + #Clean value if there are a common parameters provide from Test/Get-TargetResource parameter + $desiredValuesClean = Remove-CommonParameter -Hashtable $DesiredValues + #region generate keyList based on $Properties and $excludeProperties value + if (-not $Properties) + { + $keyList = $desiredValuesClean.Keys + } + else + { + $keyList = $Properties + } + + if ($ExcludeProperties) + { + $keyList = $keyList | Where-Object -FilterScript { $_ -notin $ExcludeProperties } + } + #endregion + #region enumerate of each key in list + foreach ($key in $keyList) + { + #generate default value + $InDesiredStateTable = [ordered]@{ + Property = $key + InDesiredState = $true + } + $returnValue += $InDesiredStateTable + #get value of each key + $desiredValue = $desiredValuesClean.$key + $currentValue = $CurrentValues.$key + + #Check if IncludeValue parameter is used. + if ($IncludeValue) + { + $InDesiredStateTable['ExpectedValue'] = $desiredValue + $InDesiredStateTable['ActualValue'] = $currentValue + } + + #region convert to hashtable if value of key is CimInstance + if ($desiredValue -is [Microsoft.Management.Infrastructure.CimInstance] -or + $desiredValue -is [Microsoft.Management.Infrastructure.CimInstance[]]) + { + $desiredValue = ConvertTo-HashTable -CimInstance $desiredValue + } + if ($currentValue -is [Microsoft.Management.Infrastructure.CimInstance] -or + $currentValue -is [Microsoft.Management.Infrastructure.CimInstance[]]) + { + $currentValue = ConvertTo-HashTable -CimInstance $currentValue + } + #endregion converttohashtable + #region gettype of value to check if they are the same. + if ($null -ne $desiredValue) + { + $desiredType = $desiredValue.GetType() + } + else + { + $desiredType = @{ + Name = 'Unknown' + } + } + + $InDesiredStateTable['ExpectedType'] = $desiredType + + if ($null -ne $currentValue) + { + $currentType = $currentValue.GetType() + } + else + { + $currentType = @{ + Name = 'Unknown' + } + } + + $InDesiredStateTable['ActualType'] = $currentType + + #endregion + #region check if the desiredtype if a credential object. Only if the current type isn't unknown. + if ($currentType.Name -ne 'Unknown' -and $desiredType.Name -eq 'PSCredential') + { + # This is a credential object. Compare only the user name + if ($currentType.Name -eq 'PSCredential' -and $currentValue.UserName -eq $desiredValue.UserName) + { + Write-Verbose -Message ($script:localizedData.MatchPsCredentialUsernameMessage -f $currentValue.UserName, $desiredValue.UserName) + continue # pass to the next key + } + elseif ($currentType.Name -ne 'string') + { + Write-Verbose -Message ($script:localizedData.NoMatchPsCredentialUsernameMessage -f $currentValue.UserName, $desiredValue.UserName) + $InDesiredStateTable.InDesiredState = $false + } + + # Assume the string is our username when the matching desired value is actually a credential + if ($currentType.Name -eq 'string' -and $currentValue -eq $desiredValue.UserName) + { + Write-Verbose -Message ($script:localizedData.MatchPsCredentialUsernameMessage -f $currentValue, $desiredValue.UserName) + continue # pass to the next key + } + else + { + Write-Verbose -Message ($script:localizedData.NoMatchPsCredentialUsernameMessage -f $currentValue, $desiredValue.UserName) + $InDesiredStateTable.InDesiredState = $false + } + } + #endregion test credential + #region Test type of object. And if they're not InDesiredState, generate en exception + if (-not $TurnOffTypeChecking) + { + if (($desiredType.Name -ne 'Unknown' -and $currentType.Name -ne 'Unknown') -and + $desiredType.FullName -ne $currentType.FullName) + { + Write-Verbose -Message ($script:localizedData.NoMatchTypeMismatchMessage -f $key, $currentType.FullName, $desiredType.FullName) + $InDesiredStateTable.InDesiredState = $false + continue # pass to the next key + } + } + #endregion TestType + #region Check if the value of Current and desired state is the same but only if they are not an array + if ($currentValue -eq $desiredValue -and -not $desiredType.IsArray) + { + Write-Verbose -Message ($script:localizedData.MatchValueMessage -f $desiredType.FullName, $key, $currentValue, $desiredValue) + continue # pass to the next key + } + #endregion check same value + #region Check if the DesiredValuesClean has the key and if it don't have, it's not necessary to check his value + if ($desiredValuesClean.GetType().Name -in 'HashTable', 'PSBoundParametersDictionary') + { + $checkDesiredValue = $desiredValuesClean.ContainsKey($key) + } + else + { + $checkDesiredValue = Test-DscObjectHasProperty -Object $desiredValuesClean -PropertyName $key + } + # if there no key, don't need to check + if (-not $checkDesiredValue) + { + Write-Verbose -Message ($script:localizedData.MatchValueMessage -f $desiredType.FullName, $key, $currentValue, $desiredValue) + continue # pass to the next key + } + #endregion + #region Check if desired type is array, ifno Hashtable and currenttype hashtable to + if ($desiredType.IsArray) + { + Write-Verbose -Message ($script:localizedData.TestDscParameterCompareMessage -f $key, $desiredType.FullName) + # Check if the currentValues and desiredValue are empty array. + if (-not $currentValue -and -not $desiredValue) + { + Write-Verbose -Message ($script:localizedData.MatchValueMessage -f $desiredType.FullName, $key, 'empty array', 'empty array') + continue + } + elseif (-not $currentValue) + { + #If only currentvalue is empty, the configuration isn't compliant. + Write-Verbose -Message ($script:localizedData.NoMatchValueMessage -f $desiredType.FullName, $key, $currentValue, $desiredValue) + $InDesiredStateTable.InDesiredState = $false + continue + } + elseif ($currentValue.Count -ne $desiredValue.Count) + { + #If there is a difference between the number of objects in arrays, this isn't compliant. + Write-Verbose -Message ($script:localizedData.NoMatchValueDifferentCountMessage -f $desiredType.FullName, $key, $currentValue.Count, $desiredValue.Count) + $InDesiredStateTable.InDesiredState = $false + continue + } + else + { + $desiredArrayValues = $desiredValue + $currentArrayValues = $currentValue + # if the sortArrayValues parameter is using, sort value of array + if ($SortArrayValues) + { + $desiredArrayValues = @($desiredArrayValues | Sort-Object) + $currentArrayValues = @($currentArrayValues | Sort-Object) + } + <# + for all object in collection, check their type.ConvertoString if they are script block. + + #> + for ($i = 0; $i -lt $desiredArrayValues.Count; $i++) + { + if ($desiredArrayValues[$i]) + { + $desiredType = $desiredArrayValues[$i].GetType() + } + else + { + $desiredType = @{ + Name = 'Unknown' + } + } + + if ($currentArrayValues[$i]) + { + $currentType = $currentArrayValues[$i].GetType() + } + else + { + $currentType = @{ + Name = 'Unknown' + } + } + + if (-not $TurnOffTypeChecking) + { + if (($desiredType.Name -ne 'Unknown' -and $currentType.Name -ne 'Unknown') -and + $desiredType.FullName -ne $currentType.FullName) + { + Write-Verbose -Message ($script:localizedData.NoMatchElementTypeMismatchMessage -f $key, $i, $currentType.FullName, $desiredType.FullName) + $InDesiredStateTable.InDesiredState = $false + continue + } + } + + <# + Convert a scriptblock into a string as scriptblocks are not comparable + if currentvalue is scriptblock and if desired value is string, + we invoke the result of script block. Ifno, we convert to string. + if Desired value + #> + + $wasCurrentArrayValuesConverted = $false + if ($currentArrayValues[$i] -is [scriptblock]) + { + $currentArrayValues[$i] = if ($desiredArrayValues[$i] -is [string]) + { + $currentArrayValues[$i] = $currentArrayValues[$i].Invoke() + } + else + { + $currentArrayValues[$i].ToString() + } + $wasCurrentArrayValuesConverted = $true + } + + if ($desiredArrayValues[$i] -is [scriptblock]) + { + $desiredArrayValues[$i] = if ($currentArrayValues[$i] -is [string] -and -not $wasCurrentArrayValuesConverted) + { + $desiredArrayValues[$i].Invoke() + } + else + { + $desiredArrayValues[$i].ToString() + } + } + + if ($desiredType -eq [System.Collections.Hashtable] -and $currentType -eq [System.Collections.Hashtable]) + { + $param = $PSBoundParameters + $param.CurrentValues = $currentArrayValues[$i] + $param.DesiredValues = $desiredArrayValues[$i] + + 'IncludeInDesiredState','IncludeValue' | ForEach-Object { + if ($param.ContainsKey($_)) + { + $null = $param.Remove($_) + } + } + + if ($InDesiredStateTable.InDesiredState) + { + $InDesiredStateTable.InDesiredState = Test-DscParameterState @param + } + else + { + Test-DscParameterState @param | Out-Null + } + continue + } + + if ($desiredArrayValues[$i] -ne $currentArrayValues[$i]) + { + Write-Verbose -Message ($script:localizedData.NoMatchElementValueMismatchMessage -f $i, $desiredType.FullName, $key, $currentArrayValues[$i], $desiredArrayValues[$i]) + $InDesiredStateTable.InDesiredState = $false + continue + } + else + { + Write-Verbose -Message ($script:localizedData.MatchElementValueMessage -f $i, $desiredType.FullName, $key, $currentArrayValues[$i], $desiredArrayValues[$i]) + continue + } + } + + } + } + elseif ($desiredType -eq [System.Collections.Hashtable] -and $currentType -eq [System.Collections.Hashtable]) + { + $param = $PSBoundParameters + $param.CurrentValues = $currentValue + $param.DesiredValues = $desiredValue + + 'IncludeInDesiredState','IncludeValue' | ForEach-Object { + if ($param.ContainsKey($_)) + { + $null = $param.Remove($_) + } + } + + if ($InDesiredStateTable.InDesiredState) + { + $InDesiredStateTable.InDesiredState = Test-DscParameterState @param + } + else + { + Test-DscParameterState @param | Out-Null + } + continue + } + else + { + #Convert a scriptblock into a string as scriptblocks are not comparable + $wasCurrentValue = $false + if ($currentValue -is [scriptblock]) + { + $currentValue = if ($desiredValue -is [string]) + { + $currentValue = $currentValue.Invoke() + } + else + { + $currentValue.ToString() + } + $wasCurrentValue = $true + } + if ($desiredValue -is [scriptblock]) + { + $desiredValue = if ($currentValue -is [string] -and -not $wasCurrentValue) + { + $desiredValue.Invoke() + } + else + { + $desiredValue.ToString() + } + } + + if ($desiredValue -ne $currentValue) + { + Write-Verbose -Message ($script:localizedData.NoMatchValueMessage -f $desiredType.FullName, $key, $currentValue, $desiredValue) + $InDesiredStateTable.InDesiredState = $false + } + } + #endregion check type + } + #endregion end of enumeration + if ($ReverseCheck) + { + Write-Verbose -Message $script:localizedData.StartingReverseCheck + $reverseCheckParameters = $PSBoundParameters + $reverseCheckParameters.CurrentValues = $DesiredValues + $reverseCheckParameters.DesiredValues = $CurrentValues + $null = $reverseCheckParameters.Remove('ReverseCheck') + + if ($returnValue) + { + $returnValue = Compare-DscParameterState @reverseCheckParameters + } + else + { + $null = Compare-DscParameterState @reverseCheckParameters + } + } + + # Remove in desired state value if IncludeDesirateState parameter is not use + if (-not $IncludeInDesiredState) + { + [array]$returnValue = $returnValue.where({$_.InDesiredState -eq $false}) + } + + #change verbose message + if ($IncludeInDesiredState.IsPresent) + { + $returnValue.ForEach({ + if ($_.InDesiredState) + { + $localizedString = $script:localizedData.PropertyInDesiredStateMessage + } + else + { + $localizedString = $script:localizedData.PropertyNotInDesiredStateMessage + } + + Write-Verbose -Message ($localizedString -f $_.Property) + }) + } + <# + If Compare-DscParameterState is used in precedent step, don't need to convert it + We use .foreach() method as we are sure that $returnValue is an array. + #> + [Array]$returnValue = @( + $returnValue.foreach( + { + if ($_ -is [System.Collections.Hashtable]) + { + [pscustomobject]$_ + } + else + { + $_ + } + } + ) + ) + + return $returnValue +} +#EndRegion './Public/Compare-DscParameterState.ps1' 616 +#Region './Public/Compare-ResourcePropertyState.ps1' 0 +<# + .SYNOPSIS + Compare current and desired property values for any DSC resource. + + .DESCRIPTION + This function is used to compare current and desired property values for any + DSC resource, and return a hashtable with the metadata from the comparison. + + .PARAMETER CurrentValues + The current values that should be compared to to desired values. Normally + the values returned from Get-TargetResource. + + .PARAMETER DesiredValues + The values set in the configuration and is provided in the call to the + functions *-TargetResource, and that will be compared against current + values. Normally set to $PSBoundParameters. + + .PARAMETER Properties + An array of property names, from the keys provided in DesiredValues, that + will be compared. If this parameter is left out, all the keys in the + DesiredValues will be compared. + + .PARAMETER IgnoreProperties + An array of property names, from the keys provided in DesiredValues, that + will be ignored in the comparison. If this parameter is left out, all the + keys in the DesiredValues will be compared. + + .PARAMETER CimInstanceKeyProperties + A hashtable containing a key for each property that contain a collection + of CimInstances and the value is an array of strings of the CimInstance + key properties. + @{ + Permission = @('State') + } + + .EXAMPLE + $compareTargetResourceStateParameters = @{ + CurrentValues = (Get-TargetResource $PSBoundParameters) + DesiredValues = $PSBoundParameters + } + + $propertyState = Compare-ResourcePropertyState @compareTargetResourceStateParameters + + This examples call Compare-ResourcePropertyState with the current state + and the desired state and returns a hashtable array of all the properties + that was evaluated based on the properties pass in the parameter DesiredValues. +#> +function Compare-ResourcePropertyState +{ + [CmdletBinding()] + [OutputType([System.Collections.Hashtable[]])] + param + ( + [Parameter(Mandatory = $true)] + [System.Collections.Hashtable] + $CurrentValues, + + [Parameter(Mandatory = $true)] + [System.Collections.Hashtable] + $DesiredValues, + + [Parameter()] + [ValidateNotNullOrEmpty()] + [System.String[]] + $Properties, + + [Parameter()] + [ValidateNotNullOrEmpty()] + [System.String[]] + $IgnoreProperties, + + [Parameter()] + [ValidateNotNull()] + [System.Collections.Hashtable] + $CimInstanceKeyProperties = @{} + ) + + if ($PSBoundParameters.ContainsKey('Properties')) + { + # Filter out the parameters (keys) not specified in Properties + $desiredValuesToRemove = $DesiredValues.Keys | + Where-Object -FilterScript { + $_ -notin $Properties + } + + $desiredValuesToRemove | + ForEach-Object -Process { + $DesiredValues.Remove($_) + } + } + else + { + <# + Remove any common parameters that might be part of DesiredValues, + if it $PSBoundParameters was used to pass the desired values. + #> + $commonParametersToRemove = $DesiredValues.Keys | + Where-Object -FilterScript { + $_ -in [System.Management.Automation.PSCmdlet]::CommonParameters ` + -or $_ -in [System.Management.Automation.PSCmdlet]::OptionalCommonParameters + } + + $commonParametersToRemove | + ForEach-Object -Process { + $DesiredValues.Remove($_) + } + } + + # Remove any properties that should be ignored. + if ($PSBoundParameters.ContainsKey('IgnoreProperties')) + { + $IgnoreProperties | + ForEach-Object -Process { + if ($DesiredValues.ContainsKey($_)) + { + $DesiredValues.Remove($_) + } + } + } + + $compareTargetResourceStateReturnValue = @() + + foreach ($parameterName in $DesiredValues.Keys) + { + Write-Debug -Message ($script:localizedData.EvaluatePropertyState -f $parameterName) + + $parameterState = @{ + ParameterName = $parameterName + Expected = $DesiredValues.$parameterName + Actual = $CurrentValues.$parameterName + } + + # Check if the parameter is in compliance. + $isPropertyInDesiredState = Test-DscPropertyState -Values @{ + CurrentValue = $CurrentValues.$parameterName + DesiredValue = $DesiredValues.$parameterName + KeyProperties = $CimInstanceKeyProperties.$parameterName + } + + if ($isPropertyInDesiredState) + { + Write-Verbose -Message ($script:localizedData.PropertyInDesiredState -f $parameterName) + + $parameterState['InDesiredState'] = $true + } + else + { + Write-Verbose -Message ($script:localizedData.PropertyNotInDesiredState -f $parameterName) + + $parameterState['InDesiredState'] = $false + } + + $compareTargetResourceStateReturnValue += $parameterState + } + + return $compareTargetResourceStateReturnValue +} +#EndRegion './Public/Compare-ResourcePropertyState.ps1' 158 +#Region './Public/ConvertTo-CimInstance.ps1' 0 +<# + .SYNOPSIS + Converts a hashtable into a CimInstance array. + + .DESCRIPTION + This function is used to convert a hashtable into MSFT_KeyValuePair objects. + These are stored as an CimInstance array. DSC cannot handle hashtables but + CimInstances arrays storing MSFT_KeyValuePair. + + .PARAMETER Hashtable + A hashtable with the values to convert. + + .OUTPUTS + An object array with CimInstance objects. + + .EXAMPLE + ConvertTo-CimInstance -Hashtable @{ + String = 'a string' + Bool = $true + Int = 99 + Array = 'a, b, c' + } + + This example returns an CimInstance with the provided hashtable values. +#> +function ConvertTo-CimInstance +{ + [CmdletBinding()] + [OutputType([System.Object[]])] + param + ( + [Parameter(Mandatory = $true, ValueFromPipeline = $true, ParameterSetName = 'Hashtable')] + [System.Collections.Hashtable] + $Hashtable + ) + + process + { + foreach ($item in $Hashtable.GetEnumerator()) + { + New-CimInstance -ClassName 'MSFT_KeyValuePair' -Namespace 'root/microsoft/Windows/DesiredStateConfiguration' -Property @{ + Key = $item.Key + Value = if ($item.Value -is [array]) + { + $item.Value -join ',' + } + else + { + $item.Value + } + } -ClientOnly + } + } +} +#EndRegion './Public/ConvertTo-CimInstance.ps1' 55 +#Region './Public/ConvertTo-HashTable.ps1' 0 +<# + .SYNOPSIS + Converts CimInstances into a hashtable. + + .DESCRIPTION + This function is used to convert a CimInstance array containing + MSFT_KeyValuePair objects into a hashtable. + + .PARAMETER CimInstance + An array of CimInstances or a single CimInstance object to convert. + + .OUTPUTS + Hashtable + + .EXAMPLE + $newInstanceParameters = @{ + ClassName = 'MSFT_KeyValuePair' + Namespace = 'root/microsoft/Windows/DesiredStateConfiguration' + ClientOnly = $true + } + + $cimInstance = [Microsoft.Management.Infrastructure.CimInstance[]] ( + (New-CimInstance @newInstanceParameters -Property @{ + Key = 'FirstName' + Value = 'John' + }), + + (New-CimInstance @newInstanceParameters -Property @{ + Key = 'LastName' + Value = 'Smith' + }) + ) + + ConvertTo-HashTable -CimInstance $cimInstance + + This creates a array om CimInstances of the class name MSFT_KeyValuePair + and passes it to ConvertTo-HashTable which returns a hashtable. +#> +function ConvertTo-HashTable +{ + [CmdletBinding()] + [OutputType([System.Collections.Hashtable])] + param + ( + [Parameter(Mandatory = $true, ValueFromPipeline = $true, ParameterSetName = 'CimInstance')] + [AllowEmptyCollection()] + [Microsoft.Management.Infrastructure.CimInstance[]] + $CimInstance + ) + + begin + { + $result = @{ } + } + + process + { + foreach ($ci in $CimInstance) + { + $result.Add($ci.Key, $ci.Value) + } + } + + end + { + $result + } +} +#EndRegion './Public/ConvertTo-HashTable.ps1' 69 +#Region './Public/Get-ComputerName.ps1' 0 +<# + .SYNOPSIS + Returns the computer name cross-plattform. + + .DESCRIPTION + The variable `$env:COMPUTERNAME` does not exist cross-platform which + hinders development and testing on macOS and Linux. Instead this cmdlet + can be used to get the computer name cross-plattform. + + .EXAMPLE + Get-ComputerName + + This example returns the computer name cross-plattform. +#> +function Get-ComputerName +{ + [CmdletBinding()] + [OutputType([System.String])] + param () + + $computerName = $null + + if ($IsLinux -or $IsMacOs) + { + $computerName = hostname + } + else + { + <# + We could run 'hostname' on Windows too, but $env:COMPUTERNAME + is more widely used. + #> + $computerName = $env:COMPUTERNAME + } + + return $computerName +} +#EndRegion './Public/Get-ComputerName.ps1' 38 +#Region './Public/Get-LocalizedData.ps1' 0 +<# + .SYNOPSIS + Gets language-specific data into scripts and functions based on the UI culture + that is selected for the operating system. + Similar to Import-LocalizedData, with extra parameter 'DefaultUICulture'. + + .DESCRIPTION + The Get-LocalizedData cmdlet dynamically retrieves strings from a subdirectory + whose name matches the UI language set for the current user of the operating system. + It is designed to enable scripts to display user messages in the UI language selected + by the current user. + + Get-LocalizedData imports data from .psd1 files in language-specific subdirectories + of the script directory and saves them in a local variable that is specified in the + command. The cmdlet selects the subdirectory and file based on the value of the + $PSUICulture automatic variable. When you use the local variable in the script to + display a user message, the message appears in the user's UI language. + + You can use the parameters of G-LocalizedData to specify an alternate UI culture, + path, and file name, to add supported commands, and to suppress the error message that + appears if the .psd1 files are not found. + + The G-LocalizedData cmdlet supports the script internationalization + initiative that was introduced in Windows PowerShell 2.0. This initiative + aims to better serve users worldwide by making it easy for scripts to display + user messages in the UI language of the current user. For more information + about this and about the format of the .psd1 files, see about_Script_Internationalization. + + .PARAMETER BindingVariable + Specifies the variable into which the text strings are imported. Enter a variable + name without a dollar sign ($). + + In Windows PowerShell 2.0, this parameter is required. In Windows PowerShell 3.0, + this parameter is optional. If you omit this parameter, Import-LocalizedData + returns a hash table of the text strings. The hash table is passed down the pipeline + or displayed at the command line. + + When using Import-LocalizedData to replace default text strings specified in the + DATA section of a script, assign the DATA section to a variable and enter the name + of the DATA section variable in the value of the BindingVariable parameter. Then, + when Import-LocalizedData saves the imported content in the BindingVariable, the + imported data will replace the default text strings. If you are not specifying + default text strings, you can select any variable name. + + .PARAMETER UICulture + Specifies an alternate UI culture. The default is the value of the $PsUICulture + automatic variable. Enter a UI culture in - format, such as + en-US, de-DE, or ar-SA. + + The value of the UICulture parameter determines the language-specific subdirectory + (within the base directory) from which Import-LocalizedData gets the .psd1 file + for the script. + + The cmdlet searches for a subdirectory with the same name as the value of the + UICulture parameter or the $PsUICulture automatic variable, such as de-DE or + ar-SA. If it cannot find the directory, or the directory does not contain a .psd1 + file for the script, it searches for a subdirectory with the name of the language + code, such as de or ar. If it cannot find the subdirectory or .psd1 file, the + command fails and the data is displayed in the default language specified in the + script. + + .PARAMETER BaseDirectory + Specifies the base directory where the .psd1 files are located. The default is + the directory where the script is located. Import-LocalizedData searches for + the .psd1 file for the script in a language-specific subdirectory of the base + directory. + + .PARAMETER FileName + Specifies the name of the data file (.psd1) to be imported. Enter a file name. + You can specify a file name that does not include its .psd1 file name extension, + or you can specify the file name including the .psd1 file name extension. + + The FileName parameter is required when Import-LocalizedData is not used in a + script. Otherwise, the parameter is optional and the default value is the base + name of the script. You can use this parameter to direct Import-LocalizedData + to search for a different .psd1 file. + + For example, if the FileName is omitted and the script name is FindFiles.ps1, + Import-LocalizedData searches for the FindFiles.psd1 data file. + + .PARAMETER SupportedCommand + Specifies cmdlets and functions that generate only data. + + Use this parameter to include cmdlets and functions that you have written or + tested. For more information, see about_Script_Internationalization. + + .PARAMETER DefaultUICulture + Specifies which UICulture to default to if current UI culture or its parents + culture don't have matching data file. + + For example, if you have a data file in 'en-US' but not in 'en' or 'en-GB' and + your current culture is 'en-GB', you can default back to 'en-US'. + + .NOTES + Before using Import-LocalizedData, localize your user messages. Format the messages + for each locale (UI culture) in a hash table of key/value pairs, and save the + hash table in a file with the same name as the script and a .psd1 file name extension. + Create a directory under the script directory for each supported UI culture, and + then save the .psd1 file for each UI culture in the directory with the UI + culture name. + + For example, localize your user messages for the de-DE locale and format them in + a hash table. Save the hash table in a .psd1 file. Then create a de-DE + subdirectory under the script directory, and save the de-DE .psd1 + file in the de-DE subdirectory. Repeat this method for each locale that you support. + + Import-LocalizedData performs a structured search for the localized user + messages for a script. + + Import-LocalizedData begins the search in the directory where the script file + is located (or the value of the BaseDirectory parameter). It then searches within + the base directory for a subdirectory with the same name as the value of the + $PsUICulture variable (or the value of the UICulture parameter), such as de-DE or + ar-SA. Then, it searches in that subdirectory for a .psd1 file with the same name + as the script (or the value of the FileName parameter). + + If Import-LocalizedData cannot find a subdirectory with the name of the UI culture, + or the subdirectory does not contain a .psd1 file for the script, it searches for + a .psd1 file for the script in a subdirectory with the name of the language code, + such as de or ar. If it cannot find the subdirectory or .psd1 file, the command + fails, the data is displayed in the default language in the script, and an error + message is displayed explaining that the data could not be imported. To suppress + the message and fail gracefully, use the ErrorAction common parameter with a value + of SilentlyContinue. + + If Import-LocalizedData finds the subdirectory and the .psd1 file, it imports the + hash table of user messages into the value of the BindingVariable parameter in the + command. Then, when you display a message from the hash table in the variable, the + localized message is displayed. + + For more information, see about_Script_Internationalization. + + .EXAMPLE + $script:localizedData = Get-LocalizedData -DefaultUICulture 'en-US' + + This is an example that can be used in DSC resources to import the + localized strings and if the current UI culture localized folder does + not exist the UI culture 'en-US' is returned. +#> +function Get-LocalizedData +{ + [CmdletBinding(DefaultParameterSetName = 'DefaultUICulture')] + param + ( + [Parameter(Position = 0)] + [Alias('Variable')] + [ValidateNotNullOrEmpty()] + [System.String] + $BindingVariable, + + [Parameter(Position = 1, ParameterSetName = 'TargetedUICulture')] + [System.String] + $UICulture, + + [Parameter()] + [System.String] + $BaseDirectory, + + [Parameter()] + [System.String] + $FileName, + + [Parameter()] + [System.String[]] + $SupportedCommand, + + [Parameter(Position = 1, ParameterSetName = 'DefaultUICulture')] + [System.String] + $DefaultUICulture = 'en-US' + ) + + begin + { + <# + Because Proxy Command changes the Invocation origin, we need to be explicit + when handing the pipeline back to original command. + #> + if (!$PSBoundParameters.ContainsKey('FileName')) + { + if ($myInvocation.ScriptName) + { + $file = [System.IO.FileInfo] $myInvocation.ScriptName + } + else + { + $file = [System.IO.FileInfo] $myInvocation.MyCommand.Module.Path + } + + $FileName = $file.BaseName + + $PSBoundParameters.Add('FileName', $file.Name) + } + + if ($PSBoundParameters.ContainsKey('BaseDirectory')) + { + $callingScriptRoot = $BaseDirectory + } + else + { + $callingScriptRoot = $MyInvocation.PSScriptRoot + + $PSBoundParameters.Add('BaseDirectory', $callingScriptRoot) + } + + if ($PSBoundParameters.ContainsKey('DefaultUICulture') -and !$PSBoundParameters.ContainsKey('UICulture')) + { + <# + We don't want the resolution to eventually return the ModuleManifest + so we run the same GetFilePath() logic than here: + https://github.com/PowerShell/PowerShell/blob/master/src/Microsoft.PowerShell.Commands.Utility/commands/utility/Import-LocalizedData.cs#L302-L333 + and if we see it will return the wrong thing, set the UICulture to DefaultUI culture, and return the logic to Import-LocalizedData + #> + $currentCulture = Get-UICulture + + $evaluateDefaultCulture = $true + + <# + If the LCID is 127 then use default UI culture instead. + + See more information in issue https://github.com/dsccommunity/DscResource.Common/issues/11. + #> + if ($currentCulture.LCID -eq 127) + { + $currentCulture = New-Object -TypeName 'System.Globalization.CultureInfo' -ArgumentList @($DefaultUICulture) + $PSBoundParameters['UICulture'] = $DefaultUICulture + + $evaluateDefaultCulture = $false + } + + $languageFile = $null + + $localizedFileNames = @( + $FileName + '.psd1' + $FileName + '.strings.psd1' + ) + + while ($null -ne $currentCulture -and $currentCulture.Name -and -not $languageFile) + { + foreach ($fullFileName in $localizedFileNames) + { + $filePath = [System.IO.Path]::Combine($callingScriptRoot, $CurrentCulture.Name, $fullFileName) + + if (Test-Path -Path $filePath) + { + Write-Debug -Message "Found $filePath" + + $languageFile = $filePath + + # Set the filename to the file we found. + $PSBoundParameters['FileName'] = $fullFileName + + # Exit loop if we find the first filename. + break + } + else + { + Write-Debug -Message "File $filePath not found" + } + } + + if (-not $languageFile) + { + <# + Evaluate the parent culture if there is one. + + If the parent culture is LCID 127 then move to the default culture. + See more information in issue https://github.com/dsccommunity/DscResource.Common/issues/11. + #> + if ($currentCulture.Parent -and $currentCulture.Parent.LCID -ne 127) + { + $currentCulture = $currentCulture.Parent + } + else + { + if ($evaluateDefaultCulture) + { + $evaluateDefaultCulture = $false + + <# + Could not find localized strings file for the the operating + system UI culture. Evaluating the default UI culture (which + defaults to 'en-US' if not specifically set). + #> + $currentCulture = New-Object -TypeName 'System.Globalization.CultureInfo' -ArgumentList @($DefaultUICulture) + $PSBoundParameters['UICulture'] = $DefaultUICulture + } + else + { + <# + Already evaluated everything we could, exit and let + Import-LocalizedData throw an exception. + #> + break + } + } + } + } + + <# + Removes the parameter DefaultUICulture so that isn't used when + calling Import-LocalizedData. + #> + $null = $PSBoundParameters.Remove('DefaultUICulture') + } + + try + { + $outBuffer = $null + + if ($PSBoundParameters.TryGetValue('OutBuffer', [ref] $outBuffer)) + { + $PSBoundParameters['OutBuffer'] = 1 + } + + $wrappedCmd = $ExecutionContext.InvokeCommand.GetCommand('Microsoft.PowerShell.Utility\Import-LocalizedData', [System.Management.Automation.CommandTypes]::Cmdlet) + $scriptCmd = { & $wrappedCmd @PSBoundParameters } + + $steppablePipeline = $scriptCmd.GetSteppablePipeline($myInvocation.CommandOrigin) + $steppablePipeline.Begin($PSCmdlet) + } + catch + { + throw + } + } + + process + { + try + { + $steppablePipeline.Process($_) + } + catch + { + throw + } + } + + end + { + if ($BindingVariable -and ($valueToBind = Get-Variable -Name $BindingVariable -ValueOnly -ErrorAction 'Ignore')) + { + # Bringing the variable to the parent scope + Set-Variable -Scope 1 -Name $BindingVariable -Force -ErrorAction 'SilentlyContinue' -Value $valueToBind + } + + try + { + $steppablePipeline.End() + } + catch + { + throw + } + } +} +#EndRegion './Public/Get-LocalizedData.ps1' 357 +#Region './Public/Get-TemporaryFolder.ps1' 0 +<# + .SYNOPSIS + Returns the path of the current user's temporary folder. + + .DESCRIPTION + Returns the path of the current user's temporary folder. + + .NOTES + This is the same as doing the following + - Windows: $env:TEMP + - macOS: $env:TMPDIR + - Linux: /tmp/ + + .EXAMPLE + Get-TemporaryFolder + + Returns the current user temporary folder on the current operating system. +#> +function Get-TemporaryFolder +{ + [CmdletBinding()] + [OutputType([System.String])] + param () + + return [IO.Path]::GetTempPath() +} +#EndRegion './Public/Get-TemporaryFolder.ps1' 27 +#Region './Public/New-InvalidArgumentException.ps1' 0 +<# + .SYNOPSIS + Creates and throws an invalid argument exception. + + .DESCRIPTION + Creates and throws an invalid argument exception. + + .PARAMETER Message + The message explaining why this error is being thrown. + + .PARAMETER ArgumentName + The name of the invalid argument that is causing this error to be thrown. + + .EXAMPLE + $errorMessage = $script:localizedData.ActionCannotBeUsedInThisContextMessage ` + -f $Action, $Parameter + + New-InvalidArgumentException -ArgumentName 'Action' -Message $errorMessage +#> +function New-InvalidArgumentException +{ + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '')] + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $Message, + + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $ArgumentName + ) + + $argumentException = New-Object -TypeName 'ArgumentException' ` + -ArgumentList @($Message, $ArgumentName) + + $newObjectParameters = @{ + TypeName = 'System.Management.Automation.ErrorRecord' + ArgumentList = @($argumentException, $ArgumentName, 'InvalidArgument', $null) + } + + $errorRecord = New-Object @newObjectParameters + + throw $errorRecord +} +#EndRegion './Public/New-InvalidArgumentException.ps1' 49 +#Region './Public/New-InvalidDataException.ps1' 0 +<# + .SYNOPSIS + Creates and throws an invalid data exception. + + .DESCRIPTION + Creates and throws an invalid data exception. + + .PARAMETER ErrorId + The error Id to assign to the exception. + + .PARAMETER ErrorMessage + The error message to assign to the exception. + + .EXAMPLE + if ( -not $resultOfEvaluation ) + { + $errorMessage = $script:localizedData.InvalidData -f $Action + + New-InvalidDataException -ErrorId 'InvalidDataError' -ErrorMessage $errorMessage + } +#> +function New-InvalidDataException +{ + [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '')] + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $ErrorId, + + [Parameter(Mandatory = $true)] + [System.String] + $ErrorMessage + ) + + $errorCategory = [System.Management.Automation.ErrorCategory]::InvalidData + $exception = New-Object ` + -TypeName System.InvalidOperationException ` + -ArgumentList $ErrorMessage + $errorRecord = New-Object ` + -TypeName System.Management.Automation.ErrorRecord ` + -ArgumentList $exception, $ErrorId, $errorCategory, $null + + throw $errorRecord +} +#EndRegion './Public/New-InvalidDataException.ps1' 47 +#Region './Public/New-InvalidOperationException.ps1' 0 +<# + .SYNOPSIS + Creates and throws an invalid operation exception. + + .DESCRIPTION + Creates and throws an invalid operation exception. + + .PARAMETER Message + The message explaining why this error is being thrown. + + .PARAMETER ErrorRecord + The error record containing the exception that is causing this terminating error. + + .EXAMPLE + try + { + Start-Process @startProcessArguments + } + catch + { + $errorMessage = $script:localizedData.InstallationFailedMessage -f $Path, $processId + New-InvalidOperationException -Message $errorMessage -ErrorRecord $_ + } +#> +function New-InvalidOperationException +{ + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '')] + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $Message, + + [Parameter()] + [ValidateNotNull()] + [System.Management.Automation.ErrorRecord] + $ErrorRecord + ) + + if ($null -eq $ErrorRecord) + { + $invalidOperationException = New-Object -TypeName 'InvalidOperationException' ` + -ArgumentList @($Message) + } + else + { + $invalidOperationException = New-Object -TypeName 'InvalidOperationException' ` + -ArgumentList @($Message, $ErrorRecord.Exception) + } + + $newObjectParameters = @{ + TypeName = 'System.Management.Automation.ErrorRecord' + ArgumentList = @( + $invalidOperationException.ToString(), + 'MachineStateIncorrect', + 'InvalidOperation', + $null + ) + } + + $errorRecordToThrow = New-Object @newObjectParameters + + throw $errorRecordToThrow +} +#EndRegion './Public/New-InvalidOperationException.ps1' 67 +#Region './Public/New-InvalidResultException.ps1' 0 +<# + .SYNOPSIS + Creates and throws an invalid result exception. + + .DESCRIPTION + Creates and throws an invalid result exception. + + .PARAMETER Message + The message explaining why this error is being thrown. + + .PARAMETER ErrorRecord + The error record containing the exception that is causing this terminating error. + + .EXAMPLE + try + { + $numberOfObjects = Get-ChildItem -Path $path + if ($numberOfObjects -eq 0) + { + throw 'To few files.' + } + } + catch + { + $errorMessage = $script:localizedData.TooFewFilesMessage -f $path + New-InvalidResultException -Message $errorMessage -ErrorRecord $_ + } +#> +function New-InvalidResultException +{ + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '')] + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $Message, + + [Parameter()] + [ValidateNotNull()] + [System.Management.Automation.ErrorRecord] + $ErrorRecord + ) + + if ($null -eq $ErrorRecord) + { + $exception = New-Object -TypeName 'System.Exception' ` + -ArgumentList @($Message) + } + else + { + $exception = New-Object -TypeName 'System.Exception' ` + -ArgumentList @($Message, $ErrorRecord.Exception) + } + + $newObjectParameters = @{ + TypeName = 'System.Management.Automation.ErrorRecord' + ArgumentList = @( + $exception.ToString(), + 'MachineStateIncorrect', + 'InvalidResult', + $null + ) + } + + $errorRecordToThrow = New-Object @newObjectParameters + + throw $errorRecordToThrow +} +#EndRegion './Public/New-InvalidResultException.ps1' 71 +#Region './Public/New-NotImplementedException.ps1' 0 +<# + .SYNOPSIS + Creates and throws an not implemented exception. + + .DESCRIPTION + Creates and throws an not implemented exception. + + .PARAMETER Message + The message explaining why this error is being thrown. + + .PARAMETER ErrorRecord + The error record containing the exception that is causing this terminating error. + + .EXAMPLE + if ($runFeature) + { + $errorMessage = $script:localizedData.FeatureMissing -f $path + New-NotImplementedException -Message $errorMessage -ErrorRecord $_ + } + + Throws an not implemented exception if the variable $runFeature contains + a value. +#> +function New-NotImplementedException +{ + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '')] + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $Message, + + [Parameter()] + [ValidateNotNull()] + [System.Management.Automation.ErrorRecord] + $ErrorRecord + ) + + if ($null -eq $ErrorRecord) + { + $invalidOperationException = New-Object -TypeName 'NotImplementedException' ` + -ArgumentList @($Message) + } + else + { + $invalidOperationException = New-Object -TypeName 'NotImplementedException' ` + -ArgumentList @($Message, $ErrorRecord.Exception) + } + + $newObjectParameters = @{ + TypeName = 'System.Management.Automation.ErrorRecord' + ArgumentList = @( + $invalidOperationException.ToString(), + 'MachineStateIncorrect', + 'NotImplemented', + $null + ) + } + + $errorRecordToThrow = New-Object @newObjectParameters + + throw $errorRecordToThrow +} +#EndRegion './Public/New-NotImplementedException.ps1' 66 +#Region './Public/New-ObjectNotFoundException.ps1' 0 + +<# + .SYNOPSIS + Creates and throws an object not found exception. + + .DESCRIPTION + Creates and throws an object not found exception. + + .PARAMETER Message + The message explaining why this error is being thrown. + + .PARAMETER ErrorRecord + The error record containing the exception that is causing this terminating error. + + .EXAMPLE + try + { + Get-ChildItem -Path $path + } + catch + { + $errorMessage = $script:localizedData.PathNotFoundMessage -f $path + New-ObjectNotFoundException -Message $errorMessage -ErrorRecord $_ + } +#> +function New-ObjectNotFoundException +{ + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '')] + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $Message, + + [Parameter()] + [ValidateNotNull()] + [System.Management.Automation.ErrorRecord] + $ErrorRecord + ) + + if ($null -eq $ErrorRecord) + { + $exception = New-Object -TypeName 'System.Exception' ` + -ArgumentList @($Message) + } + else + { + $exception = New-Object -TypeName 'System.Exception' ` + -ArgumentList @($Message, $ErrorRecord.Exception) + } + + $newObjectParameters = @{ + TypeName = 'System.Management.Automation.ErrorRecord' + ArgumentList = @( + $exception.ToString(), + 'MachineStateIncorrect', + 'ObjectNotFound', + $null + ) + } + + $errorRecordToThrow = New-Object @newObjectParameters + + throw $errorRecordToThrow +} +#EndRegion './Public/New-ObjectNotFoundException.ps1' 68 +#Region './Public/Remove-CommonParameter.ps1' 0 +<# + .SYNOPSIS + Removes common parameters from a hashtable. + + .DESCRIPTION + This function serves the purpose of removing common parameters and option + common parameters from a parameter hashtable. + + .PARAMETER Hashtable + The parameter hashtable that should be pruned. + + .EXAMPLE + Remove-CommonParameter -Hashtable $PSBoundParameters + + Returns a new hashtable without the common and optional common parameters. +#> +function Remove-CommonParameter +{ + [Diagnostics.CodeAnalysis.SuppressMessageAttribute( + 'PSUseShouldProcessForStateChangingFunctions', + '', + Justification = 'ShouldProcess is not supported in DSC resources.' + )] + [OutputType([System.Collections.Hashtable])] + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [System.Collections.Hashtable] + $Hashtable + ) + + $inputClone = $Hashtable.Clone() + + $commonParameters = [System.Management.Automation.PSCmdlet]::CommonParameters + $commonParameters += [System.Management.Automation.PSCmdlet]::OptionalCommonParameters + + $Hashtable.Keys | Where-Object -FilterScript { + $_ -in $commonParameters + } | ForEach-Object -Process { + $inputClone.Remove($_) + } + + return $inputClone +} +#EndRegion './Public/Remove-CommonParameter.ps1' 46 +#Region './Public/Set-DscMachineRebootRequired.ps1' 0 +<# + .SYNOPSIS + Set the DSC reboot required status variable. + + .DESCRIPTION + Sets the global DSCMachineStatus variable to a value of 1. + This function is used to set the global variable that indicates + to the LCM that a reboot of the node is required. + + .EXAMPLE + PS C:\> Set-DscMachineRebootRequired + + Sets the $global:DSCMachineStatus variable to 1. + + .NOTES + This function is implemented so that individual resource modules + do not need to use and therefore suppress Global variables + directly. It also enables mocking to increase testability of + consumers. +#> +function Set-DscMachineRebootRequired +{ + [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '')] + # Suppressing this rule because $global:DSCMachineStatus is used to trigger a reboot. + [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidGlobalVars', '')] + <# + Suppressing this rule because $global:DSCMachineStatus is only set, + never used (by design of Desired State Configuration). + #> + [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', '')] + [CmdletBinding()] + param + ( + ) + + $global:DSCMachineStatus = 1 +} +#EndRegion './Public/Set-DscMachineRebootRequired.ps1' 38 +#Region './Public/Set-PSModulePath.ps1' 0 + +<# + .SYNOPSIS + Set environment variable PSModulePath in the current session or machine + wide. + + .DESCRIPTION + This is a wrapper to set environment variable PSModulePath in current + session or machine wide. + + .PARAMETER Path + A string with all the paths separated by semi-colons. + + .PARAMETER Machine + If set the PSModulePath will be changed machine wide. If not set, only + the current session will be changed. + + .EXAMPLE + Set-PSModulePath -Path ';' + + .EXAMPLE + Set-PSModulePath -Path ';' -Machine +#> +function Set-PSModulePath +{ + [Diagnostics.CodeAnalysis.SuppressMessageAttribute( + 'PSUseShouldProcessForStateChangingFunctions', + '', + Justification = 'ShouldProcess is not supported in DSC resources.' + )] + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $Path, + + [Parameter()] + [System.Management.Automation.SwitchParameter] + $Machine + ) + + if ($Machine.IsPresent) + { + [System.Environment]::SetEnvironmentVariable('PSModulePath', $Path, [System.EnvironmentVariableTarget]::Machine) + } + else + { + $env:PSModulePath = $Path + } +} +#EndRegion './Public/Set-PSModulePath.ps1' 53 +#Region './Public/Test-DscParameterState.ps1' 0 +<# + .SYNOPSIS + This method is used to test current and desired values for any DSC resource. + + .DESCRIPTION + This function tests the parameter status of DSC resource parameters against + the current values present on the system. + + .PARAMETER CurrentValues + A hashtable with the current values on the system, obtained by e.g. + Get-TargetResource. + + .PARAMETER DesiredValues + The hashtable of desired values. For example $PSBoundParameters with the + desired values. + + .PARAMETER Properties + This is a list of properties in the desired values list should be checked. + If this is empty then all values in DesiredValues are checked. + + .PARAMETER ExcludeProperties + This is a list of which properties in the desired values list should be checked. + If this is empty then all values in DesiredValues are checked. + + .PARAMETER TurnOffTypeChecking + Indicates that the type of the parameter should not be checked. + + .PARAMETER ReverseCheck + Indicates that a reverse check should be done. The current and desired state + are swapped for another test. + + .PARAMETER SortArrayValues + If the sorting of array values does not matter, values are sorted internally + before doing the comparison. + + .EXAMPLE + $currentState = Get-TargetResource @PSBoundParameters + + $returnValue = Test-DscParameterState -CurrentValues $currentState -DesiredValues $PSBoundParameters + + The function Get-TargetResource is called first using all bound parameters + to get the values in the current state. The result is then compared to the + desired state by calling `Test-DscParameterState`. + + .EXAMPLE + $getTargetResourceParameters = @{ + ServerName = $ServerName + InstanceName = $InstanceName + Name = $Name + } + + $returnValue = Test-DscParameterState ` + -CurrentValues (Get-TargetResource @getTargetResourceParameters) ` + -DesiredValues $PSBoundParameters ` + -ExcludeProperties @( + 'FailsafeOperator' + 'NotificationMethod' + ) + + This compares the values in the current state against the desires state. + The function Get-TargetResource is called using just the required parameters + to get the values in the current state. The parameter 'ExcludeProperties' + is used to exclude the properties 'FailsafeOperator' and + 'NotificationMethod' from the comparison. + + .EXAMPLE + $getTargetResourceParameters = @{ + ServerName = $ServerName + InstanceName = $InstanceName + Name = $Name + } + + $returnValue = Test-DscParameterState ` + -CurrentValues (Get-TargetResource @getTargetResourceParameters) ` + -DesiredValues $PSBoundParameters ` + -Properties ServerName, Name + + This compares the values in the current state against the desires state. + The function Get-TargetResource is called using just the required parameters + to get the values in the current state. The 'Properties' parameter is used + to to only compare the properties 'ServerName' and 'Name'. +#> +function Test-DscParameterState +{ + [CmdletBinding()] + [OutputType([Bool])] + param + ( + [Parameter(Mandatory = $true)] + [System.Object] + $CurrentValues, + + [Parameter(Mandatory = $true)] + [System.Object] + $DesiredValues, + + [Parameter()] + [System.String[]] + [Alias('ValuesToCheck')] + $Properties, + + [Parameter()] + [System.String[]] + $ExcludeProperties, + + [Parameter()] + [System.Management.Automation.SwitchParameter] + $TurnOffTypeChecking, + + [Parameter()] + [System.Management.Automation.SwitchParameter] + $ReverseCheck, + + [Parameter()] + [System.Management.Automation.SwitchParameter] + $SortArrayValues + ) + + $returnValue = $true + + $resultCompare = Compare-DscParameterState @PSBoundParameters + + if ($resultCompare.InDesiredState -contains $false) + { + $returnValue = $false + } + + return $returnValue +} +#EndRegion './Public/Test-DscParameterState.ps1' 130 +#Region './Public/Test-IsNanoServer.ps1' 0 +<# + .SYNOPSIS + Tests if the current OS is a Nano server. + + .DESCRIPTION + Tests if the current OS is a Nano server. + + .EXAMPLE + Test-IsNanoServer + + Returns $true if the current operating system is Nano Server, if not $false + is returned. +#> +function Test-IsNanoServer +{ + [OutputType([System.Boolean])] + [CmdletBinding()] + param () + + $productDatacenterNanoServer = 143 + $productStandardNanoServer = 144 + + $operatingSystemSKU = (Get-CimInstance -ClassName Win32_OperatingSystem).OperatingSystemSKU + + Write-Verbose -Message ($script:localizedData.TestIsNanoServerOperatingSystemSku -f $operatingSystemSKU) + + return ($operatingSystemSKU -in ($productDatacenterNanoServer, $productStandardNanoServer)) +} +#EndRegion './Public/Test-IsNanoServer.ps1' 29 +#Region './suffix.ps1' 0 +$script:localizedData = Get-LocalizedData -DefaultUICulture 'en-US' +#EndRegion './suffix.ps1' 2 diff --git a/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/Modules/DscResource.Common/0.10.2/en-US/DscResource.Common.strings.psd1 b/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/Modules/DscResource.Common/0.10.2/en-US/DscResource.Common.strings.psd1 new file mode 100644 index 0000000..14c3a4f --- /dev/null +++ b/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/Modules/DscResource.Common/0.10.2/en-US/DscResource.Common.strings.psd1 @@ -0,0 +1,38 @@ +# Localized English (en-US) strings. + +ConvertFrom-StringData @' + TestIsNanoServerOperatingSystemSku = OperatingSystemSKU {0} was returned by Win32_OperatingSystem when detecting if operating system is Nano Server. (DRC0008) + ModuleNotFound = Please ensure that the PowerShell module '{0}' is installed. (DRC0009) + ParameterUsageWrong = None of the parameter(s) '{0}' may be used at the same time as any of the parameter(s) '{1}'. (DRC0010) + AddressFormatError = Address '{0}' is not in the correct format. Please correct the Address parameter in the configuration and try again. (DRC0011) + AddressIPv4MismatchError = Address '{0}' is in IPv4 format, which does not match server address family {1}. Please correct either of them in the configuration and try again. (DRC0012) + AddressIPv6MismatchError = Address '{0}' is in IPv6 format, which does not match server address family {1}. Please correct either of them in the configuration and try again. (DRC0013) + InvalidDesiredValuesError = Property 'DesiredValues' in Test-DscParameterState must be either a Hashtable or CimInstance. Type detected was '{0}'. (DRC0014) + InvalidCurrentValuesError = Property 'CurrentValues' in Test-DscParameterState must be either a Hashtable, CimInstance, or CimIntance[]. Type detected was '{0}'. (DRC0015) + InvalidPropertiesError = If 'DesiredValues' is a CimInstance then property 'Properties' must contain a value. (DRC0016) + MatchPsCredentialUsernameMessage = MATCH: PSCredential username match. Current state is '{0}' and desired state is '{1}'. (DRC0017) + NoMatchPsCredentialUsernameMessage = NOTMATCH: PSCredential username mismatch. Current state is '{0}' and desired state is '{1}'. (DRC0018) + NoMatchTypeMismatchMessage = NOTMATCH: Type mismatch for property '{0}' Current state type is '{1}' and desired type is '{2}'. (DRC0019) + MatchValueMessage = MATCH: Value (type '{0}') for property '{1}' does match. Current state is '{2}' and desired state is '{3}'. (DRC0020) + NoMatchValueMessage = NOTMATCH: Value (type '{0}') for property '{1}' does not match. Current state is '{2}' and desired state is '{3}'. (DRC0021) + NoMatchValueDifferentCountMessage = NOTMATCH: Value (type '{0}') for property '{1}' does have a different count. Current state count is '{2}' and desired state count is '{3}'. (DRC0022) + NoMatchElementTypeMismatchMessage = NOTMATCH: Type mismatch for property '{0}' Current state type of element [{1}] is '{2}' and desired type is '{3}'. (DRC0023) + NoMatchElementValueMismatchMessage = NOTMATCH: Value [{0}] (type '{1}') for property '{2}' does match. Current state is '{3}' and desired state is '{4}'. (DRC0024) + MatchElementValueMessage = MATCH: Value [{0}] (type '{1}') for property '{2}' does match. Current state is '{3}' and desired state is '{4}'. (DRC0025) + PropertyInDesiredStateMessage = Property '{0}' is in desired state. (DRC0026) + StartingReverseCheck = Starting with a reverse check. (DRC0027) + TestDscParameterCompareMessage = Comparing values in property '{0}'. (DRC0028) + TooManyCimInstances = More than one CIM instance was returned from the current state. (DRC0029) + TestingCimInstance = Testing CIM instance '{0}' with the key properties '{1}'. (DRC0030) + MissingCimInstance = The CIM instance '{0}' with the key properties '{1}' is missing. (DRC0031) + ArrayValueIsAbsent = The array value '{0}' is absent. (DRC0032) + ArrayValueIsPresent = The array value '{0}' is present. (DRC0033) + KeyPropertiesMissing = The hashtable passed to function Test-DscPropertyState is missing the key 'KeyProperties'. This must be set to the property names that makes each instance in the CIM instance collection unique. (DRC0034) + ArrayDoesNotMatch = One or more values in an array does not match the desired state. Details of the changes are below. (DRC0035) + PropertyValueOfTypeDoesNotMatch = {0} value does not match. Current value is '{1}', but expected the value '{2}'. (DRC0036) + UnableToCompareType = Unable to compare the type {0} as it is not handled by the Test-DscPropertyState cmdlet. (DRC0037) + EvaluatePropertyState = Evaluating the state of the property '{0}'. (DRC0038) + PropertyInDesiredState = The parameter '{0}' is in desired state. (DRC0039) + PropertyNotInDesiredState = The parameter '{0}' is not in desired state. (DRC0040) + PropertyNotInDesiredStateMessage = Property '{0}' is not in desired state. (DRC0041) +'@ diff --git a/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/Modules/DscResource.Common/0.10.2/en-US/about_DscResource.Common.help.txt b/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/Modules/DscResource.Common/0.10.2/en-US/about_DscResource.Common.help.txt new file mode 100644 index 0000000..2a99677 --- /dev/null +++ b/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/Modules/DscResource.Common/0.10.2/en-US/about_DscResource.Common.help.txt @@ -0,0 +1,26 @@ +TOPIC + about_DscResource.Common + +SHORT DESCRIPTION + Common functions used in DSC tesources. + +LONG DESCRIPTION + This module contains common functions that are used in DSC resources. + +EXAMPLES + PS C:\> Get-Command -Module DscResource.Common + +NOTE: + Thank you to the DSC Community contributors who contributed to this module by + writing code, sharing opinions, and provided feedback. + +TROUBLESHOOTING NOTE: + Go to the Github repository for read about issues, submit a new issue, and read + about new releases. https://github.com/dsccommunity/DscResource.Common + +SEE ALSO + - https://github.com/dsccommunity/DscResource.Common + +KEYWORDS + DSC, Localization + diff --git a/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/PSGetModuleInfo.xml b/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/PSGetModuleInfo.xml new file mode 100644 index 0000000..7242bf7 Binary files /dev/null and b/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/PSGetModuleInfo.xml differ diff --git a/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/en-US/DnsRecordA.strings.psd1 b/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/en-US/DnsRecordA.strings.psd1 new file mode 100644 index 0000000..3f083ea --- /dev/null +++ b/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/en-US/DnsRecordA.strings.psd1 @@ -0,0 +1,10 @@ +<# + .SYNOPSIS + The localized resource strings in English (en-US) for the + resource DnsRecordA. +#> + +ConvertFrom-StringData @' + GettingDnsRecordMessage = Getting specified DNS {0} record in zone '{1}' from '{3}'. + CreatingDnsRecordMessage = Creating {0} record specified in zone '{1}' on '{3}'. +'@ diff --git a/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/en-US/DnsRecordAScoped.strings.psd1 b/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/en-US/DnsRecordAScoped.strings.psd1 new file mode 100644 index 0000000..73d661a --- /dev/null +++ b/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/en-US/DnsRecordAScoped.strings.psd1 @@ -0,0 +1,10 @@ +<# + .SYNOPSIS + The localized resource strings in English (en-US) for the + resource DnsRecordAScoped. +#> + +ConvertFrom-StringData @' + GettingDnsRecordMessage = Getting specified DNS {0} record in zone '{1}' with the '{2}' scope, from '{3}'. + CreatingDnsRecordMessage = Creating {0} record specified in zone '{1}' with the '{2}' scope on '{3}'. +'@ diff --git a/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/en-US/DnsRecordAaaa.strings.psd1 b/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/en-US/DnsRecordAaaa.strings.psd1 new file mode 100644 index 0000000..ee652e2 --- /dev/null +++ b/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/en-US/DnsRecordAaaa.strings.psd1 @@ -0,0 +1,10 @@ +<# + .SYNOPSIS + The localized resource strings in English (en-US) for the + resource DnsRecordAaaa. +#> + +ConvertFrom-StringData @' + GettingDnsRecordMessage = Getting specified DNS {0} record in zone '{1}' from '{3}'. + CreatingDnsRecordMessage = Creating {0} record specified in zone '{1}' on '{3}'. +'@ diff --git a/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/en-US/DnsRecordAaaaScoped.strings.psd1 b/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/en-US/DnsRecordAaaaScoped.strings.psd1 new file mode 100644 index 0000000..9524359 --- /dev/null +++ b/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/en-US/DnsRecordAaaaScoped.strings.psd1 @@ -0,0 +1,10 @@ +<# + .SYNOPSIS + The localized resource strings in English (en-US) for the + resource DnsRecordAaaaScoped. +#> + +ConvertFrom-StringData @' + GettingDnsRecordMessage = Getting specified DNS {0} record in zone '{1}' with the '{2}' scope, from '{3}'. + CreatingDnsRecordMessage = Creating {0} record specified in zone '{1}' with the '{2}' scope on '{3}'. +'@ diff --git a/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/en-US/DnsRecordBase.strings.psd1 b/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/en-US/DnsRecordBase.strings.psd1 new file mode 100644 index 0000000..fb9879b --- /dev/null +++ b/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/en-US/DnsRecordBase.strings.psd1 @@ -0,0 +1,20 @@ +<# + .SYNOPSIS + The localized resource strings in English (en-US) for the + class DnsRecordBase. +#> + +ConvertFrom-StringData @' + GettingDscResourceObject = Calling GetResourceRecord() from the {0} class to get the object's current state. + RecordNotFound = A matching DNS resource record not found. + RecordFound = A matching DNS resource record was found. + ModifyingExistingRecord = Modifying existing record. + RemovingExistingRecord = Removing existing record. + AddingNewRecord = Calling AddResourceRecord() from the {0} class to create a new record. + PropertyIsNotInDesiredState = DNS record property '{0}' is not correct. Expected '{1}', actual '{2}'. + ObjectInDesiredState = DNS record is in the desired state. + ObjectNotInDesiredState = DNS record is NOT in the desired state. + GetResourceRecordNotImplemented = GetResourceRecord() not implemented. + AddResourceRecordNotImplemented = AddResourceRecord() not implemented. + NewResourceObjectFromRecordNotImplemented = NewDscResourceObjectFromRecord() not implemented. +'@ diff --git a/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/en-US/DnsRecordCname.strings.psd1 b/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/en-US/DnsRecordCname.strings.psd1 new file mode 100644 index 0000000..a619bf0 --- /dev/null +++ b/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/en-US/DnsRecordCname.strings.psd1 @@ -0,0 +1,10 @@ +<# + .SYNOPSIS + The localized resource strings in English (en-US) for the + resource DnsRecordCname. +#> + +ConvertFrom-StringData @' + GettingDnsRecordMessage = Getting specified DNS {0} record in zone '{1}' from '{3}'. + CreatingDnsRecordMessage = Creating {0} record specified in zone '{1}' on '{3}'. +'@ diff --git a/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/en-US/DnsRecordCnameScoped.strings.psd1 b/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/en-US/DnsRecordCnameScoped.strings.psd1 new file mode 100644 index 0000000..058737e --- /dev/null +++ b/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/en-US/DnsRecordCnameScoped.strings.psd1 @@ -0,0 +1,10 @@ +<# + .SYNOPSIS + The localized resource strings in English (en-US) for the + resource DnsRecordCnameScoped. +#> + +ConvertFrom-StringData @' + GettingDnsRecordMessage = Getting specified DNS {0} record in zone '{1}' with the '{2}' scope, from '{3}'. + CreatingDnsRecordMessage = Creating {0} record specified in zone '{1}' with the '{2}' scope on '{3}'. +'@ diff --git a/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/en-US/DnsRecordMx.strings.psd1 b/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/en-US/DnsRecordMx.strings.psd1 new file mode 100644 index 0000000..7d23c5a --- /dev/null +++ b/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/en-US/DnsRecordMx.strings.psd1 @@ -0,0 +1,11 @@ +<# + .SYNOPSIS + The localized resource strings in English (en-US) for the + resource DnsRecordMx. +#> + +ConvertFrom-StringData @' + GettingDnsRecordMessage = Getting specified DNS {0} record in zone '{1}' from '{3}'. + CreatingDnsRecordMessage = Creating {0} record specified in zone '{1}' on '{3}'. + DomainZoneMismatch = Email domain '{0}' must be the same as the zone specified ('{1}') or a subdomain thereof. +'@ diff --git a/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/en-US/DnsRecordMxScoped.strings.psd1 b/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/en-US/DnsRecordMxScoped.strings.psd1 new file mode 100644 index 0000000..a35fa4b --- /dev/null +++ b/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/en-US/DnsRecordMxScoped.strings.psd1 @@ -0,0 +1,10 @@ +<# + .SYNOPSIS + The localized resource strings in English (en-US) for the + resource DnsRecordMxScoped. +#> + +ConvertFrom-StringData @' + GettingDnsRecordMessage = Getting specified DNS {0} record in zone '{1}' with the '{2}' scope, from '{3}'. + CreatingDnsRecordMessage = Creating {0} record specified in zone '{1}' with the '{2}' scope on '{3}'. +'@ diff --git a/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/en-US/DnsRecordNs.strings.psd1 b/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/en-US/DnsRecordNs.strings.psd1 new file mode 100644 index 0000000..2977996 --- /dev/null +++ b/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/en-US/DnsRecordNs.strings.psd1 @@ -0,0 +1,11 @@ +<# + .SYNOPSIS + The localized resource strings in English (en-US) for the + resource DnsRecordNs. +#> + +ConvertFrom-StringData @' + GettingDnsRecordMessage = Getting specified DNS {0} record in zone '{1}' from '{3}'. + CreatingDnsRecordMessage = Creating {0} record specified in zone '{1}' on '{3}'. + DomainZoneMismatch = Domain '{0}' must be the same as the zone specified ('{1}') or a subdomain thereof. +'@ diff --git a/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/en-US/DnsRecordNsScoped.strings.psd1 b/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/en-US/DnsRecordNsScoped.strings.psd1 new file mode 100644 index 0000000..28d8aba --- /dev/null +++ b/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/en-US/DnsRecordNsScoped.strings.psd1 @@ -0,0 +1,10 @@ +<# + .SYNOPSIS + The localized resource strings in English (en-US) for the + resource DnsRecordNsScoped. +#> + +ConvertFrom-StringData @' + GettingDnsRecordMessage = Getting specified DNS {0} record in zone '{1}' with the '{2}' scope, from '{3}'. + CreatingDnsRecordMessage = Creating {0} record specified in zone '{1}' with the '{2}' scope on '{3}'. +'@ diff --git a/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/en-US/DnsRecordPtr.strings.psd1 b/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/en-US/DnsRecordPtr.strings.psd1 new file mode 100644 index 0000000..332d6be --- /dev/null +++ b/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/en-US/DnsRecordPtr.strings.psd1 @@ -0,0 +1,13 @@ +<# + .SYNOPSIS + The localized resource strings in English (en-US) for the + resource DnsRecordPtr. +#> + +ConvertFrom-StringData @' + GettingDnsRecordMessage = Getting specified DNS {0} record in zone '{1}' from '{3}'. + CreatingDnsRecordMessage = Creating {0} record specified in zone '{1}' on '{3}'. + NotAnIPv4Zone = The zone "{0}" is not an IPv4 reverse lookup zone. + NotAnIPv6Zone = The zone "{0}" is not an IPv6 reverse lookup zone. + WrongZone = "{0}" does not belong to the "{1}" zone. +'@ diff --git a/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/en-US/DnsRecordSrv.strings.psd1 b/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/en-US/DnsRecordSrv.strings.psd1 new file mode 100644 index 0000000..739f372 --- /dev/null +++ b/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/en-US/DnsRecordSrv.strings.psd1 @@ -0,0 +1,10 @@ +<# + .SYNOPSIS + The localized resource strings in English (en-US) for the + resource DnsRecordSrvScoped. +#> + +ConvertFrom-StringData @' + GettingDnsRecordMessage = Getting DNS record '{0}' with target of '{1}' ({2}) in zone '{3}' from '{5}'. + CreatingDnsRecordMessage = Creating {0} record for symbolic name '{1}' with target '{2}' in zone '{3}' on '{5}'. +'@ diff --git a/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/en-US/DnsRecordSrvScoped.strings.psd1 b/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/en-US/DnsRecordSrvScoped.strings.psd1 new file mode 100644 index 0000000..42a70d7 --- /dev/null +++ b/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/en-US/DnsRecordSrvScoped.strings.psd1 @@ -0,0 +1,11 @@ +<# + .SYNOPSIS + The localized resource strings in English (en-US) for the + resource DnsRecordSrvScoped. +#> + +ConvertFrom-StringData @' + GettingDnsRecordMessage = Getting DNS record '{0}' with target of '{1}' ({2}) in zone '{3}' with the '{4}' scope, from '{5}'. + CreatingDnsRecordMessage = Creating {0} record for symbolic name '{1}' with target '{2}' in zone '{3}' with the '{4}' scope on '{5}'. + +'@ diff --git a/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/en-US/DnsServerCache.strings.psd1 b/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/en-US/DnsServerCache.strings.psd1 new file mode 100644 index 0000000..fddeab9 --- /dev/null +++ b/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/en-US/DnsServerCache.strings.psd1 @@ -0,0 +1,14 @@ +<# + .SYNOPSIS + The localized resource strings in English (en-US) for the + resource DnsServerCache. +#> + +ConvertFrom-StringData @' + GetCurrentState = Getting the current state of the cache settings for the server '{0}'. (DSC0001) + TestDesiredState = Determining the current state of the cache settings for the server '{0}'. (DSC0002) + SetDesiredState = Setting the desired state for the cache settings for the server '{0}'. (DSC0003) + NotInDesiredState = The cache settings for the server '{0}' is not in desired state. (DSC0004) + InDesiredState = The cache settings for the server '{0}' is in desired state. (DSC0005) + SetProperty = The cache property '{0}' will be set to '{1}'. (DSC0006) +'@ diff --git a/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/en-US/DnsServerDsSetting.strings.psd1 b/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/en-US/DnsServerDsSetting.strings.psd1 new file mode 100644 index 0000000..82c8af9 --- /dev/null +++ b/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/en-US/DnsServerDsSetting.strings.psd1 @@ -0,0 +1,14 @@ +<# + .SYNOPSIS + The localized resource strings in English (en-US) for the + resource DnsServerDsSetting. +#> + +ConvertFrom-StringData @' + GetCurrentState = Getting the current state of the directory services settings for the server '{0}'. (DSDS0001) + TestDesiredState = Determining the current state of the directory services settings for the server '{0}'. (DSDS0002) + SetDesiredState = Setting the desired state for the directory services settings for the server '{0}'. (DSDS0003) + NotInDesiredState = The directory services settings for the server '{0}' is not in desired state. (DSDS0004) + InDesiredState = The directory services settings for the server '{0}' is in desired state. (DSDS0005) + SetProperty = The directory services property '{0}' will be set to '{1}'. (DSDS0006) +'@ diff --git a/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/en-US/DnsServerDsc.strings.psd1 b/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/en-US/DnsServerDsc.strings.psd1 new file mode 100644 index 0000000..3b4e73d --- /dev/null +++ b/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/en-US/DnsServerDsc.strings.psd1 @@ -0,0 +1,12 @@ +<# + .SYNOPSIS + The localized resource strings in English (en-US) for the + resource DnsServerDsc module. This file should only contain + localized strings for private and public functions. +#> + +ConvertFrom-StringData @' + PropertyHasWrongFormat = The property '{0}' has the value '{1}' that cannot be converted to [System.TimeSpan]. (DS0001) + TimeSpanExceedMaximumValue = The property '{0}' has the value '{1}' that exceeds the maximum value of '{2}'. (DS0002) + TimeSpanBelowMinimumValue = The property '{0}' has the value '{1}' that is below the minimum value of '{2}'. (DS0003) +'@ diff --git a/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/en-US/DnsServerEDns.strings.psd1 b/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/en-US/DnsServerEDns.strings.psd1 new file mode 100644 index 0000000..629d3b0 --- /dev/null +++ b/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/en-US/DnsServerEDns.strings.psd1 @@ -0,0 +1,14 @@ +<# + .SYNOPSIS + The localized resource strings in English (en-US) for the + resource DnsServerEDns. +#> + +ConvertFrom-StringData @' + GetCurrentState = Getting the current state of the extension mechanisms for DNS (EDNS) settings for the server '{0}'. (DSED0001) + TestDesiredState = Determining the current state of the extension mechanisms for DNS (EDNS) settings for the server '{0}'. (DSED0002) + SetDesiredState = Setting the desired state for the extension mechanisms for DNS (EDNS) settings for the server '{0}'. (DSED0003) + NotInDesiredState = The extension mechanisms for DNS (EDNS) settings for the server '{0}' is not in desired state. (DSED0004) + InDesiredState = The extension mechanisms for DNS (EDNS) settings for the server '{0}' is in desired state. (DSED0005) + SetProperty = The extension mechanisms for DNS (EDNS) property '{0}' will be set to '{1}'. (DSED0006) +'@ diff --git a/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/en-US/DnsServerRecursion.strings.psd1 b/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/en-US/DnsServerRecursion.strings.psd1 new file mode 100644 index 0000000..5571875 --- /dev/null +++ b/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/en-US/DnsServerRecursion.strings.psd1 @@ -0,0 +1,15 @@ +<# + .SYNOPSIS + The localized resource strings in English (en-US) for the + resource DnsServerRecursion. +#> + +ConvertFrom-StringData @' + GetCurrentState = Getting the current state of the recursion settings for the server '{0}'. (DSR0001) + TestDesiredState = Determining the current state of the recursion settings for the server '{0}'. (DSR0002) + SetDesiredState = Setting the desired state for the recursion settings for the server '{0}'. (DSR0003) + NotInDesiredState = The recursion settings for the server '{0}' is not in desired state. (DSR0004) + InDesiredState = The recursion settings for the server '{0}' is in desired state. (DSR0005) + SetProperty = The recursion property '{0}' will be set to '{1}'. (DSR0006) + PropertyIsNotInValidRange = The property '{0}' has the value '{1}' that is not within the range of 1 seconds to 15 seconds. (DSR0007) +'@ diff --git a/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/en-US/DnsServerScavenging.strings.psd1 b/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/en-US/DnsServerScavenging.strings.psd1 new file mode 100644 index 0000000..a74c392 --- /dev/null +++ b/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/en-US/DnsServerScavenging.strings.psd1 @@ -0,0 +1,14 @@ +<# + .SYNOPSIS + The localized resource strings in English (en-US) for the + resource DnsServerScavenging. +#> + +ConvertFrom-StringData @' + GetCurrentState = Getting the current state of the scavenging settings for the server '{0}'. (DSS0001) + TestDesiredState = Determining the current state of the scavenging settings for the server '{0}'. (DSS0002) + SetDesiredState = Setting the desired state for the scavenging settings for the server '{0}'. (DSS0003) + NotInDesiredState = The scavenging settings for the server '{0}' is not in desired state. (DSS0004) + InDesiredState = The scavenging settings for the server '{0}' is in desired state. (DSS0005) + SetProperty = The scavenging property '{0}' will be set to '{1}'. (DSS0006) +'@ diff --git a/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/en-US/ResourceBase.strings.psd1 b/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/en-US/ResourceBase.strings.psd1 new file mode 100644 index 0000000..f73e82d --- /dev/null +++ b/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/en-US/ResourceBase.strings.psd1 @@ -0,0 +1,17 @@ +<# + .SYNOPSIS + The localized resource strings in English (en-US) for the + class ResourceBase. +#> + +ConvertFrom-StringData @' + GetCurrentState = Getting the current state of {1} for the server '{0}'. (RB0001) + TestDesiredState = Determining the current state of {1} for the server '{0}'. (RB0002) + SetDesiredState = Setting the desired state of {1} for the server '{0}'. (RB0003) + NotInDesiredState = The {1} for the server '{0}' is not in desired state. (RB0004) + InDesiredState = The {1} for the server '{0}' is in desired state. (RB0005) + SetProperty = The {2} property '{0}' will be set to '{1}'. (RB0006) + NoPropertiesToSet = All properties are in desired state. (RB0007) + ModifyMethodNotImplemented = An override for the method Modify() is not implemented in the resource. (RB0008) + GetCurrentStateMethodNotImplemented = An override for the method GetCurrentState() is not implemented in the resource. (RB0009) +'@ diff --git a/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/en-US/ResourcePropertiesBase.strings.psd1 b/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/en-US/ResourcePropertiesBase.strings.psd1 new file mode 100644 index 0000000..20d13e8 --- /dev/null +++ b/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/en-US/ResourcePropertiesBase.strings.psd1 @@ -0,0 +1,14 @@ +<# + .SYNOPSIS + The localized resource strings in English (en-US) for the + class ResourcePropertiesBase. + + .NOTES + This should normally not have any strings since the base class is only meant + to have DSC properties that can be inherited. Though this localized string + file exist so that it possible to recursively look for strings in all inherited + classes (base classes). +#> + +ConvertFrom-StringData @' +'@ diff --git a/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/en-US/about_DnsRecordA.help.txt b/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/en-US/about_DnsRecordA.help.txt new file mode 100644 index 0000000..b7b73da --- /dev/null +++ b/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/en-US/about_DnsRecordA.help.txt @@ -0,0 +1,80 @@ +.NAME + DnsRecordA + +.SYNOPSIS + The DnsRecordA DSC resource manages A DNS records against a specific zone on a Domain Name System (DNS) server. + +.DESCRIPTION + The DnsRecordA DSC resource manages A DNS records against a specific zone on a Domain Name System (DNS) server. + +.PARAMETER Name + Key - System.String + Specifies the name of a DNS server resource record object. (Key Parameter) + +.PARAMETER IPv4Address + Key - System.String + Specifies the IPv4 address of a host. (Key Parameter) + +.EXAMPLE 1 + +This configuration will ensure a DNS A record exists when only the mandatory properties are specified. + +Configuration DnsRecordA_Mandatory_config +{ + Import-DscResource -ModuleName 'DnsServerDsc' + + Node localhost + { + DnsRecordA 'TestRecord' + { + ZoneName = 'contoso.com' + Name = 'www' + IPv4Address = '192.168.50.10' + Ensure = 'Present' + } + } +} + +.EXAMPLE 2 + +This configuration will ensure a DNS A record exists when all properties are specified. + +Configuration DnsRecordA_Full_config +{ + Import-DscResource -ModuleName 'DnsServerDsc' + + Node localhost + { + DnsRecordA 'TestRecord' + { + ZoneName = 'contoso.com' + Name = 'www' + IPv4Address = '192.168.50.10' + TimeToLive = '01:00:00' + DnsServer = 'localhost' + Ensure = 'Present' + } + } +} + +.EXAMPLE 3 + +This configuration will ensure a DNS A record does not exist when mandatory properties are specified. + +Note that not all mandatory properties are necessarily key properties. Non-key property values will be ignored when determining whether the record is to be removed. + +Configuration DnsRecordA_Remove_config +{ + Import-DscResource -ModuleName 'DnsServerDsc' + + Node localhost + { + DnsRecordA 'TestRecord' + { + ZoneName = 'contoso.com' + Name = 'www' + IPv4Address = '192.168.50.10' + Ensure = 'Absent' + } + } +} diff --git a/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/en-US/about_DnsRecordAScoped.help.txt b/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/en-US/about_DnsRecordAScoped.help.txt new file mode 100644 index 0000000..1f84f01 --- /dev/null +++ b/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/en-US/about_DnsRecordAScoped.help.txt @@ -0,0 +1,79 @@ +.NAME + DnsRecordAScoped + +.SYNOPSIS + The DnsRecordAScoped DSC resource manages A DNS records against a specific zone and zone scope on a Domain Name System (DNS) server. + +.DESCRIPTION + The DnsRecordAScoped DSC resource manages A DNS records against a specific zone and zone scope on a Domain Name System (DNS) server. + +.PARAMETER ZoneScope + Key - System.String + Specifies the name of a zone scope. (Key Parameter) + +.EXAMPLE 1 + +This configuration will ensure a DNS A record exists when only the mandatory properties are specified. + +Configuration DnsRecordAScoped_Mandatory_config +{ + Import-DscResource -ModuleName 'DnsServerDsc' + + Node localhost + { + DnsRecordAScoped 'TestRecord' + { + ZoneName = 'contoso.com' + ZoneScope = 'external' + Name = 'www' + IPv4Address = '192.168.50.10' + Ensure = 'Present' + } + } +} + +.EXAMPLE 2 + +This configuration will ensure a DNS A record exists when all properties are specified. + +Configuration DnsRecordAScoped_Full_config +{ + Import-DscResource -ModuleName 'DnsServerDsc' + + Node localhost + { + DnsRecordAScoped 'TestRecord' + { + ZoneName = 'contoso.com' + ZoneScope = 'external' + Name = 'www' + IPv4Address = '192.168.50.10' + TimeToLive = '01:00:00' + DnsServer = 'localhost' + Ensure = 'Present' + } + } +} + +.EXAMPLE 3 + +This configuration will ensure a DNS A record does not exist when mandatory properties are specified. + +Note that not all mandatory properties are necessarily key properties. Non-key property values will be ignored when determining whether the record is to be removed. + +Configuration DnsRecordAScoped_Remove_config +{ + Import-DscResource -ModuleName 'DnsServerDsc' + + Node localhost + { + DnsRecordAScoped 'TestRecord' + { + ZoneName = 'contoso.com' + ZoneScope = 'external' + Name = 'www' + IPv4Address = '192.168.50.10' + Ensure = 'Absent' + } + } +} diff --git a/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/en-US/about_DnsRecordAaaa.help.txt b/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/en-US/about_DnsRecordAaaa.help.txt new file mode 100644 index 0000000..f582638 --- /dev/null +++ b/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/en-US/about_DnsRecordAaaa.help.txt @@ -0,0 +1,80 @@ +.NAME + DnsRecordAaaa + +.SYNOPSIS + The DnsRecordAaaa DSC resource manages AAAA DNS records against a specific zone on a Domain Name System (DNS) server. + +.DESCRIPTION + The DnsRecordAaaa DSC resource manages AAAA DNS records against a specific zone on a Domain Name System (DNS) server. + +.PARAMETER Name + Key - System.String + Specifies the name of a DNS server resource record object. (Key Parameter) + +.PARAMETER IPv6Address + Key - System.String + Specifies the IPv6 address of a host. (Key Parameter) + +.EXAMPLE 1 + +This configuration will ensure a DNS AAAA record exists when only the mandatory properties are specified. + +Configuration DnsRecordAaaa_Mandatory_config +{ + Import-DscResource -ModuleName 'DnsServerDsc' + + Node localhost + { + DnsRecordAaaa 'TestRecord' + { + ZoneName = 'contoso.com' + Name = 'www' + IPv6Address = '2001:db8:85a3::8a2e:370:7334' + Ensure = 'Present' + } + } +} + +.EXAMPLE 2 + +This configuration will ensure a DNS AAAA record exists when all properties are specified. + +Configuration DnsRecordAaaa_Full_config +{ + Import-DscResource -ModuleName 'DnsServerDsc' + + Node localhost + { + DnsRecordAaaa 'TestRecord' + { + ZoneName = 'contoso.com' + Name = 'www' + IPv6Address = '2001:db8:85a3::8a2e:370:7334' + TimeToLive = '01:00:00' + DnsServer = 'localhost' + Ensure = 'Present' + } + } +} + +.EXAMPLE 3 + +This configuration will ensure a DNS AAAA record does not exist when mandatory properties are specified. + +Note that not all mandatory properties are necessarily key properties. Non-key property values will be ignored when determining whether the record is to be removed. + +Configuration DnsRecordAaaa_Remove_config +{ + Import-DscResource -ModuleName 'DnsServerDsc' + + Node localhost + { + DnsRecordAaaa 'TestRecord' + { + ZoneName = 'contoso.com' + Name = 'www' + IPv6Address = '2001:db8:85a3::8a2e:370:7334' + Ensure = 'Absent' + } + } +} diff --git a/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/en-US/about_DnsRecordAaaaScoped.help.txt b/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/en-US/about_DnsRecordAaaaScoped.help.txt new file mode 100644 index 0000000..87de7af --- /dev/null +++ b/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/en-US/about_DnsRecordAaaaScoped.help.txt @@ -0,0 +1,79 @@ +.NAME + DnsRecordAaaaScoped + +.SYNOPSIS + The DnsRecordAaaaScoped DSC resource manages AAAA DNS records against a specific zone and zone scope on a Domain Name System (DNS) server. + +.DESCRIPTION + The DnsRecordAaaaScoped DSC resource manages AAAA DNS records against a specific zone and zone scope on a Domain Name System (DNS) server. + +.PARAMETER ZoneScope + Key - System.String + Specifies the name of a zone scope. (Key Parameter) + +.EXAMPLE 1 + +This configuration will ensure a DNS AAAA record exists when only the mandatory properties are specified. + +Configuration DnsRecordAaaaScoped_Mandatory_config +{ + Import-DscResource -ModuleName 'DnsServerDsc' + + Node localhost + { + DnsRecordAaaaScoped 'TestRecord' + { + ZoneName = 'contoso.com' + ZoneScope = 'external' + Name = 'www' + IPv6Address = '2001:db8:85a3::8a2e:370:7334' + Ensure = 'Present' + } + } +} + +.EXAMPLE 2 + +This configuration will ensure a DNS AAAA record exists when all properties are specified. + +Configuration DnsRecordAaaaScoped_Full_config +{ + Import-DscResource -ModuleName 'DnsServerDsc' + + Node localhost + { + DnsRecordAaaaScoped 'TestRecord' + { + ZoneName = 'contoso.com' + ZoneScope = 'external' + Name = 'www' + IPv6Address = '2001:db8:85a3::8a2e:370:7334' + TimeToLive = '01:00:00' + DnsServer = 'localhost' + Ensure = 'Present' + } + } +} + +.EXAMPLE 3 + +This configuration will ensure a DNS AAAA record does not exist when mandatory properties are specified. + +Note that not all mandatory properties are necessarily key properties. Non-key property values will be ignored when determining whether the record is to be removed. + +Configuration DnsRecordAaaaScoped_Remove_config +{ + Import-DscResource -ModuleName 'DnsServerDsc' + + Node localhost + { + DnsRecordAaaaScoped 'TestRecord' + { + ZoneName = 'contoso.com' + ZoneScope = 'external' + Name = 'www' + IPv6Address = '2001:db8:85a3::8a2e:370:7334' + Ensure = 'Absent' + } + } +} diff --git a/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/en-US/about_DnsRecordCname.help.txt b/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/en-US/about_DnsRecordCname.help.txt new file mode 100644 index 0000000..954a7e9 --- /dev/null +++ b/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/en-US/about_DnsRecordCname.help.txt @@ -0,0 +1,16 @@ +.NAME + DnsRecordCname + +.SYNOPSIS + The DnsRecordCname DSC resource manages CNAME DNS records against a specific zone on a Domain Name System (DNS) server. + +.DESCRIPTION + The DnsRecordCname DSC resource manages CNAME DNS records against a specific zone on a Domain Name System (DNS) server. + +.PARAMETER Name + Key - System.String + Specifies the name of a DNS server resource record object. (Key Parameter) + +.PARAMETER HostNameAlias + Key - System.String + Specifies a a canonical name target for a CNAME record. This must be a fully qualified domain name (FQDN). (Key Parameter) diff --git a/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/en-US/about_DnsRecordCnameScoped.help.txt b/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/en-US/about_DnsRecordCnameScoped.help.txt new file mode 100644 index 0000000..7b0c83d --- /dev/null +++ b/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/en-US/about_DnsRecordCnameScoped.help.txt @@ -0,0 +1,12 @@ +.NAME + DnsRecordCnameScoped + +.SYNOPSIS + The DnsRecordCnameScoped DSC resource manages CNAME DNS records against a specific zone and zone scope on a Domain Name System (DNS) server. + +.DESCRIPTION + The DnsRecordCnameScoped DSC resource manages CNAME DNS records against a specific zone and zone scope on a Domain Name System (DNS) server. + +.PARAMETER ZoneScope + Key - System.String + Specifies the name of a zone scope. (Key Parameter) diff --git a/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/en-US/about_DnsRecordMx.help.txt b/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/en-US/about_DnsRecordMx.help.txt new file mode 100644 index 0000000..b284676 --- /dev/null +++ b/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/en-US/about_DnsRecordMx.help.txt @@ -0,0 +1,90 @@ +.NAME + DnsRecordMx + +.SYNOPSIS + The DnsRecordMx DSC resource manages MX DNS records against a specific zone on a Domain Name System (DNS) server. + +.DESCRIPTION + The DnsRecordMx DSC resource manages MX DNS records against a specific zone on a Domain Name System (DNS) server. + +.PARAMETER EmailDomain + Key - System.String + Everything after the '@' in the email addresses supported by this mail exchanger. It must be a subdomain the zone or the zone itself. To specify all subdomains, use the '' character (i.e.: .contoso.com). (Key Parameter) + +.PARAMETER MailExchange + Key - System.String + FQDN of the server handling email for the specified email domain. When setting the value, this FQDN must resolve to an IP address and cannot reference a CNAME record. (Key Parameter) + +.PARAMETER Priority + Required - System.UInt16 + Specifies the priority for this MX record among other MX records that belong to the same email domain, where a lower value has a higher priority. (Mandatory Parameter) + +.PARAMETER recordName + Write - System.String + +.EXAMPLE 1 + +This configuration will ensure a DNS MX record exists when only the mandatory properties are specified. + +Configuration DnsRecordMx_Mandatory_config +{ + Import-DscResource -ModuleName 'DnsServerDsc' + + Node localhost + { + DnsRecordMx 'TestRecord' + { + ZoneName = 'contoso.com' + EmailDomain = 'contoso.com' + MailExchange = 'mailserver1.contoso.com' + Priority = 20 + Ensure = 'Present' + } + } +} + +.EXAMPLE 2 + +This configuration will ensure a DNS MX record exists when all properties are specified. + +Configuration DnsRecordMx_Full_config +{ + Import-DscResource -ModuleName 'DnsServerDsc' + + Node localhost + { + DnsRecordMx 'TestRecord' + { + ZoneName = 'contoso.com' + EmailDomain = 'contoso.com' + MailExchange = 'mailserver1.contoso.com' + Priority = 20 + TimeToLive = '01:00:00' + DnsServer = 'localhost' + Ensure = 'Present' + } + } +} + +.EXAMPLE 3 + +This configuration will ensure a DNS MX record does not exist when mandatory properties are specified. + +Note that the 'Priority' property value will be ignored when determining whether the record is to be removed. + +Configuration DnsRecordMx_Remove_config +{ + Import-DscResource -ModuleName 'DnsServerDsc' + + Node localhost + { + DnsRecordMx 'TestRecord' + { + ZoneName = 'contoso.com' + EmailDomain = 'contoso.com' + MailExchange = 'mailserver1.contoso.com' + Priority = 20 + Ensure = 'Absent' + } + } +} diff --git a/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/en-US/about_DnsRecordMxScoped.help.txt b/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/en-US/about_DnsRecordMxScoped.help.txt new file mode 100644 index 0000000..45fa2a7 --- /dev/null +++ b/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/en-US/about_DnsRecordMxScoped.help.txt @@ -0,0 +1,82 @@ +.NAME + DnsRecordMxScoped + +.SYNOPSIS + The DnsRecordMxScoped DSC resource manages MX DNS records against a specific zone and zone scope on a Domain Name System (DNS) server. + +.DESCRIPTION + The DnsRecordMxScoped DSC resource manages MX DNS records against a specific zone and zone scope on a Domain Name System (DNS) server. + +.PARAMETER ZoneScope + Key - System.String + Specifies the name of a zone scope. (Key Parameter) + +.EXAMPLE 1 + +This configuration will ensure a DNS MX record exists when only the mandatory properties are specified. + +Configuration DnsRecordMxScoped_Mandatory_config +{ + Import-DscResource -ModuleName 'DnsServerDsc' + + Node localhost + { + DnsRecordMxScoped 'TestRecord' + { + ZoneName = 'contoso.com' + ZoneScope = 'external' + EmailDomain = 'contoso.com' + MailExchange = 'mailserver1.contoso.com' + Priority = 20 + Ensure = 'Present' + } + } +} + +.EXAMPLE 2 + +This configuration will ensure a DNS MX record exists when all properties are specified. + +Configuration DnsRecordMxScoped_Full_config +{ + Import-DscResource -ModuleName 'DnsServerDsc' + + Node localhost + { + DnsRecordMxScoped 'TestRecord' + { + ZoneName = 'contoso.com' + ZoneScope = 'external' + EmailDomain = 'contoso.com' + MailExchange = 'mailserver1.contoso.com' + Priority = 20 + TimeToLive = '01:00:00' + DnsServer = 'localhost' + Ensure = 'Present' + } + } +} + +.EXAMPLE 3 + +This configuration will ensure a DNS MX record does not exist when mandatory properties are specified. + +Note that the 'Priority' property value will be ignored when determining whether the record is to be removed. + +Configuration DnsRecordMxScoped_Remove_config +{ + Import-DscResource -ModuleName 'DnsServerDsc' + + Node localhost + { + DnsRecordMxScoped 'TestRecord' + { + ZoneName = 'contoso.com' + ZoneScope = 'external' + EmailDomain = 'contoso.com' + MailExchange = 'mailserver1.contoso.com' + Priority = 20 + Ensure = 'Absent' + } + } +} diff --git a/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/en-US/about_DnsRecordNs.help.txt b/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/en-US/about_DnsRecordNs.help.txt new file mode 100644 index 0000000..990d15e --- /dev/null +++ b/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/en-US/about_DnsRecordNs.help.txt @@ -0,0 +1,80 @@ +.NAME + DnsRecordNs + +.SYNOPSIS + The DnsRecordNs DSC resource manages NS DNS records against a specific zone on a Domain Name System (DNS) server. + +.DESCRIPTION + The DnsRecordNs DSC resource manages NS DNS records against a specific zone on a Domain Name System (DNS) server. + +.PARAMETER DomainName + Key - System.String + Specifies the fully qualified DNS domain name for which the NameServer is authoritative. It must be a subdomain the zone or the zone itself. To specify all subdomains, use the '' character (i.e.: .contoso.com). (Key Parameter) + +.PARAMETER NameServer + Key - System.String + Specifies the name server of a domain. This should be a fully qualified domain name, not an IP address (Key Parameter) + +.EXAMPLE 1 + +This configuration will ensure a DNS NS record exists when only the mandatory properties are specified. + +Configuration DnsRecordNs_Mandatory_config +{ + Import-DscResource -ModuleName 'DnsServerDsc' + + Node localhost + { + DnsRecordNs 'TestRecord' + { + ZoneName = 'contoso.com' + DomainName = 'contoso.com' + NameServer = 'ns.contoso.com' + Ensure = 'Present' + } + } +} + +.EXAMPLE 2 + +This configuration will ensure a DNS NS record exists when all properties are specified. + +Configuration DnsRecordNs_Full_config +{ + Import-DscResource -ModuleName 'DnsServerDsc' + + Node localhost + { + DnsRecordNs 'TestRecord' + { + ZoneName = 'contoso.com' + DomainName = 'contoso.com' + NameServer = 'ns.contoso.com' + TimeToLive = '01:00:00' + DnsServer = 'localhost' + Ensure = 'Present' + } + } +} + +.EXAMPLE 3 + +This configuration will ensure a DNS NS record does not exist when mandatory properties are specified. + +Note that not all mandatory properties are necessarily key properties. Non-key property values will be ignored when determining whether the record is to be removed. + +Configuration DnsRecordNs_Remove_config +{ + Import-DscResource -ModuleName 'DnsServerDsc' + + Node localhost + { + DnsRecordNs 'TestRecord' + { + ZoneName = 'contoso.com' + DomainName = 'contoso.com' + NameServer = 'ns.contoso.com' + Ensure = 'Absent' + } + } +} diff --git a/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/en-US/about_DnsRecordNsScoped.help.txt b/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/en-US/about_DnsRecordNsScoped.help.txt new file mode 100644 index 0000000..5fd2ef6 --- /dev/null +++ b/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/en-US/about_DnsRecordNsScoped.help.txt @@ -0,0 +1,79 @@ +.NAME + DnsRecordNsScoped + +.SYNOPSIS + The DnsRecordNsScoped DSC resource manages NS DNS records against a specific zone and zone scope on a Domain Name System (DNS) server. + +.DESCRIPTION + The DnsRecordNsScoped DSC resource manages NS DNS records against a specific zone and zone scope on a Domain Name System (DNS) server. + +.PARAMETER ZoneScope + Key - System.String + Specifies the name of a zone scope. (Key Parameter) + +.EXAMPLE 1 + +This configuration will ensure a DNS NS record exists when only the mandatory properties are specified. + +Configuration DnsRecordNsScoped_Mandatory_config +{ + Import-DscResource -ModuleName 'DnsServerDsc' + + Node localhost + { + DnsRecordNsScoped 'TestRecord' + { + ZoneName = 'contoso.com' + ZoneScope = 'external' + DomainName = 'contoso.com' + NameServer = 'ns.contoso.com' + Ensure = 'Present' + } + } +} + +.EXAMPLE 2 + +This configuration will ensure a DNS NS record exists when all properties are specified. + +Configuration DnsRecordNsScoped_Full_config +{ + Import-DscResource -ModuleName 'DnsServerDsc' + + Node localhost + { + DnsRecordNsScoped 'TestRecord' + { + ZoneName = 'contoso.com' + ZoneScope = 'external' + DomainName = 'contoso.com' + NameServer = 'ns.contoso.com' + TimeToLive = '01:00:00' + DnsServer = 'localhost' + Ensure = 'Present' + } + } +} + +.EXAMPLE 3 + +This configuration will ensure a DNS NS record does not exist when mandatory properties are specified. + +Note that not all mandatory properties are necessarily key properties. Non-key property values will be ignored when determining whether the record is to be removed. + +Configuration DnsRecordNsScoped_Remove_config +{ + Import-DscResource -ModuleName 'DnsServerDsc' + + Node localhost + { + DnsRecordNsScoped 'TestRecord' + { + ZoneName = 'contoso.com' + ZoneScope = 'external' + DomainName = 'contoso.com' + NameServer = 'ns.contoso.com' + Ensure = 'Absent' + } + } +} diff --git a/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/en-US/about_DnsRecordPtr.help.txt b/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/en-US/about_DnsRecordPtr.help.txt new file mode 100644 index 0000000..b4dd5ee --- /dev/null +++ b/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/en-US/about_DnsRecordPtr.help.txt @@ -0,0 +1,143 @@ +.NAME + DnsRecordPtr + +.SYNOPSIS + The DnsRecordPtr DSC resource manages PTR DNS records against a specific zone on a Domain Name System (DNS) server. + +.DESCRIPTION + The DnsRecordPtr DSC resource manages PTR DNS records against a specific zone on a Domain Name System (DNS) server. + +.PARAMETER IpAddress + Key - System.String + Specifies the IP address to which the record is associated (Can be either IPv4 or IPv6. (Key Parameter) + +.PARAMETER Name + Key - System.String + Specifies the FQDN of the host when you add a PTR resource record. (Key Parameter) + +.PARAMETER recordHostName + Write - System.String + +.EXAMPLE 1 + +This configuration will ensure a DNS PTR record exists when only the mandatory properties are specified. + +Configuration DnsRecordPtr_Mandatory_config +{ + Import-DscResource -ModuleName 'DnsServerDsc' + + Node localhost + { + DnsRecordPtr 'TestRecord' + { + ZoneName = '0.168.192.in-addr.arpa' + IpAddress = '192.168.0.9' + Name = 'quarks.contoso.com' + Ensure = 'Present' + } + } +} + +.EXAMPLE 2 + +This configuration will ensure a DNS PTR record exists when all properties are specified. + +Configuration DnsRecordPtr_Full_config +{ + Import-DscResource -ModuleName 'DnsServerDsc' + + Node localhost + { + DnsRecordPtr 'TestRecord' + { + ZoneName = '0.168.192.in-addr.arpa' + IpAddress = '192.168.0.9' + Name = 'quarks.contoso.com' + TimeToLive = '01:00:00' + DnsServer = 'localhost' + Ensure = 'Present' + } + } +} + +.EXAMPLE 3 + +This configuration will ensure a DNS PTR record does not exist when mandatory properties are specified. + +Configuration DnsRecordPtr_Remove_config +{ + Import-DscResource -ModuleName 'DnsServerDsc' + + Node localhost + { + DnsRecordPtr 'TestRecord' + { + ZoneName = '0.168.192.in-addr.arpa' + IpAddress = '192.168.0.9' + Name = 'quarks.contoso.com' + Ensure = 'Absent' + } + } +} + +.EXAMPLE 4 + +This configuration will ensure a DNS PTR record exists for an IPv6 address when only the mandatory properties are specified. + +Configuration DnsRecordPtr_Mandatory_v6_config +{ + Import-DscResource -ModuleName 'DnsServerDsc' + + Node localhost + { + DnsRecordPtr 'TestRecord' + { + ZoneName = '0.0.d.f.ip6.arpa' + IpAddress = 'fd00::515c:0:0:d59' + Name = 'quarks.contoso.com' + Ensure = 'Present' + } + } +} + +.EXAMPLE 5 + +This configuration will ensure a DNS PTR record exists for an IPv6 address when all properties are specified. + +Configuration DnsRecordPtr_Full_v6_config +{ + Import-DscResource -ModuleName 'DnsServerDsc' + + Node localhost + { + DnsRecordPtr 'TestRecord' + { + ZoneName = '0.0.d.f.ip6.arpa' + IpAddress = 'fd00::515c:0:0:d59' + Name = 'quarks.contoso.com' + TimeToLive = '01:00:00' + DnsServer = 'localhost' + Ensure = 'Present' + } + } +} + +.EXAMPLE 6 + +This configuration will ensure a DNS PTR record does not exist for an IPv6 address when mandatory properties are specified. + +Configuration DnsRecordPtr_Remove_v6_config +{ + Import-DscResource -ModuleName 'DnsServerDsc' + + Node localhost + { + DnsRecordPtr 'TestRecord' + { + ZoneName = '0.0.d.f.ip6.arpa' + IpAddress = 'fd00::515c:0:0:d59' + Name = 'quarks.contoso.com' + Ensure = 'Absent' + } + } +} diff --git a/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/en-US/about_DnsRecordSrv.help.txt b/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/en-US/about_DnsRecordSrv.help.txt new file mode 100644 index 0000000..6f6384e --- /dev/null +++ b/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/en-US/about_DnsRecordSrv.help.txt @@ -0,0 +1,112 @@ +.NAME + DnsRecordSrv + +.SYNOPSIS + The DnsRecordSrv DSC resource manages SRV DNS records against a specific zone on a Domain Name System (DNS) server. + +.DESCRIPTION + The DnsRecordSrv DSC resource manages SRV DNS records against a specific zone on a Domain Name System (DNS) server. + +.PARAMETER SymbolicName + Key - System.String + Service name for the SRV record. eg: xmpp, ldap, etc. (Key Parameter) + +.PARAMETER Protocol + Key - System.String + Allowed values: TCP, UDP + Service transmission protocol ('TCP' or 'UDP') (Key Parameter) + +.PARAMETER Port + Key - System.UInt16 + The TCP or UDP port on which the service is found (Key Parameter) + +.PARAMETER Target + Key - System.String + Specifies the Target Hostname or IP Address. (Key Parameter) + +.PARAMETER Priority + Required - System.UInt16 + Specifies the Priority value of the SRV record. (Mandatory Parameter) + +.PARAMETER Weight + Required - System.UInt16 + Specifies the weight of the SRV record. (Mandatory Parameter) + +.EXAMPLE 1 + +This configuration will ensure a DNS SRV record exists for +XMPP that points to chat.contoso.com with a priority +of 10, weight of 20. + +Configuration DnsRecordSrv_config +{ + Import-DscResource -ModuleName 'DnsServerDsc' + + Node localhost + { + DnsRecordSrv 'TestRecord' + { + ZoneName = 'contoso.com' + SymbolicName = 'xmpp' + Protocol = 'tcp' + Port = 5222 + Target = 'chat.contoso.com' + Priority = 10 + Weight = 20 + Ensure = 'Present' + } + } +} + +.EXAMPLE 2 + +This configuration will ensure a DNS SRV record exists for +XMPP that points to chat.contoso.com with a priority of 20, +weight of 50 and TTL of 5 hours. + +Configuration DnsRecordSrv_full_config +{ + Import-DscResource -ModuleName 'DnsServerDsc' + + Node localhost + { + DnsRecordSrv 'TestRecord Full' + { + ZoneName = 'contoso.com' + SymbolicName = 'xmpp' + Protocol = 'tcp' + Port = 5222 + Target = 'chat.contoso.com' + Priority = 20 + Weight = 50 + TimeToLive = '05:00:00' + Ensure = 'Present' + } + } +} + +.EXAMPLE 3 + +This configuration will remove a specified DNS SRV record. Note that +Priority and Weight are mandatory attributes, but their values are not +used to determine which record to remove. + +Configuration DnsRecordSrv_Remove_config +{ + Import-DscResource -ModuleName 'DnsServerDsc' + + Node localhost + { + DnsRecordSrv 'RemoveTestRecord' + { + ZoneName = 'contoso.com' + SymbolicName = 'xmpp' + Protocol = 'tcp' + Port = 5222 + Target = 'chat.contoso.com' + Priority = 0 + Weight = 0 + Ensure = 'Absent' + } + } +} diff --git a/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/en-US/about_DnsRecordSrvScoped.help.txt b/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/en-US/about_DnsRecordSrvScoped.help.txt new file mode 100644 index 0000000..06bd7ab --- /dev/null +++ b/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/en-US/about_DnsRecordSrvScoped.help.txt @@ -0,0 +1,95 @@ +.NAME + DnsRecordSrvScoped + +.SYNOPSIS + The DnsRecordSrvScoped DSC resource manages SRV DNS records against a specific zone and zone scope on a Domain Name System (DNS) server. + +.DESCRIPTION + The DnsRecordSrvScoped DSC resource manages SRV DNS records against a specific zone and zone scope on a Domain Name System (DNS) server. + +.PARAMETER ZoneScope + Key - System.String + Specifies the name of a zone scope. (Key Parameter) + +.EXAMPLE 1 + +This configuration will ensure a DNS SRV record exists +in the external scope for XMPP that points to +chat.contoso.com with a priority of 10, weight of 20. + +Configuration DnsRecordSrvScoped_config +{ + Import-DscResource -ModuleName 'DnsServerDsc' + + Node localhost + { + DnsRecordSrvScoped 'TestRecord' + { + ZoneName = 'contoso.com' + ZoneScope = 'external' + SymbolicName = 'xmpp' + Protocol = 'tcp' + Port = 5222 + Target = 'chat.contoso.com' + Priority = 10 + Weight = 20 + Ensure = 'Present' + } + } +} + +.EXAMPLE 2 + +This configuration will ensure a DNS SRV record exists in the +external scope for XMPP that points to chat.contoso.com with +a priority of 20, weight of 50 and Time To Live of 5 hours. + +Configuration DnsRecordSrvScoped_full_config +{ + Import-DscResource -ModuleName 'DnsServerDsc' + + Node localhost + { + DnsRecordSrvScoped 'TestRecord Full' + { + ZoneName = 'contoso.com' + ZoneScope = 'external' + SymbolicName = 'xmpp' + Protocol = 'tcp' + Port = 5222 + Target = 'chat.contoso.com' + Priority = 20 + Weight = 50 + TimeToLive = '05:00:00' + Ensure = 'Present' + } + } +} + +.EXAMPLE 3 + +This configuration will remove a specified DNS SRV record in the +external scope. Note that Priority and Weight are mandatory +attributes, but their values are not used to determine which +record to remove. + +Configuration DnsRecordSrvScoped_Remove_config +{ + Import-DscResource -ModuleName 'DnsServerDsc' + + Node localhost + { + DnsRecordSrvScoped 'RemoveTestRecord' + { + ZoneName = 'contoso.com' + ZoneScope = 'external' + SymbolicName = 'xmpp' + Protocol = 'tcp' + Port = 5222 + Target = 'chat.contoso.com' + Priority = 0 + Weight = 0 + Ensure = 'Absent' + } + } +} diff --git a/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/en-US/about_DnsServerCache.help.txt b/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/en-US/about_DnsServerCache.help.txt new file mode 100644 index 0000000..ee79d74 --- /dev/null +++ b/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/en-US/about_DnsServerCache.help.txt @@ -0,0 +1,93 @@ +.NAME + DnsServerCache + +.SYNOPSIS + The DnsServerCache DSC resource manages cache settings on a Microsoft Domain + Name System (DNS) server. + +.DESCRIPTION + The DnsServerCache DSC resource manages cache settings on a Microsoft Domain + Name System (DNS) server. + +.PARAMETER DnsServer + Key - System.String + The host name of the Domain Name System (DNS) server, or use 'localhost' + for the current node. + +.PARAMETER IgnorePolicies + Write - Nullable[System.Boolean] + Specifies whether to ignore policies for this cache. + +.PARAMETER LockingPercent + Write - Nullable[System.UInt32] + Specifies a percentage of the original Time to Live (TTL) value that caching + can consume. Cache locking is configured as a percent value. For example, if + the cache locking value is set to 50, the DNS server does not overwrite a + cached entry for half of the duration of the TTL. If the cache locking percent + is set to 100 that means the DNS server will not overwrite cached entries + for the entire duration of the TTL. + +.PARAMETER MaxKBSize + Write - Nullable[System.UInt32] + Specifies the maximum size, in kilobytes, of the memory cache of a DNS server. + If set to 0 there is no limit. + +.PARAMETER MaxNegativeTtl + Write - System.String + Specifies how long an entry that records a negative answer to a query remains + stored in the DNS cache. Minimum value is '00:00:01' and maximum value is + '30.00:00:00' + +.PARAMETER MaxTtl + Write - System.String + Specifies how long a record is saved in cache. Minimum value is '00:00:00' + and maximum value is '30.00:00:00'. If the TimeSpan is set to '00:00:00' + (0 seconds), the DNS server does not cache records. + +.PARAMETER EnablePollutionProtection + Write - Nullable[System.Boolean] + Specifies whether DNS filters name service (NS) resource records that are + cached. Valid values are False ($false), which caches all responses to name + queries; and True ($true), which caches only the records that belong to the + same DNS subtree. + + When you set this parameter value to False ($false), cache pollution + protection is disabled. A DNS server caches the Host (A) record and all queried + NS resources that are in the DNS server zone. In this case, DNS can also cache + the NS record of an unauthorized DNS server. This event causes name resolution + to fail or to be appropriated for subsequent queries in the specified domain. + + When you set the value for this parameter to True ($true), the DNS server + enables cache pollution protection and ignores the Host (A) record. The DNS + server performs a cache update query to resolve the address of the NS if the + NS is outside the zone of the DNS server. The additional query minimally + affects DNS server performance. + +.PARAMETER StoreEmptyAuthenticationResponse + Write - Nullable[System.Boolean] + Specifies whether a DNS server stores empty authoritative responses in the + cache (RFC-2308). + +.EXAMPLE 1 + +This configuration will change the cache settings on the DNS server. + +Configuration DnsServerCache_SetCacheSettings_Config +{ + Import-DscResource -ModuleName 'DnsServerDsc' + + Node localhost + { + DnsServerCache 'SetCacheSettings' + { + DnsServer = 'localhost' + EnablePollutionProtection = $true + StoreEmptyAuthenticationResponse = $true + IgnorePolicies = $false + LockingPercent = 100 + MaxKBSize = 0 + MaxNegativeTtl = '00:15:00' + MaxTtl = '1.00:00:00' + } + } +} diff --git a/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/en-US/about_DnsServerDsSetting.help.txt b/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/en-US/about_DnsServerDsSetting.help.txt new file mode 100644 index 0000000..4e13a33 --- /dev/null +++ b/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/en-US/about_DnsServerDsSetting.help.txt @@ -0,0 +1,228 @@ +.NAME + DnsServerDsSetting + +.SYNOPSIS + The DnsServerDsSetting DSC resource manages DNS Active Directory settings + on a Microsoft Domain Name System (DNS) server. + +.DESCRIPTION + The DnsServerDsSetting DSC resource manages DNS Active Directory settings + on a Microsoft Domain Name System (DNS) server. + +.PARAMETER DnsServer + Key - System.String + The host name of the Domain Name System (DNS) server, or use 'localhost' + for the current node. + +.PARAMETER DirectoryPartitionAutoEnlistInterval + Write - System.String + Specifies the interval, during which a DNS server tries to enlist itself + in a DNS domain partition and DNS forest partition, if it is not already + enlisted. We recommend that you limit this value to the range one hour to + 180 days, inclusive, but you can use any value. We recommend that you set + the default value to one day. You must set the value 0 (zero) as a flag + value for the default value. However, you can allow zero and treat it + literally. + +.PARAMETER LazyUpdateInterval + Write - Nullable[System.UInt32] + Specifies a value, in seconds, to determine how frequently the DNS server + submits updates to the directory server without specifying the + LDAPSERVERLAZYCOMMITOID control ([MS-ADTS] section 3.1.1.3.4.1.7) at + the same time that it processes DNS dynamic update requests. We recommend + that you limit this value to the range 0x00000000 to 0x0000003c. You must + set the default value to 0x00000003. You must set the value zero to + indicate that the DNS server does not specify the + LDAPSERVERLAZYCOMMITOID control at the same time that it processes + DNS dynamic update requests. For more information about + LDAPSERVERLAZYCOMMITOID, see LDAPSERVERLAZYCOMMITOID control + code. The LDAPSERVERLAZYCOMMITOID control instructs the DNS server + to return the results of a directory service modification command after + it is completed in memory but before it is committed to disk. In this + way, the server can return results quickly and save data to disk without + sacrificing performance. The DNS server must send this control only to + the directory server that is attached to an LDAP update that the DNS + server initiates in response to a DNS dynamic update request. If the + value is nonzero, LDAP updates that occur during the processing of DNS + dynamic update requests must not specify the LDAPSERVERLAZYCOMMITOID + control if a period of less than DsLazyUpdateInterval seconds has passed + since the last LDAP update that specifies this control. If a period that + is greater than DsLazyUpdateInterval seconds passes, during which time + the DNS server does not perform an LDAP update that specifies this + control, the DNS server must specify this control on the next update. + +.PARAMETER MinimumBackgroundLoadThreads + Write - Nullable[System.UInt32] + Specifies the minimum number of background threads that the DNS server + uses to load zone data from the directory service. You must limit this + value to the range 0x00000000 to 0x00000005, inclusive. You must set the + default value to 0x00000001, and you must treat the value zero as a flag + value for the default value. + +.PARAMETER PollingInterval + Write - System.String + Specifies how frequently the DNS server polls Active Directory Domain + Services (AD DS) for changes in Active Directory-integrated zones. You + must limit the value to the range 30 seconds to 3,600 seconds, inclusive. + +.PARAMETER RemoteReplicationDelay + Write - Nullable[System.UInt32] + Specifies the minimum interval, in seconds, that the DNS server waits + between the time that it determines that a single object has changed on + a remote directory server, to the time that it tries to replicate a + single object change. You must limit the value to the range 0x00000005 + to 0x00000E10, inclusive. You must set the default value to 0x0000001E, + and you must treat the value zero as a flag value for the default value. + +.PARAMETER TombstoneInterval + Write - System.String + Specifies the amount of time that DNS keeps tombstoned records alive in + Active Directory. We recommend that you limit this value to the range + three days to eight weeks, inclusive, but you can set it to any value in + the range 82 hours to 8 weeks. We recommend that you set the default + value to 14 days and treat the value zero as a flag value for the + default. However, you can allow the value zero and treat it literally. + At 2:00 A.M. local time every day, the DNS server must search all + directory service zones for nodes that have the Active Directory + dnsTombstoned attribute set to True, and for a directory service + EntombedTime (section 2.2.2.2.3.23 of MS-DNSP) value that is greater + than previous directory service DSTombstoneInterval seconds. You must + permanently delete all such nodes from the directory server. + +.EXAMPLE 1 + +This configuration will change the Directory Partition Auto Enlist +Interval in Active Directory. + +configuration DnsServerDsSetting_DirectoryPartitionAutoEnlistInterval_Config +{ + Import-DscResource -ModuleName 'DnsServerDsc' + + node localhost + { + DnsServerDsSetting 'Integration_Test' + { + DnsServer = 'localhost' + DirectoryPartitionAutoEnlistInterval = '1.00:00:00' + } + } +} + +.EXAMPLE 2 + +This configuration will change the Lazy Update +Interval in Active Directory. + +configuration DnsServerDsSetting_LazyUpdateInterval_Config +{ + Import-DscResource -ModuleName 'DnsServerDsc' + + node localhost + { + DnsServerDsSetting 'Integration_Test' + { + DnsServer = 'localhost' + LazyUpdateInterval = 3 + } + } +} + +.EXAMPLE 3 + +This configuration will change the Minimum Background Load Threads +in Active Directory. + +configuration DnsServerDsSetting_MinimumBackgroundLoadThreads_Config +{ + Import-DscResource -ModuleName 'DnsServerDsc' + + node localhost + { + DnsServerDsSetting 'Integration_Test' + { + DnsServer = 'localhost' + MinimumBackgroundLoadThreads = 1 + } + } +} + +.EXAMPLE 4 + +This configuration will change the Polling +Interval in Active Directory. + +configuration DnsServerDsSetting_PollingInterval_Config +{ + Import-DscResource -ModuleName 'DnsServerDsc' + + node localhost + { + DnsServerDsSetting 'Integration_Test' + { + DnsServer = 'localhost' + PollingInterval = 180 + } + } +} + +.EXAMPLE 5 + +This configuration will change the Remote Replication Delay +in Active Directory. + +configuration DnsServerDsSetting_RemoteReplicationDelay_Config +{ + Import-DscResource -ModuleName 'DnsServerDsc' + + node localhost + { + DnsServerDsSetting 'Integration_Test' + { + DnsServer = 'localhost' + RemoteReplicationDelay = 30 + } + } +} + +.EXAMPLE 6 + +This configuration will change the DNS Tombstone +Interval in Active Directory. + +configuration DnsServerDsSetting_TombstoneInterval_Config +{ + Import-DscResource -ModuleName 'DnsServerDsc' + + node localhost + { + DnsServerDsSetting 'Integration_Test' + { + DnsServer = 'localhost' + TombstoneInterval = '14.00:00:00' + } + } +} + +.EXAMPLE 7 + +This configuration will set all Active Directory-based DNS settings on +the specified server. + +configuration DnsServerDsSetting_All_Config +{ + Import-DscResource -ModuleName 'DnsServerDsc' + + node localhost + { + DnsServerDsSetting 'Integration_Test' + { + DnsServer = 'localhost' + DirectoryPartitionAutoEnlistInterval = '1.00:00:00' + LazyUpdateInterval = 3 + MinimumBackgroundLoadThreads = 1 + PollingInterval = 180 + RemoteReplicationDelay = 30 + TombstoneInterval = '14.00:00:00' + } + } +} diff --git a/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/en-US/about_DnsServerDsc.help.txt b/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/en-US/about_DnsServerDsc.help.txt new file mode 100644 index 0000000..766eef3 --- /dev/null +++ b/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/en-US/about_DnsServerDsc.help.txt @@ -0,0 +1,25 @@ +TOPIC + about_DnsServerDsc + +SHORT DESCRIPTION + DSC resources for the management and configuration of Windows Server DNS Server. + +LONG DESCRIPTION + This module contains DSC resources for the management and configuration of Windows Server DNS Server. + +EXAMPLES + PS C:\> Get-DscResource -Module DnsServerDsc + +NOTE: + Thank you to the DSC Community contributors who contributed to this module by + writing code, sharing opinions, and provided feedback. + +TROUBLESHOOTING NOTE: + Go to the Github repository for read about issues, submit a new issue, and read + about new releases. https://github.com/dsccommunity/DnsServerDsc + +SEE ALSO + - https://github.com/dsccommunity/DnsServerDsc + +KEYWORDS + DSC, DscResource, DNS diff --git a/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/en-US/about_DnsServerEDns.help.txt b/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/en-US/about_DnsServerEDns.help.txt new file mode 100644 index 0000000..d5791aa --- /dev/null +++ b/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/en-US/about_DnsServerEDns.help.txt @@ -0,0 +1,85 @@ +.NAME + DnsServerEDns + +.SYNOPSIS + The DnsServerEDns DSC resource manages extension mechanisms for DNS (EDNS) + on a Microsoft Domain Name System (DNS) server. + +.DESCRIPTION + The DnsServerEDns DSC resource manages extension mechanisms for DNS (EDNS) + on a Microsoft Domain Name System (DNS) server. + +.PARAMETER DnsServer + Key - System.String + The host name of the Domain Name System (DNS) server, or use 'localhost' + for the current node. + +.PARAMETER CacheTimeout + Write - System.String + Specifies the number of seconds that the DNS server caches EDNS information. + +.PARAMETER EnableProbes + Write - Nullable[System.Boolean] + Specifies whether to enable the server to probe other servers to determine + whether they support EDNS. + +.PARAMETER EnableReception + Write - Nullable[System.Boolean] + Specifies whether the DNS server accepts queries that contain an EDNS record. + +.EXAMPLE 1 + +This configuration will change the cache timeout for +extension mechanisms for DNS (EDNS) on the DNS server. + +Configuration DnsServerEDns_SetCacheTimeout_Config +{ + Import-DscResource -ModuleName 'DnsServerDsc' + + Node localhost + { + DnsServerEDns 'SetCacheTimeout' + { + DnsServer = 'localhost' + CacheTimeout = '00:15:00' + } + } +} + +.EXAMPLE 2 + +This configuration will enable probes for the extension mechanisms for DNS +(EDNS) on the DNS server. + +Configuration DnsServerEDns_EnableProbes_Config +{ + Import-DscResource -ModuleName 'DnsServerDsc' + + Node localhost + { + DnsServerEDns 'EnableProbes' + { + DnsServer = 'localhost' + EnableProbes = $true + } + } +} + +.EXAMPLE 3 + +This configuration will allow to accepts queries for the extension mechanisms +for DNS (EDNS) on the DNS server. + +Configuration DnsServerEDns_EnableReception_Config +{ + Import-DscResource -ModuleName 'DnsServerDsc' + + Node localhost + { + DnsServerEDns 'EnableReception' + { + DnsServer = 'localhost' + EnableReception = $true + } + } +} diff --git a/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/en-US/about_DnsServerRecursion.help.txt b/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/en-US/about_DnsServerRecursion.help.txt new file mode 100644 index 0000000..4eb9c4a --- /dev/null +++ b/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/en-US/about_DnsServerRecursion.help.txt @@ -0,0 +1,52 @@ +.NAME + DnsServerRecursion + +.SYNOPSIS + The DnsServerRecursion DSC resource manages recursion settings on a Microsoft + Domain Name System (DNS) server. + +.DESCRIPTION + The DnsServerRecursion DSC resource manages recursion settings on a Microsoft + Domain Name System (DNS) server. Recursion occurs when a DNS server queries + other DNS servers on behalf of a requesting client, and then sends the answer + back to the client. + + The property SecureResponse that can be set by the cmdlet Set-DnsServerRecursion + changes the same value as EnablePollutionProtection in the resource DnsServerCache + does. Use the property EnablePollutionProtection in the resource DnsServerCache + to enforce pollution protection. + +.PARAMETER DnsServer + Key - System.String + The host name of the Domain Name System (DNS) server, or use 'localhost' + for the current node. + +.PARAMETER Enable + Write - Nullable[System.Boolean] + Specifies whether the server enables recursion. + +.PARAMETER AdditionalTimeout + Write - Nullable[System.UInt32] + Specifies the time interval, in seconds, that a DNS server waits as it uses + recursion to get resource records from a remote DNS server. Valid values are + in the range of 1 second to 15 seconds. See recommendation in the documentation + of https://docs.microsoft.com/en-us/powershell/module/dnsserver/set-dnsserverrecursion. + +.PARAMETER RetryInterval + Write - Nullable[System.UInt32] + Specifies elapsed seconds before a DNS server retries a recursive lookup. + Valid values are in the range of 1 second to 15 seconds. The + recommendation is that in general this value should not be change. However, + under a few circumstances it can be considered changing the value. For + example, if a DNS server contacts a remote DNS server over a slow link and + retries the lookup before it gets a response, it could help to raise the + retry interval to be slightly longer than the observed response time. + See recommendation in the documentation of https://docs.microsoft.com/en-us/powershell/module/dnsserver/set-dnsserverrecursion. + +.PARAMETER Timeout + Write - Nullable[System.UInt32] + Specifies the number of seconds that a DNS server waits before it stops + trying to contact a remote server. The valid value is in the range of 1 + second to 15 seconds. Recommendation is to increase this value when + recursion occurs over a slow link. See recommendation in the documentation + of https://docs.microsoft.com/en-us/powershell/module/dnsserver/set-dnsserverrecursion. diff --git a/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/en-US/about_DnsServerScavenging.help.txt b/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/en-US/about_DnsServerScavenging.help.txt new file mode 100644 index 0000000..75712d4 --- /dev/null +++ b/deployment/dsc/azshcihost/DnsServerDsc/3.0.0/en-US/about_DnsServerScavenging.help.txt @@ -0,0 +1,137 @@ +.NAME + DnsServerScavenging + +.SYNOPSIS + The DnsServerScavenging DSC resource manages scavenging on a Microsoft + Domain Name System (DNS) server. + +.DESCRIPTION + The DnsServerScavenging DSC resource manages scavenging on a Microsoft + Domain Name System (DNS) server. + +.PARAMETER DnsServer + Key - System.String + The host name of the Domain Name System (DNS) server, or use 'localhost' + for the current node. + +.PARAMETER ScavengingState + Write - Nullable[System.Boolean] + Specifies whether to Enable automatic scavenging of stale records. + ScavengingState determines whether the DNS scavenging feature is enabled + by default on newly created zones. + +.PARAMETER ScavengingInterval + Write - System.String + Specifies a length of time as a value that can be converted to a [TimeSpan] + object. ScavengingInterval determines whether the scavenging feature for + the DNS server is enabled and sets the number of hours between scavenging + cycles. The value 0 disables scavenging for the DNS server. A setting + greater than 0 enables scavenging for the server and sets the number of + days, hours, minutes, and seconds (formatted as dd.hh:mm:ss) between + scavenging cycles. The minimum value is 0. The maximum value is 365.00:00:00 + (1 year). + +.PARAMETER RefreshInterval + Write - System.String + Specifies the refresh interval as a value that can be converted to a [TimeSpan] + object (formatted as dd.hh:mm:ss). During this interval, a DNS server can + refresh a resource record that has a non-zero time stamp. Zones on the server + inherit this value automatically. If a DNS server does not refresh a resource + record that has a non-zero time stamp, the DNS server can remove that record + during the next scavenging. Do not select a value smaller than the longest + refresh period of a resource record registered in the zone. The minimum value + is 0. The maximum value is 365.00:00:00 (1 year). + +.PARAMETER NoRefreshInterval + Write - System.String + Specifies a length of time as a value that can be converted to a [TimeSpan] + object (formatted as dd.hh:mm:ss). NoRefreshInterval sets a period of time + in which no refreshes are accepted for dynamically updated records. Zones on + the server inherit this value automatically. This value is the interval between + the last update of a timestamp for a record and the earliest time when the + timestamp can be refreshed. The minimum value is 0. The maximum value is + 365.00:00:00 (1 year). + +.PARAMETER LastScavengeTime + Read - Nullable[System.DateTime] + The time when the last scavenging cycle was executed. + +.EXAMPLE 1 + +This configuration will enable scavenging and change the scavenging intervals +on the DNS server. + +Configuration DnsServerScavenging_EnableAndChangeScavengingIntervals_Config +{ + Import-DscResource -ModuleName 'DnsServerDsc' + + Node localhost + { + DnsServerScavenging 'EnableScavengingAndChangeIntervals' + { + DnsServer = 'localhost' + ScavengingState = $true + ScavengingInterval = '7.00:00:00' + RefreshInterval = '7.00:00:00' + NoRefreshInterval = '7.00:00:00' + } + } +} + +.EXAMPLE 2 + +This configuration will enable scavenging on the DNS server, using +the default interval values. + +Configuration DnsServerScavenging_EnableScavenging_Config +{ + Import-DscResource -ModuleName 'DnsServerDsc' + + Node localhost + { + DnsServerScavenging 'EnableScavenging' + { + DnsServer = 'localhost' + ScavengingState = $true + } + } +} + +.EXAMPLE 3 + +This configuration will change scavenging intervals on the DNS server, but +does not enforce that scavenging is enabled. + +Configuration DnsServerScavenging_ChangeScavengingIntervals_Config +{ + Import-DscResource -ModuleName 'DnsServerDsc' + + Node localhost + { + DnsServerScavenging 'ChangeScavengingIntervals' + { + DnsServer = 'localhost' + ScavengingInterval = '7.00:00:00' + RefreshInterval = '7.00:00:00' + NoRefreshInterval = '7.00:00:00' + } + } +} + +.EXAMPLE 4 + +This configuration will disable scavenging on the DNS server. + +Configuration DnsServerScavenging_DisableScavenging_Config +{ + Import-DscResource -ModuleName 'DnsServerDsc' + + Node localhost + { + DnsServerScavenging 'DisableScavenging' + { + DnsServer = 'localhost' + ScavengingState = $false + } + } +} diff --git a/deployment/dsc/azshcihost/Hyper-ConvertImage/10.2/Convert-WindowsImage.ps1 b/deployment/dsc/azshcihost/Hyper-ConvertImage/10.2/Convert-WindowsImage.ps1 new file mode 100644 index 0000000..43f4efa --- /dev/null +++ b/deployment/dsc/azshcihost/Hyper-ConvertImage/10.2/Convert-WindowsImage.ps1 @@ -0,0 +1,4081 @@ + +function +Convert-WindowsImage +{ + <# + .NOTES + Copyright (c) Microsoft Corporation. All rights reserved. + + Use of this sample source code is subject to the terms of the Microsoft + license agreement under which you licensed this sample source code. If + you did not accept the terms of the license agreement, you are not + authorized to use this sample source code. For the terms of the license, + please see the license agreement between you and Microsoft or, if applicable, + see the LICENSE.RTF on your install media or the root of your tools installation. + THE SAMPLE SOURCE CODE IS PROVIDED "AS IS", WITH NO WARRANTIES. + + .SYNOPSIS + Creates a bootable VHD(X) based on Windows 7 or Windows 8 installation media. + + .DESCRIPTION + Creates a bootable VHD(X) based on Windows 7 or Windows 8 installation media. + + .PARAMETER SourcePath + The complete path to the WIM or ISO file that will be converted to a Virtual Hard Disk. + The ISO file must be valid Windows installation media to be recognized successfully. + + .PARAMETER CacheSource + If the source WIM/ISO was copied locally, we delete it by default. + Pass $true to cache the source image from the temp directory. + + .PARAMETER VHDPath + The name and path of the Virtual Hard Disk to create. + Omitting this parameter will create the Virtual Hard Disk is the current directory, (or, + if specified by the -WorkingDirectory parameter, the working directory) and will automatically + name the file in the following format: + + ....___. + i.e.: + 9200.0.amd64fre.winmain_win8rtm.120725-1247_client_professional_en-us.vhd(x) + + .PARAMETER WorkingDirectory + Specifies the directory where the VHD(X) file should be generated. + If specified along with -VHDPath, the -WorkingDirectory value is ignored. + The default value is the current directory ($pwd). + + .PARAMETER TempDirectory + Specifies the directory where the logs and ISO files should be placed. + The default value is the temp directory ($env:Temp). + + .PARAMETER SizeBytes + The size of the Virtual Hard Disk to create. + For fixed disks, the VHD(X) file will be allocated all of this space immediately. + For dynamic disks, this will be the maximum size that the VHD(X) can grow to. + The default value is 40GB. + + .PARAMETER VHDFormat + Specifies whether to create a VHD or VHDX formatted Virtual Hard Disk. + The default is AUTO, which will create a VHD if using the BIOS disk layout or + VHDX if using UEFI or WindowsToGo layouts. + + .PARAMETER DiskLayout + Specifies whether to build the image for BIOS (MBR), UEFI (GPT), or WindowsToGo (MBR). + Generation 1 VMs require BIOS (MBR) images. Generation 2 VMs require UEFI (GPT) images. + Windows To Go images will boot in UEFI or BIOS but are not technically supported (upgrade + doesn't work) + + .PARAMETER UnattendPath + The complete path to an unattend.xml file that can be injected into the VHD(X). + + .PARAMETER Edition + The name or image index of the image to apply from the WIM. + + .PARAMETER Passthru + Specifies that the full path to the VHD(X) that is created should be + returned on the pipeline. + + .PARAMETER BCDBoot + By default, the version of BCDBOOT.EXE that is present in \Windows\System32 + is used by Convert-WindowsImage. If you need to specify an alternate version, + use this parameter to do so. + + .PARAMETER MergeFolder + Specifies additional MergeFolder path to be added to the root of the VHD(X) + + .PARAMETER BCDinVHD + Specifies the purpose of the VHD(x). Use NativeBoot to skip cration of BCD store + inside the VHD(x). Use VirtualMachine (or do not specify this option) to ensure + the BCD store is created inside the VHD(x). + + .PARAMETER Driver + Full path to driver(s) (.inf files) to inject to the OS inside the VHD(x). + + .PARAMETER ExpandOnNativeBoot + Specifies whether to expand the VHD(x) to its maximum suze upon native boot. + The default is True. Set to False to disable expansion. + + .PARAMETER RemoteDesktopEnable + Enable Remote Desktop to connect to the OS inside the VHD(x) upon provisioning. + Does not include Windows Firewall rules (firewall exceptions). The default is False. + + .PARAMETER Feature + Enables specified Windows Feature(s). Note that you need to specify the Internal names + understood by DISM and DISM CMDLets (e.g. NetFx3) instead of the "Friendly" names + from Server Manager CMDLets (e.g. NET-Framework-Core). + + .PARAMETER Package + Injects specified Windows Package(s). Accepts path to either a directory or individual + CAB or MSU file. + + .PARAMETER ShowUI + Specifies that the Graphical User Interface should be displayed. + + .PARAMETER EnableDebugger + Configures kernel debugging for the VHD(X) being created. + EnableDebugger takes a single argument which specifies the debugging transport to use. + Valid transports are: None, Serial, 1394, USB, Network, Local. + + Depending on the type of transport selected, additional configuration parameters will become + available. + + Serial: + -ComPort - The COM port number to use while communicating with the debugger. + The default value is 1 (indicating COM1). + -BaudRate - The baud rate (in bps) to use while communicating with the debugger. + The default value is 115200, valid values are: + 9600, 19200, 38400, 56700, 115200 + + 1394: + -Channel - The 1394 channel used to communicate with the debugger. + The default value is 10. + + USB: + -Target - The target name used for USB debugging. + The default value is "debugging". + + Network: + -IPAddress - The IP address of the debugging host computer. + -Port - The port on which to connect to the debugging host. + The default value is 50000, with a minimum value of 49152. + -Key - The key used to encrypt the connection. Only [0-9] and [a-z] are allowed. + -nodhcp - Prevents the use of DHCP to obtain the target IP address. + -newkey - Specifies that a new encryption key should be generated for the connection. + + .PARAMETER DismPath + Full Path to an alternative version of the Dism.exe tool. The default is the current OS version. + + .PARAMETER ApplyEA + Specifies that any EAs captured in the WIM should be applied to the VHD. + The default is False. + + .EXAMPLE + .\Convert-WindowsImage.ps1 -SourcePath D:\foo\install.wim -Edition Professional -WorkingDirectory D:\foo + + This command will create a 40GB dynamically expanding VHD in the D:\foo folder. + The VHD will be based on the Professional edition from D:\foo\install.wim, + and will be named automatically. + + .EXAMPLE + .\Convert-WindowsImage.ps1 -SourcePath D:\foo\Win7SP1.iso -Edition Ultimate -VHDPath D:\foo\Win7_Ultimate_SP1.vhd + + This command will parse the ISO file D:\foo\Win7SP1.iso and try to locate + \sources\install.wim. If that file is found, it will be used to create a + dynamically-expanding 40GB VHD containing the Ultimate SKU, and will be + named D:\foo\Win7_Ultimate_SP1.vhd + + .EXAMPLE + .\Convert-WindowsImage.ps1 -SourcePath D:\foo\install.wim -Edition Professional -EnableDebugger Serial -ComPort 2 -BaudRate 38400 + + This command will create a VHD from D:\foo\install.wim of the Professional SKU. + Serial debugging will be enabled in the VHD via COM2 at a baud rate of 38400bps. + + .OUTPUTS + System.IO.FileInfo + #> + #Requires -Version 3.0 + [CmdletBinding(DefaultParameterSetName="SRC", + HelpURI="https://github.com/Microsoft/Virtualization-Documentation/tree/master/hyperv-tools/Convert-WindowsImage")] + + param( + [Parameter(ParameterSetName="SRC", Mandatory=$true, ValueFromPipeline=$true)] + [Alias("WIM")] + [string] + [ValidateNotNullOrEmpty()] + [ValidateScript({ Test-Path $(Resolve-Path $_) })] + $SourcePath, + + [Parameter(ParameterSetName="SRC")] + [switch] + $CacheSource = $false, + + [Parameter(ParameterSetName="SRC")] + [Alias("SKU")] + [string[]] + [ValidateNotNullOrEmpty()] + $Edition, + + [Parameter(ParameterSetName="SRC")] + [Alias("WorkDir")] + [string] + [ValidateNotNullOrEmpty()] + [ValidateScript({ Test-Path $_ })] + $WorkingDirectory = $pwd, + + [Parameter(ParameterSetName="SRC")] + [Alias("TempDir")] + [string] + [ValidateNotNullOrEmpty()] + $TempDirectory = $env:Temp, + + [Parameter(ParameterSetName="SRC")] + [Alias("VHD")] + [string] + [ValidateNotNullOrEmpty()] + $VHDPath, + + [Parameter(ParameterSetName="SRC")] + [Alias("Size")] + [UInt64] + [ValidateNotNullOrEmpty()] + [ValidateRange(512MB, 64TB)] + $SizeBytes = 25GB, + + [Parameter(ParameterSetName="SRC")] + [Alias("Format")] + [string] + [ValidateNotNullOrEmpty()] + [ValidateSet("VHD", "VHDX", "AUTO")] + $VHDFormat = "AUTO", + + [Parameter(ParameterSetName="SRC")] + [Alias("MergeFolder")] + [string] + [ValidateNotNullOrEmpty()] + $MergeFolderPath = "", + + [Parameter(ParameterSetName="SRC", Mandatory=$true)] + [Alias("Layout")] + [string] + [ValidateNotNullOrEmpty()] + [ValidateSet("BIOS", "UEFI", "WindowsToGo")] + $DiskLayout, + + [Parameter(ParameterSetName="SRC")] + [string] + [ValidateNotNullOrEmpty()] + [ValidateSet("NativeBoot", "VirtualMachine")] + $BCDinVHD = "VirtualMachine", + + [Parameter(ParameterSetName="SRC")] + [Parameter(ParameterSetName="UI")] + [string] + $BCDBoot = "bcdboot.exe", + + [Parameter(ParameterSetName="SRC")] + [Parameter(ParameterSetName="UI")] + [string] + [ValidateNotNullOrEmpty()] + [ValidateSet("None", "Serial", "1394", "USB", "Local", "Network")] + $EnableDebugger = "None", + + [Parameter(ParameterSetName="SRC")] + [string[]] + [ValidateNotNullOrEmpty()] + $Feature, + + [Parameter(ParameterSetName="SRC")] + [string[]] + [ValidateNotNullOrEmpty()] + [ValidateScript({ Test-Path $(Resolve-Path $_) })] + $Driver, + + [Parameter(ParameterSetName="SRC")] + [string[]] + [ValidateNotNullOrEmpty()] + [ValidateScript({ Test-Path $(Resolve-Path $_) })] + $Package, + + [Parameter(ParameterSetName="SRC")] + [switch] + $ExpandOnNativeBoot = $true, + + [Parameter(ParameterSetName="SRC")] + [switch] + $RemoteDesktopEnable = $false, + + [Parameter(ParameterSetName="SRC")] + [Alias("Unattend")] + [string] + [ValidateNotNullOrEmpty()] + [ValidateScript({ Test-Path $(Resolve-Path $_) })] + $UnattendPath, + + [Parameter(ParameterSetName="SRC")] + [Parameter(ParameterSetName="UI")] + [switch] + $Passthru, + + [Parameter(ParameterSetName="SRC")] + [string] + [ValidateNotNullOrEmpty()] + [ValidateScript({ Test-Path $(Resolve-Path $_) })] + $DismPath, + + [Parameter(ParameterSetName="SRC")] + [switch] + $ApplyEA = $false, + + [Parameter(ParameterSetName="UI")] + [switch] + $ShowUI + ) + #region Code + + # Begin Dynamic Parameters + # Create the parameters for the various types of debugging. + DynamicParam + { + Set-StrictMode -version 3 + + # Set up the dynamic parameters. + # Dynamic parameters are only available if certain conditions are met, so they'll only show up + # as valid parameters when those conditions apply. Here, the conditions are based on the value of + # the EnableDebugger parameter. Depending on which of a set of values is the specified argument + # for EnableDebugger, different parameters will light up, as outlined below. + + $parameterDictionary = New-Object System.Management.Automation.RuntimeDefinedParameterDictionary + + if (!(Test-Path Variable:Private:EnableDebugger)) + { + return $parameterDictionary + } + + switch ($EnableDebugger) + { + "Serial" + { + #region ComPort + + $ComPortAttr = New-Object System.Management.Automation.ParameterAttribute + $ComPortAttr.ParameterSetName = "__AllParameterSets" + $ComPortAttr.Mandatory = $false + + $ComPortValidator = New-Object System.Management.Automation.ValidateRangeAttribute( + 1, + 10 # Is that a good maximum? + ) + + $ComPortNotNull = New-Object System.Management.Automation.ValidateNotNullOrEmptyAttribute + + $ComPortAttrCollection = New-Object System.Collections.ObjectModel.Collection[System.Attribute] + $ComPortAttrCollection.Add($ComPortAttr) + $ComPortAttrCollection.Add($ComPortValidator) + $ComPortAttrCollection.Add($ComPortNotNull) + + $ComPort = New-Object System.Management.Automation.RuntimeDefinedParameter( + "ComPort", + [UInt16], + $ComPortAttrCollection + ) + + # By default, use COM1 + $ComPort.Value = 1 + $parameterDictionary.Add("ComPort", $ComPort) + #endregion ComPort + + #region BaudRate + $BaudRateAttr = New-Object System.Management.Automation.ParameterAttribute + $BaudRateAttr.ParameterSetName = "__AllParameterSets" + $BaudRateAttr.Mandatory = $false + + $BaudRateValidator = New-Object System.Management.Automation.ValidateSetAttribute( + 9600, 19200,38400, 57600, 115200 + ) + + $BaudRateNotNull = New-Object System.Management.Automation.ValidateNotNullOrEmptyAttribute + + $BaudRateAttrCollection = New-Object System.Collections.ObjectModel.Collection[System.Attribute] + $BaudRateAttrCollection.Add($BaudRateAttr) + $BaudRateAttrCollection.Add($BaudRateValidator) + $BaudRateAttrCollection.Add($BaudRateNotNull) + + $BaudRate = New-Object System.Management.Automation.RuntimeDefinedParameter( + "BaudRate", + [UInt32], + $BaudRateAttrCollection + ) + + # By default, use 115,200. + $BaudRate.Value = 115200 + $parameterDictionary.Add("BaudRate", $BaudRate) + #endregion BaudRate + + break + } + + "1394" + { + $ChannelAttr = New-Object System.Management.Automation.ParameterAttribute + $ChannelAttr.ParameterSetName = "__AllParameterSets" + $ChannelAttr.Mandatory = $false + + $ChannelValidator = New-Object System.Management.Automation.ValidateRangeAttribute( + 0, + 62 + ) + + $ChannelNotNull = New-Object System.Management.Automation.ValidateNotNullOrEmptyAttribute + + $ChannelAttrCollection = New-Object System.Collections.ObjectModel.Collection[System.Attribute] + $ChannelAttrCollection.Add($ChannelAttr) + $ChannelAttrCollection.Add($ChannelValidator) + $ChannelAttrCollection.Add($ChannelNotNull) + + $Channel = New-Object System.Management.Automation.RuntimeDefinedParameter( + "Channel", + [UInt16], + $ChannelAttrCollection + ) + + # By default, use channel 10 + $Channel.Value = 10 + $parameterDictionary.Add("Channel", $Channel) + break + } + + "USB" + { + $TargetAttr = New-Object System.Management.Automation.ParameterAttribute + $TargetAttr.ParameterSetName = "__AllParameterSets" + $TargetAttr.Mandatory = $false + + $TargetNotNull = New-Object System.Management.Automation.ValidateNotNullOrEmptyAttribute + + $TargetAttrCollection = New-Object System.Collections.ObjectModel.Collection[System.Attribute] + $TargetAttrCollection.Add($TargetAttr) + $TargetAttrCollection.Add($TargetNotNull) + + $Target = New-Object System.Management.Automation.RuntimeDefinedParameter( + "Target", + [string], + $TargetAttrCollection + ) + + # By default, use target = "debugging" + $Target.Value = "Debugging" + $parameterDictionary.Add("Target", $Target) + break + } + + "Network" + { + #region IP + $IpAttr = New-Object System.Management.Automation.ParameterAttribute + $IpAttr.ParameterSetName = "__AllParameterSets" + $IpAttr.Mandatory = $true + + $IpValidator = New-Object System.Management.Automation.ValidatePatternAttribute( + "\b(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\b" + ) + $IpNotNull = New-Object System.Management.Automation.ValidateNotNullOrEmptyAttribute + + $IpAttrCollection = New-Object System.Collections.ObjectModel.Collection[System.Attribute] + $IpAttrCollection.Add($IpAttr) + $IpAttrCollection.Add($IpValidator) + $IpAttrCollection.Add($IpNotNull) + + $IP = New-Object System.Management.Automation.RuntimeDefinedParameter( + "IPAddress", + [string], + $IpAttrCollection + ) + + # There's no good way to set a default value for this. + $parameterDictionary.Add("IPAddress", $IP) + #endregion IP + + #region Port + $PortAttr = New-Object System.Management.Automation.ParameterAttribute + $PortAttr.ParameterSetName = "__AllParameterSets" + $PortAttr.Mandatory = $false + + $PortValidator = New-Object System.Management.Automation.ValidateRangeAttribute( + 49152, + 50039 + ) + + $PortNotNull = New-Object System.Management.Automation.ValidateNotNullOrEmptyAttribute + + $PortAttrCollection = New-Object System.Collections.ObjectModel.Collection[System.Attribute] + $PortAttrCollection.Add($PortAttr) + $PortAttrCollection.Add($PortValidator) + $PortAttrCollection.Add($PortNotNull) + + + $Port = New-Object System.Management.Automation.RuntimeDefinedParameter( + "Port", + [UInt16], + $PortAttrCollection + ) + + # By default, use port 50000 + $Port.Value = 50000 + $parameterDictionary.Add("Port", $Port) + #endregion Port + + #region Key + $KeyAttr = New-Object System.Management.Automation.ParameterAttribute + $KeyAttr.ParameterSetName = "__AllParameterSets" + $KeyAttr.Mandatory = $true + + $KeyValidator = New-Object System.Management.Automation.ValidatePatternAttribute( + "\b([A-Z0-9]+).([A-Z0-9]+).([A-Z0-9]+).([A-Z0-9]+)\b" + ) + + $KeyNotNull = New-Object System.Management.Automation.ValidateNotNullOrEmptyAttribute + + $KeyAttrCollection = New-Object System.Collections.ObjectModel.Collection[System.Attribute] + $KeyAttrCollection.Add($KeyAttr) + $KeyAttrCollection.Add($KeyValidator) + $KeyAttrCollection.Add($KeyNotNull) + + $Key = New-Object System.Management.Automation.RuntimeDefinedParameter( + "Key", + [string], + $KeyAttrCollection + ) + + # Don't set a default key. + $parameterDictionary.Add("Key", $Key) + #endregion Key + + #region NoDHCP + $NoDHCPAttr = New-Object System.Management.Automation.ParameterAttribute + $NoDHCPAttr.ParameterSetName = "__AllParameterSets" + $NoDHCPAttr.Mandatory = $false + + $NoDHCPAttrCollection = New-Object System.Collections.ObjectModel.Collection[System.Attribute] + $NoDHCPAttrCollection.Add($NoDHCPAttr) + + $NoDHCP = New-Object System.Management.Automation.RuntimeDefinedParameter( + "NoDHCP", + [switch], + $NoDHCPAttrCollection + ) + + $parameterDictionary.Add("NoDHCP", $NoDHCP) + #endregion NoDHCP + + #region NewKey + $NewKeyAttr = New-Object System.Management.Automation.ParameterAttribute + $NewKeyAttr.ParameterSetName = "__AllParameterSets" + $NewKeyAttr.Mandatory = $false + + $NewKeyAttrCollection = New-Object System.Collections.ObjectModel.Collection[System.Attribute] + $NewKeyAttrCollection.Add($NewKeyAttr) + + $NewKey = New-Object System.Management.Automation.RuntimeDefinedParameter( + "NewKey", + [switch], + $NewKeyAttrCollection + ) + + # Don't set a default key. + $parameterDictionary.Add("NewKey", $NewKey) + #endregion NewKey + + break + } + + # There's nothing to do for local debugging. + # Synthetic debugging is not yet implemented. + + default + { + break + } + } + + return $parameterDictionary + } + + Begin + { + ########################################################################################## + # Constants and Pseudo-Constants + ########################################################################################## + $PARTITION_STYLE_MBR = 0x00000000 # The default value + $PARTITION_STYLE_GPT = 0x00000001 # Just in case... + + # Version information that can be populated by timebuild. + $ScriptVersion = DATA + { + ConvertFrom-StringData -StringData @" + Major = 10 + Minor = 0 + Build = 14278 + Qfe = 1000 + Branch = rs1_es_media + Timestamp = 160201-1707 + Flavor = amd64fre +"@ +} + + $myVersion = "$($ScriptVersion.Major).$($ScriptVersion.Minor).$($ScriptVersion.Build).$($ScriptVersion.QFE).$($ScriptVersion.Flavor).$($ScriptVersion.Branch).$($ScriptVersion.Timestamp)" + $scriptName = "Convert-WindowsImage" # Name of the script, obviously. + $sessionKey = [Guid]::NewGuid().ToString() # Session key, used for keeping records unique between multiple runs. + $logFolder = "$($TempDirectory)\$($scriptName)\$($sessionKey)" # Log folder path. + $vhdMaxSize = 2040GB # Maximum size for VHD is ~2040GB. + $vhdxMaxSize = 64TB # Maximum size for VHDX is ~64TB. + $lowestSupportedVersion = New-Object Version "6.1" # The lowest supported *image* version; making sure we don't run against Vista/2k8. + $lowestSupportedBuild = 9200 # The lowest supported *host* build. Set to Win8 CP. + $transcripting = $false + + # Since we use the VHDFormat in output, make it uppercase. + # We'll make it lowercase again when we use it as a file extension. + $VHDFormat = $VHDFormat.ToUpper() + ########################################################################################## + # Here Strings + ########################################################################################## + + # Banner text displayed during each run. + $header = @" + +Windows(R) Image to Virtual Hard Disk Converter for Windows(R) 10 +Copyright (C) Microsoft Corporation. All rights reserved. +Version $myVersion + +"@ + + # Text used as the banner in the UI. + $uiHeader = @" +You can use the fields below to configure the VHD or VHDX that you want to create! +"@ + + #region Helper Functions + + ########################################################################################## + # Helper Functions + ########################################################################################## + + <# + Functions to mount and dismount registry hives. + These hives will automatically be accessible via the HKLM:\ registry PSDrive. + + It should be noted that I have more confidence in using the RegLoadKey and + RegUnloadKey Win32 APIs than I do using REG.EXE - it just seems like we should + do things ourselves if we can, instead of using yet another binary. + + Consider this a TODO for future versions. + #> + Function Mount-RegistryHive + { + [CmdletBinding()] + param( + [Parameter(Mandatory = $true, ValueFromPipeline = $true, Position = 0)] + [System.IO.FileInfo] + [ValidateNotNullOrEmpty()] + [ValidateScript({ $_.Exists })] + $Hive + ) + + $mountKey = [System.Guid]::NewGuid().ToString() + $regPath = "REG.EXE" + + if (Test-Path HKLM:\$mountKey) + { + throw "The registry path already exists. I should just regenerate it, but I'm lazy." + } + + $regArgs = ( + "LOAD", + "HKLM\$mountKey", + $Hive.Fullname + ) + try + { + + Run-Executable -Executable $regPath -Arguments $regArgs + + } + catch + { + throw + } + + # Set a global variable containing the name of the mounted registry key + # so we can unmount it if there's an error. + $global:mountedHive = $mountKey + + return $mountKey + } + + ########################################################################################## + + Function Dismount-RegistryHive + { + [CmdletBinding()] + param( + [Parameter(Mandatory = $true, ValueFromPipeline = $true, Position = 0)] + [string] + [ValidateNotNullOrEmpty()] + $HiveMountPoint + ) + + $regPath = "REG.EXE" + + $regArgs = ( + "UNLOAD", + "HKLM\$($HiveMountPoint)" + ) + + Run-Executable -Executable $regPath -Arguments $regArgs + + $global:mountedHive = $null + } + + ########################################################################################## + + function + Test-Admin + { + <# + .SYNOPSIS + Short function to determine whether the logged-on user is an administrator. + + .EXAMPLE + Do you honestly need one? There are no parameters! + + .OUTPUTS + $true if user is admin. + $false if user is not an admin. + #> + [CmdletBinding()] + param() + + $currentUser = New-Object Security.Principal.WindowsPrincipal $([Security.Principal.WindowsIdentity]::GetCurrent()) + $isAdmin = $currentUser.IsInRole([Security.Principal.WindowsBuiltinRole]::Administrator) + Write-W2VTrace "isUserAdmin? $isAdmin" + + return $isAdmin + } + + ########################################################################################## + + function + Get-WindowsBuildNumber + { + $os = Get-WmiObject -Class Win32_OperatingSystem + return [int]($os.BuildNumber) + } + + ########################################################################################## + + function + Test-WindowsVersion + { + $isWin8 = ((Get-WindowsBuildNumber) -ge [int]$lowestSupportedBuild) + + Write-W2VTrace "is Windows 8 or Higher? $isWin8" + return $isWin8 + } + + ########################################################################################## + + function + Write-W2VInfo + { + # Function to make the Write-Host output a bit prettier. + [CmdletBinding()] + param( + [Parameter(Mandatory = $true, ValueFromPipeline = $true)] + [string] + [ValidateNotNullOrEmpty()] + $text + ) + Write-Host "INFO : $($text)" + } + + ########################################################################################## + + function + Write-W2VTrace + { + # Function to make the Write-Verbose output... well... exactly the same as it was before. + [CmdletBinding()] + param( + [Parameter(Mandatory = $true, ValueFromPipeline = $true)] + [string] + [ValidateNotNullOrEmpty()] + $text + ) + Write-Verbose $text + } + + ########################################################################################## + + function + Write-W2VError + { + # Function to make the Write-Host (NOT Write-Error) output prettier in the case of an error. + [CmdletBinding()] + param( + [Parameter(Mandatory = $true, ValueFromPipeline = $true)] + [string] + [ValidateNotNullOrEmpty()] + $text + ) + Write-Host "ERROR : $($text)" -ForegroundColor (Get-Host).PrivateData.ErrorForegroundColor + } + + ########################################################################################## + + function + Write-W2VWarn + { + # Function to make the Write-Host (NOT Write-Warning) output prettier. + [CmdletBinding()] + param( + [Parameter(Mandatory = $true, ValueFromPipeline = $true)] + [string] + [ValidateNotNullOrEmpty()] + $text + ) + Write-Host "WARN : $($text)" -ForegroundColor (Get-Host).PrivateData.WarningForegroundColor + } + + ########################################################################################## + + function + Run-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-W2VTrace "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-W2VTrace "Return code was $($ret.ExitCode)." + + if ($ret.ExitCode -ne $SuccessfulErrorCode) + { + throw "$Executable failed with code $($ret.ExitCode)!" + } + } + + ########################################################################################## + Function Test-IsNetworkLocation + { + <# + .SYNOPSIS + Determines whether or not a given path is a network location or a local drive. + + .DESCRIPTION + Function to determine whether or not a specified path is a local path, a UNC path, + or a mapped network drive. + + .PARAMETER Path + The path that we need to figure stuff out about, + #> + + [CmdletBinding()] + param( + [Parameter(ValueFromPipeLine = $true)] + [string] + [ValidateNotNullOrEmpty()] + $Path + ) + + $result = $false + + if ([bool]([URI]$Path).IsUNC) + { + $result = $true + } + else + { + $driveInfo = [IO.DriveInfo]((Resolve-Path $Path).Path) + + if ($driveInfo.DriveType -eq "Network") + { + $result = $true + } + } + + return $result + } + ########################################################################################## + + #endregion Helper Functions + } + + Process + { + Write-Host $header + + $disk = $null + $openWim = $null + $openIso = $null + $openImage = $null + $vhdFinalName = $null + $vhdFinalPath = $null + $mountedHive = $null + $isoPath = $null + $tempSource = $null + + if (Get-Command Get-WindowsOptionalFeature -ErrorAction SilentlyContinue) + { + try + { + $hyperVEnabled = $((Get-WindowsOptionalFeature -Online -FeatureName Microsoft-Hyper-V).State -eq "Enabled") + } + catch + { + # WinPE DISM does not support online queries. This will throw on non-WinPE machines + $winpeVersion = (Get-ItemProperty -Path 'HKLM:\Software\Microsoft\Windows NT\CurrentVersion\WinPE').Version + + Write-W2VInfo "Running WinPE version $winpeVersion" + + $hyperVEnabled = $false + } + } + else + { + $hyperVEnabled = $false + } + + $vhd = @() + + try + { + # Create log folder + if (Test-Path $logFolder) + { + $null = rd $logFolder -Force -Recurse + } + + $null = md $logFolder -Force + + # Try to start transcripting. If it's already running, we'll get an exception and swallow it. + try + { + $null = Start-Transcript -Path (Join-Path $logFolder "Convert-WindowsImageTranscript.txt") -Force -ErrorAction SilentlyContinue + $transcripting = $true + } + catch + { + Write-W2VWarn "Transcription is already running. No Convert-WindowsImage-specific transcript will be created." + $transcripting = $false + } + + # + # Add types + # + Add-WindowsImageTypes + + # Check to make sure we're running as Admin. + if (!(Test-Admin)) + { + throw "Images can only be applied by an administrator. Please launch PowerShell elevated and run this script again." + } + + # Check to make sure we're running on Win8. + if (!(Test-WindowsVersion)) + { + throw "$scriptName requires Windows 8 Consumer Preview or higher. Please use WIM2VHD.WSF (http://code.msdn.microsoft.com/wim2vhd) if you need to create VHDs from Windows 7." + } + + # Resolve the path for the unattend file. + if (![string]::IsNullOrEmpty($UnattendPath)) + { + $UnattendPath = (Resolve-Path $UnattendPath).Path + } + + if ($ShowUI) + { + + Write-W2VInfo "Launching UI..." + Add-Type -AssemblyName System.Drawing,System.Windows.Forms + + #region Form Objects + $frmMain = New-Object System.Windows.Forms.Form + $groupBox4 = New-Object System.Windows.Forms.GroupBox + $btnGo = New-Object System.Windows.Forms.Button + $groupBox3 = New-Object System.Windows.Forms.GroupBox + $txtVhdName = New-Object System.Windows.Forms.TextBox + $label6 = New-Object System.Windows.Forms.Label + $btnWrkBrowse = New-Object System.Windows.Forms.Button + $cmbVhdSizeUnit = New-Object System.Windows.Forms.ComboBox + $numVhdSize = New-Object System.Windows.Forms.NumericUpDown + $cmbVhdFormat = New-Object System.Windows.Forms.ComboBox + $label5 = New-Object System.Windows.Forms.Label + $txtWorkingDirectory = New-Object System.Windows.Forms.TextBox + $label4 = New-Object System.Windows.Forms.Label + $label3 = New-Object System.Windows.Forms.Label + $label2 = New-Object System.Windows.Forms.Label + $label7 = New-Object System.Windows.Forms.Label + $txtUnattendFile = New-Object System.Windows.Forms.TextBox + $btnUnattendBrowse = New-Object System.Windows.Forms.Button + $groupBox2 = New-Object System.Windows.Forms.GroupBox + $cmbSkuList = New-Object System.Windows.Forms.ComboBox + $label1 = New-Object System.Windows.Forms.Label + $groupBox1 = New-Object System.Windows.Forms.GroupBox + $txtSourcePath = New-Object System.Windows.Forms.TextBox + $btnBrowseWim = New-Object System.Windows.Forms.Button + $openFileDialog1 = New-Object System.Windows.Forms.OpenFileDialog + $openFolderDialog1 = New-Object System.Windows.Forms.FolderBrowserDialog + $InitialFormWindowState = New-Object System.Windows.Forms.FormWindowState + + #endregion Form Objects + + #region Event scriptblocks. + + $btnGo_OnClick = { + $frmMain.Close() + } + + $btnWrkBrowse_OnClick = { + $openFolderDialog1.RootFolder = "Desktop" + $openFolderDialog1.Description = "Select the folder you'd like your VHD(X) to be created in." + $openFolderDialog1.SelectedPath = $WorkingDirectory + + $ret = $openFolderDialog1.ShowDialog() + + if ($ret -ilike "ok") + { + $WorkingDirectory = $txtWorkingDirectory = $openFolderDialog1.SelectedPath + Write-W2VInfo "Selected Working Directory is $WorkingDirectory..." + } + } + + $btnUnattendBrowse_OnClick = { + $openFileDialog1.InitialDirectory = $pwd + $openFileDialog1.Filter = "XML files (*.xml)|*.XML|All files (*.*)|*.*" + $openFileDialog1.FilterIndex = 1 + $openFileDialog1.CheckFileExists = $true + $openFileDialog1.CheckPathExists = $true + $openFileDialog1.FileName = $null + $openFileDialog1.ShowHelp = $false + $openFileDialog1.Title = "Select an unattend file..." + + $ret = $openFileDialog1.ShowDialog() + + if ($ret -ilike "ok") + { + $UnattendPath = $txtUnattendFile.Text = $openFileDialog1.FileName + } + } + + $btnBrowseWim_OnClick = { + $openFileDialog1.InitialDirectory = $pwd + $openFileDialog1.Filter = "All compatible files (*.ISO, *.WIM)|*.ISO;*.WIM|All files (*.*)|*.*" + $openFileDialog1.FilterIndex = 1 + $openFileDialog1.CheckFileExists = $true + $openFileDialog1.CheckPathExists = $true + $openFileDialog1.FileName = $null + $openFileDialog1.ShowHelp = $false + $openFileDialog1.Title = "Select a source file..." + + $ret = $openFileDialog1.ShowDialog() + + if ($ret -ilike "ok") + { + + if (([IO.FileInfo]$openFileDialog1.FileName).Extension -ilike ".iso") + { + + if (Test-IsNetworkLocation $openFileDialog1.FileName) + { + Write-W2VInfo "Copying ISO $(Split-Path $openFileDialog1.FileName -Leaf) to temp folder..." + Write-W2VWarn "The UI may become non-responsive while this copy takes place..." + Copy-Item -Path $openFileDialog1.FileName -Destination $TempDirectory -Force + $openFileDialog1.FileName = "$($TempDirectory)\$(Split-Path $openFileDialog1.FileName -Leaf)" + } + + $txtSourcePath.Text = $isoPath = (Resolve-Path $openFileDialog1.FileName).Path + Write-W2VInfo "Opening ISO $(Split-Path $isoPath -Leaf)..." + + $openIso = Mount-DiskImage -ImagePath $isoPath -StorageType ISO -PassThru + + # Refresh the DiskImage object so we can get the real information about it. I assume this is a bug. + $openIso = Get-DiskImage -ImagePath $isoPath + $driveLetter = ($openIso | Get-Volume).DriveLetter + + $script:SourcePath = "$($driveLetter):\sources\install.wim" + + # Check to see if there's a WIM file we can muck about with. + Write-W2VInfo "Looking for $($SourcePath)..." + if (!(Test-Path $SourcePath)) + { + throw "The specified ISO does not appear to be valid Windows installation media." + } + } + else + { + $txtSourcePath.Text = $script:SourcePath = $openFileDialog1.FileName + } + + # Check to see if the WIM is local, or on a network location. If the latter, copy it locally. + if (Test-IsNetworkLocation $SourcePath) + { + Write-W2VInfo "Copying WIM $(Split-Path $SourcePath -Leaf) to temp folder..." + Write-W2VWarn "The UI may become non-responsive while this copy takes place..." + Copy-Item -Path $SourcePath -Destination $TempDirectory -Force + $txtSourcePath.Text = $script:SourcePath = "$($TempDirectory)\$(Split-Path $SourcePath -Leaf)" + } + + $script:SourcePath = (Resolve-Path $SourcePath).Path + + Write-W2VInfo "Scanning WIM metadata..." + + $tempOpenWim = $null + + try + { + $tempOpenWim = New-Object WIM2VHD.WimFile $SourcePath + + # Let's see if we're running against an unstaged build. If we are, we need to blow up. + if ($tempOpenWim.ImageNames.Contains("Windows Longhorn Client") -or + $tempOpenWim.ImageNames.Contains("Windows Longhorn Server") -or + $tempOpenWim.ImageNames.Contains("Windows Longhorn Server Core")) + { + [Windows.Forms.MessageBox]::Show( + "Convert-WindowsImage cannot run against unstaged builds. Please try again with a staged build.", + "WIM is incompatible!", + "OK", + "Error" + ) + + return + } + else + { + $tempOpenWim.Images | %{ $cmbSkuList.Items.Add($_.ImageFlags) } + $cmbSkuList.SelectedIndex = 0 + } + + } + catch + { + throw "Unable to load WIM metadata!" + } + finally + { + $tempOpenWim.Close() + Write-W2VTrace "Closing WIM metadata..." + } + } + } + + $OnLoadForm_StateCorrection = { + + # Correct the initial state of the form to prevent the .Net maximized form issue + $frmMain.WindowState = $InitialFormWindowState + } + + #endregion Event scriptblocks + + # Figure out VHD size and size unit. + $unit = $null + switch ([Math]::Round($SizeBytes.ToString().Length / 3)) + { + 3 { $unit = "MB"; break } + 4 { $unit = "GB"; break } + 5 { $unit = "TB"; break } + default { $unit = ""; break } + } + + $quantity = Invoke-Expression -Command "$($SizeBytes) / 1$($unit)" + + #region Form Code + #region frmMain + $frmMain.DataBindings.DefaultDataSourceUpdateMode = 0 + $System_Drawing_Size = New-Object System.Drawing.Size + $System_Drawing_Size.Height = 579 + $System_Drawing_Size.Width = 512 + $frmMain.ClientSize = $System_Drawing_Size + $frmMain.Font = New-Object System.Drawing.Font("Segoe UI",10,0,3,1) + $frmMain.FormBorderStyle = 1 + $frmMain.MaximizeBox = $False + $frmMain.MinimizeBox = $False + $frmMain.Name = "frmMain" + $frmMain.StartPosition = 1 + $frmMain.Text = "Convert-WindowsImage UI" + #endregion frmMain + + #region groupBox4 + $groupBox4.DataBindings.DefaultDataSourceUpdateMode = 0 + $System_Drawing_Point = New-Object System.Drawing.Point + $System_Drawing_Point.X = 10 + $System_Drawing_Point.Y = 498 + $groupBox4.Location = $System_Drawing_Point + $groupBox4.Name = "groupBox4" + $System_Drawing_Size = New-Object System.Drawing.Size + $System_Drawing_Size.Height = 69 + $System_Drawing_Size.Width = 489 + $groupBox4.Size = $System_Drawing_Size + $groupBox4.TabIndex = 8 + $groupBox4.TabStop = $False + $groupBox4.Text = "4. Make the VHD!" + + $frmMain.Controls.Add($groupBox4) + #endregion groupBox4 + + #region btnGo + $btnGo.DataBindings.DefaultDataSourceUpdateMode = 0 + $System_Drawing_Point = New-Object System.Drawing.Point + $System_Drawing_Point.X = 39 + $System_Drawing_Point.Y = 24 + $btnGo.Location = $System_Drawing_Point + $btnGo.Name = "btnGo" + $System_Drawing_Size = New-Object System.Drawing.Size + $System_Drawing_Size.Height = 33 + $System_Drawing_Size.Width = 415 + $btnGo.Size = $System_Drawing_Size + $btnGo.TabIndex = 0 + $btnGo.Text = "&Make my VHD" + $btnGo.UseVisualStyleBackColor = $True + $btnGo.DialogResult = "OK" + $btnGo.add_Click($btnGo_OnClick) + + $groupBox4.Controls.Add($btnGo) + $frmMain.AcceptButton = $btnGo + #endregion btnGo + + #region groupBox3 + $groupBox3.DataBindings.DefaultDataSourceUpdateMode = 0 + $System_Drawing_Point = New-Object System.Drawing.Point + $System_Drawing_Point.X = 10 + $System_Drawing_Point.Y = 243 + $groupBox3.Location = $System_Drawing_Point + $groupBox3.Name = "groupBox3" + $System_Drawing_Size = New-Object System.Drawing.Size + $System_Drawing_Size.Height = 245 + $System_Drawing_Size.Width = 489 + $groupBox3.Size = $System_Drawing_Size + $groupBox3.TabIndex = 7 + $groupBox3.TabStop = $False + $groupBox3.Text = "3. Choose configuration options" + + $frmMain.Controls.Add($groupBox3) + #endregion groupBox3 + + #region txtVhdName + $txtVhdName.DataBindings.DefaultDataSourceUpdateMode = 0 + $System_Drawing_Point = New-Object System.Drawing.Point + $System_Drawing_Point.X = 25 + $System_Drawing_Point.Y = 150 + $txtVhdName.Location = $System_Drawing_Point + $txtVhdName.Name = "txtVhdName" + $System_Drawing_Size = New-Object System.Drawing.Size + $System_Drawing_Size.Height = 25 + $System_Drawing_Size.Width = 418 + $txtVhdName.Size = $System_Drawing_Size + $txtVhdName.TabIndex = 10 + + $groupBox3.Controls.Add($txtVhdName) + #endregion txtVhdName + + #region txtUnattendFile + $txtUnattendFile.DataBindings.DefaultDataSourceUpdateMode = 0 + $System_Drawing_Point = New-Object System.Drawing.Point + $System_Drawing_Point.X = 25 + $System_Drawing_Point.Y = 198 + $txtUnattendFile.Location = $System_Drawing_Point + $txtUnattendFile.Name = "txtUnattendFile" + $System_Drawing_Size = New-Object System.Drawing.Size + $System_Drawing_Size.Height = 25 + $System_Drawing_Size.Width = 418 + $txtUnattendFile.Size = $System_Drawing_Size + $txtUnattendFile.TabIndex = 11 + + $groupBox3.Controls.Add($txtUnattendFile) + #endregion txtUnattendFile + + #region label7 + $label7.DataBindings.DefaultDataSourceUpdateMode = 0 + $System_Drawing_Point = New-Object System.Drawing.Point + $System_Drawing_Point.X = 23 + $System_Drawing_Point.Y = 180 + $label7.Location = $System_Drawing_Point + $label7.Name = "label7" + $System_Drawing_Size = New-Object System.Drawing.Size + $System_Drawing_Size.Height = 23 + $System_Drawing_Size.Width = 175 + $label7.Size = $System_Drawing_Size + $label7.Text = "Unattend File (Optional)" + + $groupBox3.Controls.Add($label7) + #endregion label7 + + #region label6 + $label6.DataBindings.DefaultDataSourceUpdateMode = 0 + $System_Drawing_Point = New-Object System.Drawing.Point + $System_Drawing_Point.X = 23 + $System_Drawing_Point.Y = 132 + $label6.Location = $System_Drawing_Point + $label6.Name = "label6" + $System_Drawing_Size = New-Object System.Drawing.Size + $System_Drawing_Size.Height = 23 + $System_Drawing_Size.Width = 175 + $label6.Size = $System_Drawing_Size + $label6.Text = "VHD Name (Optional)" + + $groupBox3.Controls.Add($label6) + #endregion label6 + + #region btnUnattendBrowse + $btnUnattendBrowse.DataBindings.DefaultDataSourceUpdateMode = 0 + $System_Drawing_Point = New-Object System.Drawing.Point + $System_Drawing_Point.X = 449 + $System_Drawing_Point.Y = 199 + $btnUnattendBrowse.Location = $System_Drawing_Point + $btnUnattendBrowse.Name = "btnUnattendBrowse" + $System_Drawing_Size = New-Object System.Drawing.Size + $System_Drawing_Size.Height = 25 + $System_Drawing_Size.Width = 27 + $btnUnattendBrowse.Size = $System_Drawing_Size + $btnUnattendBrowse.TabIndex = 9 + $btnUnattendBrowse.Text = "..." + $btnUnattendBrowse.UseVisualStyleBackColor = $True + $btnUnattendBrowse.add_Click($btnUnattendBrowse_OnClick) + + $groupBox3.Controls.Add($btnUnattendBrowse) + #endregion btnUnattendBrowse + + #region btnWrkBrowse + $btnWrkBrowse.DataBindings.DefaultDataSourceUpdateMode = 0 + $System_Drawing_Point = New-Object System.Drawing.Point + $System_Drawing_Point.X = 449 + $System_Drawing_Point.Y = 98 + $btnWrkBrowse.Location = $System_Drawing_Point + $btnWrkBrowse.Name = "btnWrkBrowse" + $System_Drawing_Size = New-Object System.Drawing.Size + $System_Drawing_Size.Height = 25 + $System_Drawing_Size.Width = 27 + $btnWrkBrowse.Size = $System_Drawing_Size + $btnWrkBrowse.TabIndex = 9 + $btnWrkBrowse.Text = "..." + $btnWrkBrowse.UseVisualStyleBackColor = $True + $btnWrkBrowse.add_Click($btnWrkBrowse_OnClick) + + $groupBox3.Controls.Add($btnWrkBrowse) + #endregion btnWrkBrowse + + #region cmbVhdSizeUnit + $cmbVhdSizeUnit.DataBindings.DefaultDataSourceUpdateMode = 0 + $cmbVhdSizeUnit.FormattingEnabled = $True + $cmbVhdSizeUnit.Items.Add("MB") | Out-Null + $cmbVhdSizeUnit.Items.Add("GB") | Out-Null + $cmbVhdSizeUnit.Items.Add("TB") | Out-Null + $System_Drawing_Point = New-Object System.Drawing.Point + $System_Drawing_Point.X = 409 + $System_Drawing_Point.Y = 42 + $cmbVhdSizeUnit.Location = $System_Drawing_Point + $cmbVhdSizeUnit.Name = "cmbVhdSizeUnit" + $System_Drawing_Size = New-Object System.Drawing.Size + $System_Drawing_Size.Height = 25 + $System_Drawing_Size.Width = 67 + $cmbVhdSizeUnit.Size = $System_Drawing_Size + $cmbVhdSizeUnit.TabIndex = 5 + $cmbVhdSizeUnit.Text = $unit + + $groupBox3.Controls.Add($cmbVhdSizeUnit) + #endregion cmbVhdSizeUnit + + #region numVhdSize + $numVhdSize.DataBindings.DefaultDataSourceUpdateMode = 0 + $System_Drawing_Point = New-Object System.Drawing.Point + $System_Drawing_Point.X = 340 + $System_Drawing_Point.Y = 42 + $numVhdSize.Location = $System_Drawing_Point + $numVhdSize.Name = "numVhdSize" + $System_Drawing_Size = New-Object System.Drawing.Size + $System_Drawing_Size.Height = 25 + $System_Drawing_Size.Width = 63 + $numVhdSize.Size = $System_Drawing_Size + $numVhdSize.TabIndex = 4 + $numVhdSize.Value = $quantity + + $groupBox3.Controls.Add($numVhdSize) + #endregion numVhdSize + + #region cmbVhdFormat + $cmbVhdFormat.DataBindings.DefaultDataSourceUpdateMode = 0 + $cmbVhdFormat.FormattingEnabled = $True + $cmbVhdFormat.Items.Add("VHD") | Out-Null + $cmbVhdFormat.Items.Add("VHDX") | Out-Null + $System_Drawing_Point = New-Object System.Drawing.Point + $System_Drawing_Point.X = 25 + $System_Drawing_Point.Y = 42 + $cmbVhdFormat.Location = $System_Drawing_Point + $cmbVhdFormat.Name = "cmbVhdFormat" + $System_Drawing_Size = New-Object System.Drawing.Size + $System_Drawing_Size.Height = 25 + $System_Drawing_Size.Width = 136 + $cmbVhdFormat.Size = $System_Drawing_Size + $cmbVhdFormat.TabIndex = 0 + $cmbVhdFormat.Text = $VHDFormat + + $groupBox3.Controls.Add($cmbVhdFormat) + #endregion cmbVhdFormat + + #region label5 + $label5.DataBindings.DefaultDataSourceUpdateMode = 0 + $System_Drawing_Point = New-Object System.Drawing.Point + $System_Drawing_Point.X = 23 + $System_Drawing_Point.Y = 76 + $label5.Location = $System_Drawing_Point + $label5.Name = "label5" + $System_Drawing_Size = New-Object System.Drawing.Size + $System_Drawing_Size.Height = 23 + $System_Drawing_Size.Width = 264 + $label5.Size = $System_Drawing_Size + $label5.TabIndex = 8 + $label5.Text = "Working Directory" + + $groupBox3.Controls.Add($label5) + #endregion label5 + + #region txtWorkingDirectory + $txtWorkingDirectory.DataBindings.DefaultDataSourceUpdateMode = 0 + $System_Drawing_Point = New-Object System.Drawing.Point + $System_Drawing_Point.X = 25 + $System_Drawing_Point.Y = 99 + $txtWorkingDirectory.Location = $System_Drawing_Point + $txtWorkingDirectory.Name = "txtWorkingDirectory" + $System_Drawing_Size = New-Object System.Drawing.Size + $System_Drawing_Size.Height = 25 + $System_Drawing_Size.Width = 418 + $txtWorkingDirectory.Size = $System_Drawing_Size + $txtWorkingDirectory.TabIndex = 7 + $txtWorkingDirectory.Text = $WorkingDirectory + + $groupBox3.Controls.Add($txtWorkingDirectory) + #endregion txtWorkingDirectory + + #region label4 + $label4.DataBindings.DefaultDataSourceUpdateMode = 0 + $System_Drawing_Point = New-Object System.Drawing.Point + $System_Drawing_Point.X = 340 + $System_Drawing_Point.Y = 21 + $label4.Location = $System_Drawing_Point + $label4.Name = "label4" + $System_Drawing_Size = New-Object System.Drawing.Size + $System_Drawing_Size.Height = 27 + $System_Drawing_Size.Width = 86 + $label4.Size = $System_Drawing_Size + $label4.TabIndex = 6 + $label4.Text = "VHD Size" + + $groupBox3.Controls.Add($label4) + #endregion label4 + + #region label3 + $label3.DataBindings.DefaultDataSourceUpdateMode = 0 + $System_Drawing_Point = New-Object System.Drawing.Point + $System_Drawing_Point.X = 176 + $System_Drawing_Point.Y = 21 + $label3.Location = $System_Drawing_Point + $label3.Name = "label3" + $System_Drawing_Size = New-Object System.Drawing.Size + $System_Drawing_Size.Height = 27 + $System_Drawing_Size.Width = 92 + $label3.Size = $System_Drawing_Size + $label3.TabIndex = 3 + $label3.Text = "VHD Type" + + $groupBox3.Controls.Add($label3) + #endregion label3 + + #region label2 + $label2.DataBindings.DefaultDataSourceUpdateMode = 0 + $System_Drawing_Point = New-Object System.Drawing.Point + $System_Drawing_Point.X = 25 + $System_Drawing_Point.Y = 21 + $label2.Location = $System_Drawing_Point + $label2.Name = "label2" + $System_Drawing_Size = New-Object System.Drawing.Size + $System_Drawing_Size.Height = 30 + $System_Drawing_Size.Width = 118 + $label2.Size = $System_Drawing_Size + $label2.TabIndex = 1 + $label2.Text = "VHD Format" + + $groupBox3.Controls.Add($label2) + #endregion label2 + + #region groupBox2 + $groupBox2.DataBindings.DefaultDataSourceUpdateMode = 0 + $System_Drawing_Point = New-Object System.Drawing.Point + $System_Drawing_Point.X = 10 + $System_Drawing_Point.Y = 169 + $groupBox2.Location = $System_Drawing_Point + $groupBox2.Name = "groupBox2" + $System_Drawing_Size = New-Object System.Drawing.Size + $System_Drawing_Size.Height = 68 + $System_Drawing_Size.Width = 490 + $groupBox2.Size = $System_Drawing_Size + $groupBox2.TabIndex = 6 + $groupBox2.TabStop = $False + $groupBox2.Text = "2. Choose a SKU from the list" + + $frmMain.Controls.Add($groupBox2) + #endregion groupBox2 + + #region cmbSkuList + $cmbSkuList.DataBindings.DefaultDataSourceUpdateMode = 0 + $cmbSkuList.FormattingEnabled = $True + $System_Drawing_Point = New-Object System.Drawing.Point + $System_Drawing_Point.X = 25 + $System_Drawing_Point.Y = 24 + $cmbSkuList.Location = $System_Drawing_Point + $cmbSkuList.Name = "cmbSkuList" + $System_Drawing_Size = New-Object System.Drawing.Size + $System_Drawing_Size.Height = 25 + $System_Drawing_Size.Width = 452 + $cmbSkuList.Size = $System_Drawing_Size + $cmbSkuList.TabIndex = 2 + + $groupBox2.Controls.Add($cmbSkuList) + #endregion cmbSkuList + + #region label1 + $label1.DataBindings.DefaultDataSourceUpdateMode = 0 + $System_Drawing_Point = New-Object System.Drawing.Point + $System_Drawing_Point.X = 23 + $System_Drawing_Point.Y = 21 + $label1.Location = $System_Drawing_Point + $label1.Name = "label1" + $System_Drawing_Size = New-Object System.Drawing.Size + $System_Drawing_Size.Height = 71 + $System_Drawing_Size.Width = 464 + $label1.Size = $System_Drawing_Size + $label1.TabIndex = 5 + $label1.Text = $uiHeader + + $frmMain.Controls.Add($label1) + #endregion label1 + + #region groupBox1 + $groupBox1.DataBindings.DefaultDataSourceUpdateMode = 0 + $System_Drawing_Point = New-Object System.Drawing.Point + $System_Drawing_Point.X = 10 + $System_Drawing_Point.Y = 95 + $groupBox1.Location = $System_Drawing_Point + $groupBox1.Name = "groupBox1" + $System_Drawing_Size = New-Object System.Drawing.Size + $System_Drawing_Size.Height = 68 + $System_Drawing_Size.Width = 490 + $groupBox1.Size = $System_Drawing_Size + $groupBox1.TabIndex = 4 + $groupBox1.TabStop = $False + $groupBox1.Text = "1. Choose a source" + + $frmMain.Controls.Add($groupBox1) + #endregion groupBox1 + + #region txtSourcePath + $txtSourcePath.DataBindings.DefaultDataSourceUpdateMode = 0 + $System_Drawing_Point = New-Object System.Drawing.Point + $System_Drawing_Point.X = 25 + $System_Drawing_Point.Y = 24 + $txtSourcePath.Location = $System_Drawing_Point + $txtSourcePath.Name = "txtSourcePath" + $System_Drawing_Size = New-Object System.Drawing.Size + $System_Drawing_Size.Height = 25 + $System_Drawing_Size.Width = 418 + $txtSourcePath.Size = $System_Drawing_Size + $txtSourcePath.TabIndex = 0 + + $groupBox1.Controls.Add($txtSourcePath) + #endregion txtSourcePath + + #region btnBrowseWim + $btnBrowseWim.DataBindings.DefaultDataSourceUpdateMode = 0 + $System_Drawing_Point = New-Object System.Drawing.Point + $System_Drawing_Point.X = 449 + $System_Drawing_Point.Y = 24 + $btnBrowseWim.Location = $System_Drawing_Point + $btnBrowseWim.Name = "btnBrowseWim" + $System_Drawing_Size = New-Object System.Drawing.Size + $System_Drawing_Size.Height = 25 + $System_Drawing_Size.Width = 28 + $btnBrowseWim.Size = $System_Drawing_Size + $btnBrowseWim.TabIndex = 1 + $btnBrowseWim.Text = "..." + $btnBrowseWim.UseVisualStyleBackColor = $True + $btnBrowseWim.add_Click($btnBrowseWim_OnClick) + + $groupBox1.Controls.Add($btnBrowseWim) + #endregion btnBrowseWim + + $openFileDialog1.FileName = "openFileDialog1" + $openFileDialog1.ShowHelp = $True + + #endregion Form Code + + # Save the initial state of the form + $InitialFormWindowState = $frmMain.WindowState + + # Init the OnLoad event to correct the initial state of the form + $frmMain.add_Load($OnLoadForm_StateCorrection) + + # Return the constructed form. + $ret = $frmMain.ShowDialog() + + if (!($ret -ilike "OK")) + { + throw "Form session has been cancelled." + } + + if ([string]::IsNullOrEmpty($SourcePath)) + { + throw "No source path specified." + } + + # VHD Format + $VHDFormat = $cmbVhdFormat.SelectedItem + + # VHD Size + $SizeBytes = Invoke-Expression "$($numVhdSize.Value)$($cmbVhdSizeUnit.SelectedItem)" + + # Working Directory + $WorkingDirectory = $txtWorkingDirectory.Text + + # VHDPath + if (![string]::IsNullOrEmpty($txtVhdName.Text)) + { + $VHDPath = "$($WorkingDirectory)\$($txtVhdName.Text)" + } + + # Edition + if (![string]::IsNullOrEmpty($cmbSkuList.SelectedItem)) + { + $Edition = $cmbSkuList.SelectedItem + } + + # Because we used ShowDialog, we need to manually dispose of the form. + # This probably won't make much of a difference, but let's free up all of the resources we can + # before we start the conversion process. + + $frmMain.Dispose() + } + + if ($VHDFormat -ilike "AUTO") + { + if ($DiskLayout -eq "BIOS") + { + $VHDFormat = "VHD" + } + else + { + $VHDFormat = "VHDX" + } + } + + # + # Choose smallest supported block size for dynamic VHD(X) + # + $BlockSizeBytes = 1MB + + # There's a difference between the maximum sizes for VHDs and VHDXs. Make sure we follow it. + if ("VHD" -ilike $VHDFormat) + { + if ($SizeBytes -gt $vhdMaxSize) + { + Write-W2VWarn "For the VHD file format, the maximum file size is ~2040GB. We're automatically setting the size to 2040GB for you." + $SizeBytes = 2040GB + } + + $BlockSizeBytes = 512KB + } + + # Check if -VHDPath and -WorkingDirectory were both specified. + if ((![String]::IsNullOrEmpty($VHDPath)) -and (![String]::IsNullOrEmpty($WorkingDirectory))) + { + if ($WorkingDirectory -ne $pwd) + { + # If the WorkingDirectory is anything besides $pwd, tell people that the WorkingDirectory is being ignored. + Write-W2VWarn "Specifying -VHDPath and -WorkingDirectory at the same time is contradictory." + Write-W2VWarn "Ignoring the WorkingDirectory specification." + $WorkingDirectory = Split-Path $VHDPath -Parent + } + } + + if ($VHDPath) + { + # Check to see if there's a conflict between the specified file extension and the VHDFormat being used. + $ext = ([IO.FileInfo]$VHDPath).Extension + + if (!($ext -ilike ".$($VHDFormat)")) + { + throw "There is a mismatch between the VHDPath file extension ($($ext.ToUpper())), and the VHDFormat (.$($VHDFormat)). Please ensure that these match and try again." + } + } + + # Create a temporary name for the VHD(x). We'll name it properly at the end of the script. + if ([String]::IsNullOrEmpty($VHDPath)) + { + $VHDPath = Join-Path $WorkingDirectory "$($sessionKey).$($VHDFormat.ToLower())" + } + else + { + # Since we can't do Resolve-Path against a file that doesn't exist, we need to get creative in determining + # the full path that the user specified (or meant to specify if they gave us a relative path). + # Check to see if the path has a root specified. If it doesn't, use the working directory. + if (![IO.Path]::IsPathRooted($VHDPath)) + { + $VHDPath = Join-Path $WorkingDirectory $VHDPath + } + + $vhdFinalName = Split-Path $VHDPath -Leaf + $VHDPath = Join-Path (Split-Path $VHDPath -Parent) "$($sessionKey).$($VHDFormat.ToLower())" + } + + Write-W2VTrace "Temporary $VHDFormat path is : $VHDPath" + + # If we're using an ISO, mount it and get the path to the WIM file. + if (([IO.FileInfo]$SourcePath).Extension -ilike ".ISO") + { + # If the ISO isn't local, copy it down so we don't have to worry about resource contention + # or about network latency. + if (Test-IsNetworkLocation $SourcePath) + { + Write-W2VInfo "Copying ISO $(Split-Path $SourcePath -Leaf) to temp folder..." + robocopy $(Split-Path $SourcePath -Parent) $TempDirectory $(Split-Path $SourcePath -Leaf) | Out-Null + $SourcePath = "$($TempDirectory)\$(Split-Path $SourcePath -Leaf)" + + $tempSource = $SourcePath + } + + $isoPath = (Resolve-Path $SourcePath).Path + + Write-W2VInfo "Opening ISO $(Split-Path $isoPath -Leaf)..." + $openIso = Mount-DiskImage -ImagePath $isoPath -StorageType ISO -PassThru + # Refresh the DiskImage object so we can get the real information about it. I assume this is a bug. + $openIso = Get-DiskImage -ImagePath $isoPath + $driveLetter = ($openIso | Get-Volume).DriveLetter + + $SourcePath = "$($driveLetter):\sources\install.wim" + + # Check to see if there's a WIM file we can muck about with. + Write-W2VInfo "Looking for $($SourcePath)..." + if (!(Test-Path $SourcePath)) + { + throw "The specified ISO does not appear to be valid Windows installation media." + } + } + + # Check to see if the WIM is local, or on a network location. If the latter, copy it locally. + if (Test-IsNetworkLocation $SourcePath) + { + Write-W2VInfo "Copying WIM $(Split-Path $SourcePath -Leaf) to temp folder..." + robocopy $(Split-Path $SourcePath -Parent) $TempDirectory $(Split-Path $SourcePath -Leaf) | Out-Null + $SourcePath = "$($TempDirectory)\$(Split-Path $SourcePath -Leaf)" + + $tempSource = $SourcePath + } + + $SourcePath = (Resolve-Path $SourcePath).Path + + #################################################################################################### + # QUERY WIM INFORMATION AND EXTRACT THE INDEX OF TARGETED IMAGE + #################################################################################################### + + Write-W2VInfo "Looking for the requested Windows image in the WIM file" + $WindowsImage = Get-WindowsImage -ImagePath $SourcePath + + if (-not $WindowsImage -or ($WindowsImage -is [System.Array])) + { + # + # WIM may have multiple images. Filter on Edition (can be index or name) and try to find a unique image + # + $EditionIndex = 0; + if ([Int32]::TryParse($Edition, [ref]$EditionIndex)) + { + $WindowsImage = Get-WindowsImage -ImagePath $SourcePath -Index $EditionIndex + } + else + { + $WindowsImage = Get-WindowsImage -ImagePath $SourcePath | Where-Object {$_.ImageName -ilike "*$($Edition)"} + } + + if (-not $WindowsImage) + { + throw "Requested windows Image was not found on the WIM file!" + } + if ($WindowsImage -is [System.Array]) + { + Write-W2VInfo "WIM file has the following $($WindowsImage.Count) images that match filter *$($Edition)" + Get-WindowsImage -ImagePath $SourcePath + + Write-W2VError "You must specify an Edition or SKU index, since the WIM has more than one image." + throw "There are more than one images that match ImageName filter *$($Edition)" + } + } + + $ImageIndex = $WindowsImage[0].ImageIndex + + # We're good. Open the WIM container. + # NOTE: this is only required because we want to get the XML-based meta-data at the end. Is there a better way? + # If we can get this information from DISM cmdlets, we can remove the openWim constructs + $openWim = New-Object WIM2VHD.WimFile $SourcePath + + $openImage = $openWim[[Int32]$ImageIndex] + + if ($null -eq $openImage) + { + Write-W2VError "The specified edition does not appear to exist in the specified WIM." + Write-W2VError "Valid edition names are:" + $openWim.Images | %{ Write-W2VError " $($_.ImageFlags)" } + throw + } + + Write-W2VInfo "Image $($openImage.ImageIndex) selected ($($openImage.ImageFlags))..." + + # Check to make sure that the image we're applying is Windows 7 or greater. + if ($openImage.ImageVersion -lt $lowestSupportedVersion) + { + if ($openImage.ImageVersion -eq "0.0.0.0") + { + Write-W2VWarn "The specified WIM does not encode the Windows version." + } + else + { + throw "Convert-WindowsImage only supports Windows 7 and Windows 8 WIM files. The specified image (version $($openImage.ImageVersion)) does not appear to contain one of those operating systems." + } + } + + if ($hyperVEnabled) + { + Write-W2VInfo "Creating sparse disk..." + $newVhd = New-VHD -Path $VHDPath -SizeBytes $SizeBytes -BlockSizeBytes $BlockSizeBytes -Dynamic + + Write-W2VInfo "Mounting $VHDFormat..." + $disk = $newVhd | Mount-VHD -PassThru | Get-Disk + } + else + { + <# + Create the VHD using the VirtDisk Win32 API. + So, why not use the New-VHD cmdlet here? + + New-VHD depends on the Hyper-V Cmdlets, which aren't installed by default. + Installing those cmdlets isn't a big deal, but they depend on the Hyper-V WMI + APIs, which in turn depend on Hyper-V. In order to prevent Convert-WindowsImage + from being dependent on Hyper-V (and thus, x64 systems only), we're using the + VirtDisk APIs directly. + #> + + Write-W2VInfo "Creating sparse disk..." + [WIM2VHD.VirtualHardDisk]::CreateSparseDisk( + $VHDFormat, + $VHDPath, + $SizeBytes, + $true + ) + + # Attach the VHD.\ + Write-W2VInfo "Attaching $VHDFormat..." + $disk = Mount-DiskImage -ImagePath $VHDPath -PassThru | Get-DiskImage | Get-Disk + } + + switch ($DiskLayout) + { + "BIOS" + { + Write-W2VInfo "Initializing disk..." + Initialize-Disk -Number $disk.Number -PartitionStyle MBR + + # + # Create the Windows/system partition + # + Write-W2VInfo "Creating single partition..." + $systemPartition = New-Partition -DiskNumber $disk.Number -UseMaximumSize -MbrType IFS -IsActive + $windowsPartition = $systemPartition + + Write-W2VInfo "Formatting windows volume..." + $systemVolume = Format-Volume -Partition $systemPartition -FileSystem NTFS -Force -Confirm:$false + $windowsVolume = $systemVolume + } + + "UEFI" + { + Write-W2VInfo "Initializing disk..." + Initialize-Disk -Number $disk.Number -PartitionStyle GPT + + if ((Get-WindowsBuildNumber) -ge 10240) + { + # + # Create the system partition. Create a data partition so we can format it, then change to ESP + # + Write-W2VInfo "Creating EFI system partition..." + $systemPartition = New-Partition -DiskNumber $disk.Number -Size 200MB -GptType '{ebd0a0a2-b9e5-4433-87c0-68b6b72699c7}' + + Write-W2VInfo "Formatting system volume..." + $systemVolume = Format-Volume -Partition $systemPartition -FileSystem FAT32 -Force -Confirm:$false + + Write-W2VInfo "Setting system partition as ESP..." + $systemPartition | Set-Partition -GptType '{c12a7328-f81f-11d2-ba4b-00a0c93ec93b}' + $systemPartition | Add-PartitionAccessPath -AssignDriveLetter + } + else + { + # + # Create the system partition + # + Write-W2VInfo "Creating EFI system partition (ESP)..." + $systemPartition = New-Partition -DiskNumber $disk.Number -Size 200MB -GptType '{c12a7328-f81f-11d2-ba4b-00a0c93ec93b}' -AssignDriveLetter + + Write-W2VInfo "Formatting ESP..." + $formatArgs = @( + "$($systemPartition.DriveLetter):", # Partition drive letter + "/FS:FAT32", # File system + "/Q", # Quick format + "/Y" # Suppress prompt + ) + + Run-Executable -Executable format -Arguments $formatArgs + } + + # + # Create the reserved partition + # + Write-W2VInfo "Creating MSR partition..." + $reservedPartition = New-Partition -DiskNumber $disk.Number -Size 128MB -GptType '{e3c9e316-0b5c-4db8-817d-f92df00215ae}' + + # + # Create the Windows partition + # + Write-W2VInfo "Creating windows partition..." + $windowsPartition = New-Partition -DiskNumber $disk.Number -UseMaximumSize -GptType '{ebd0a0a2-b9e5-4433-87c0-68b6b72699c7}' + + Write-W2VInfo "Formatting windows volume..." + $windowsVolume = Format-Volume -Partition $windowsPartition -FileSystem NTFS -Force -Confirm:$false + } + + "WindowsToGo" + { + Write-W2VInfo "Initializing disk..." + Initialize-Disk -Number $disk.Number -PartitionStyle MBR + + # + # Create the system partition + # + Write-W2VInfo "Creating system partition..." + $systemPartition = New-Partition -DiskNumber $disk.Number -Size 350MB -MbrType FAT32 -IsActive + + Write-W2VInfo "Formatting system volume..." + $systemVolume = Format-Volume -Partition $systemPartition -FileSystem FAT32 -Force -Confirm:$false + + # + # Create the Windows partition + # + Write-W2VInfo "Creating windows partition..." + $windowsPartition = New-Partition -DiskNumber $disk.Number -UseMaximumSize -MbrType IFS + + Write-W2VInfo "Formatting windows volume..." + $windowsVolume = Format-Volume -Partition $windowsPartition -FileSystem NTFS -Force -Confirm:$false + } + } + + # + # Assign drive letter to Windows partition. This is required for bcdboot + # + + $attempts = 1 + $assigned = $false + + do + { + $windowsPartition | Add-PartitionAccessPath -AssignDriveLetter + $windowsPartition = $windowsPartition | Get-Partition + if($windowsPartition.DriveLetter -ne 0) + { + $assigned = $true + } + else + { + #sleep for up to 10 seconds and retry + Get-Random -Minimum 1 -Maximum 10 | Start-Sleep + + $attempts++ + } + } + while ($attempts -le 100 -and -not($assigned)) + + if (-not($assigned)) + { + throw "Unable to get Partition after retry" + } + + $windowsDrive = $(Get-Partition -Volume $windowsVolume).AccessPaths[0].substring(0,2) + Write-W2VInfo "Windows path ($windowsDrive) has been assigned." + Write-W2VInfo "Windows path ($windowsDrive) took $attempts attempts to be assigned." + + # + # Refresh access paths (we have now formatted the volume) + # + $systemPartition = $systemPartition | Get-Partition + $systemDrive = $systemPartition.AccessPaths[0].trimend("\").replace("\?", "??") + Write-W2VInfo "System volume location: $systemDrive" + + #################################################################################################### + # APPLY IMAGE FROM WIM TO THE NEW VHD + #################################################################################################### + + Write-W2VInfo "Applying image to $VHDFormat. This could take a while..." + if ((Get-Command Expand-WindowsImage -ErrorAction SilentlyContinue) -and ((-not $ApplyEA) -and ([string]::IsNullOrEmpty($DismPath)))) + { + Expand-WindowsImage -ApplyPath $windowsDrive -ImagePath $SourcePath -Index $ImageIndex -LogPath "$($logFolder)\DismLogs.log" | Out-Null + } + else + { + if (![string]::IsNullOrEmpty($DismPath)) + { + $dismPath = $DismPath + } + else + { + $dismPath = $(Join-Path (get-item env:\windir).value "system32\dism.exe") + } + + $applyImage = "/Apply-Image" + if ($ApplyEA) + { + $applyImage = $applyImage + " /EA" + } + + $dismArgs = @("$applyImage /ImageFile:`"$SourcePath`" /Index:$ImageIndex /ApplyDir:$windowsDrive /LogPath:`"$($logFolder)\DismLogs.log`"") + Write-W2VInfo "Applying image: $dismPath $dismArgs" + $process = Start-Process -Passthru -Wait -NoNewWindow -FilePath $dismPath ` + -ArgumentList $dismArgs ` + + if ($process.ExitCode -ne 0) + { + throw "Image Apply failed! See DismImageApply logs for details" + } + } + Write-W2VInfo "Image was applied successfully. " + + # + # Here we copy in the unattend file (if specified by the command line) + # + if (![string]::IsNullOrEmpty($UnattendPath)) + { + Write-W2VInfo "Applying unattend file ($(Split-Path $UnattendPath -Leaf))..." + Copy-Item -Path $UnattendPath -Destination (Join-Path $windowsDrive "unattend.xml") -Force + } + + if (![string]::IsNullOrEmpty($MergeFolderPath)) + { + Write-W2VInfo "Applying merge folder ($MergeFolderPath)..." + Copy-Item -Recurse -Path (Join-Path $MergeFolderPath "*") -Destination $windowsDrive -Force #added to handle merge folders + } + + if (($openImage.ImageArchitecture -ne "ARM") -and # No virtualization platform for ARM images + ($openImage.ImageArchitecture -ne "ARM64") -and # No virtualization platform for ARM64 images + ($BCDinVHD -ne "NativeBoot")) # User asked for a non-bootable image + { + if (Test-Path "$($systemDrive)\boot\bcd") + { + Write-W2VInfo "Image already has BIOS BCD store..." + } + elseif (Test-Path "$($systemDrive)\efi\microsoft\boot\bcd") + { + Write-W2VInfo "Image already has EFI BCD store..." + } + else + { + Write-W2VInfo "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. + ) + + switch ($DiskLayout) + { + "BIOS" + { + $bcdBootArgs += "/f BIOS" # Specifies the firmware type of the target system partition + } + + "UEFI" + { + $bcdBootArgs += "/f UEFI" # Specifies the firmware type of the target system partition + } + + "WindowsToGo" + { + # Create entries for both UEFI and BIOS if possible + if (Test-Path "$($windowsDrive)\Windows\boot\EFI\bootmgfw.efi") + { + $bcdBootArgs += "/f ALL" + } + } + } + + Run-Executable -Executable $BCDBoot -Arguments $bcdBootArgs + + # The following is added to mitigate the VMM diff disk handling + # We're going to change from MBRBootOption to LocateBootOption. + + if ($DiskLayout -eq "BIOS") + { + Write-W2VInfo "Fixing the Device ID in the BCD store on $($VHDFormat)..." + Run-Executable -Executable "BCDEDIT.EXE" -Arguments ( + "/store $($systemDrive)\boot\bcd", + "/set `{bootmgr`} device locate" + ) + Run-Executable -Executable "BCDEDIT.EXE" -Arguments ( + "/store $($systemDrive)\boot\bcd", + "/set `{default`} device locate" + ) + Run-Executable -Executable "BCDEDIT.EXE" -Arguments ( + "/store $($systemDrive)\boot\bcd", + "/set `{default`} osdevice locate" + ) + } + } + + Write-W2VInfo "Drive is bootable. Cleaning up..." + + # Are we turning the debugger on? + if ($EnableDebugger -inotlike "None") + { + $bcdEditArgs = $null; + + # Configure the specified debugging transport and other settings. + switch ($EnableDebugger) + { + "Serial" + { + $bcdEditArgs = @( + "/dbgsettings SERIAL", + "DEBUGPORT:$($ComPort.Value)", + "BAUDRATE:$($BaudRate.Value)" + ) + } + + "1394" + { + $bcdEditArgs = @( + "/dbgsettings 1394", + "CHANNEL:$($Channel.Value)" + ) + } + + "USB" + { + $bcdEditArgs = @( + "/dbgsettings USB", + "TARGETNAME:$($Target.Value)" + ) + } + + "Local" + { + $bcdEditArgs = @( + "/dbgsettings LOCAL" + ) + } + + "Network" + { + $bcdEditArgs = @( + "/dbgsettings NET", + "HOSTIP:$($IP.Value)", + "PORT:$($Port.Value)", + "KEY:$($Key.Value)" + ) + } + } + + $bcdStores = @( + "$($systemDrive)\boot\bcd", + "$($systemDrive)\efi\microsoft\boot\bcd" + ) + + foreach ($bcdStore in $bcdStores) + { + if (Test-Path $bcdStore) + { + Write-W2VInfo "Turning kernel debugging on in the $($VHDFormat) for $($bcdStore)..." + Run-Executable -Executable "BCDEDIT.EXE" -Arguments ( + "/store $($bcdStore)", + "/set `{default`} debug on" + ) + + $bcdEditArguments = @("/store $($bcdStore)") + $bcdEditArgs + + Run-Executable -Executable "BCDEDIT.EXE" -Arguments $bcdEditArguments + } + } + } + } + else + { + # Don't bother to check on debugging. We can't boot WoA VHDs in VMs, and + # if we're native booting, the changes need to be made to the BCD store on the + # physical computer's boot volume. + + Write-W2VInfo "Image applied. It is not bootable." + } + + if ($RemoteDesktopEnable -or (-not $ExpandOnNativeBoot)) + { + $hive = Mount-RegistryHive -Hive (Join-Path $windowsDrive "Windows\System32\Config\System") + + if ($RemoteDesktopEnable) + { + Write-W2VInfo -text "Enabling Remote Desktop" + Set-ItemProperty -Path "HKLM:\$($hive)\ControlSet001\Control\Terminal Server" -Name "fDenyTSConnections" -Value 0 + } + + if (-not $ExpandOnNativeBoot) + { + Write-W2VInfo -text "Disabling automatic $VHDFormat expansion for Native Boot" + Set-ItemProperty -Path "HKLM:\$($hive)\ControlSet001\Services\FsDepends\Parameters" -Name "VirtualDiskExpandOnMount" -Value 4 + } + + Dismount-RegistryHive -HiveMountPoint $hive + } + + if ($Driver) + { + Write-W2VInfo -text "Adding Windows Drivers to the Image" + $Driver | ForEach-Object -Process { + Write-W2VInfo -text "Driver path: $PSItem" + Add-WindowsDriver -Path $windowsDrive -Recurse -Driver $PSItem -Verbose | Out-Null + } + } + + If ($Feature) + { + Write-W2VInfo -text "Installing Windows Feature(s) $Feature to the Image" + $FeatureSourcePath = Join-Path -Path "$($driveLetter):" -ChildPath "sources\sxs" + Write-W2VInfo -text "From $FeatureSourcePath" + Enable-WindowsOptionalFeature -FeatureName $Feature -Source $FeatureSourcePath -Path $windowsDrive -All | Out-Null + } + + if ($Package) + { + Write-W2VInfo -text "Adding Windows Packages to the Image" + + $Package | ForEach-Object -Process { + Write-W2VInfo -text "Package path: $PSItem" + Add-WindowsPackage -Path $windowsDrive -PackagePath $PSItem | Out-Null + } + } + + # + # Remove system partition access path, if necessary + # + if ($DiskLayout -eq "UEFI") + { + $systemPartition | Remove-PartitionAccessPath -AccessPath $systemPartition.AccessPaths[0] + } + + if ([String]::IsNullOrEmpty($vhdFinalName)) + { + # We need to generate a file name. + Write-W2VInfo "Generating name for $($VHDFormat)..." + $hive = Mount-RegistryHive -Hive (Join-Path $windowsDrive "Windows\System32\Config\Software") + + $buildLabEx = (Get-ItemProperty "HKLM:\$($hive)\Microsoft\Windows NT\CurrentVersion").BuildLabEx + $installType = (Get-ItemProperty "HKLM:\$($hive)\Microsoft\Windows NT\CurrentVersion").InstallationType + $editionId = (Get-ItemProperty "HKLM:\$($hive)\Microsoft\Windows NT\CurrentVersion").EditionID + $skuFamily = $null + + Dismount-RegistryHive -HiveMountPoint $hive + + # Is this ServerCore? + # Since we're only doing this string comparison against the InstallType key, we won't get + # false positives with the Core SKU. + if ($installType.ToUpper().Contains("CORE")) + { + $editionId += "Core" + } + + # What type of SKU are we? + if ($installType.ToUpper().Contains("SERVER")) + { + $skuFamily = "Server" + } + elseif ($installType.ToUpper().Contains("CLIENT")) + { + $skuFamily = "Client" + } + else + { + $skuFamily = "Unknown" + } + + # + # ISSUE - do we want VL here? + # + $vhdFinalName = "$($buildLabEx)_$($skuFamily)_$($editionId)_$($openImage.ImageDefaultLanguage).$($VHDFormat.ToLower())" + Write-W2VTrace "$VHDFormat final name is : $vhdFinalName" + } + + if ($hyperVEnabled) + { + Write-W2VInfo "Dismounting $VHDFormat..." + Dismount-VHD -Path $VHDPath + } + else + { + Write-W2VInfo "Closing $VHDFormat..." + Dismount-DiskImage -ImagePath $VHDPath + } + + $vhdFinalPath = Join-Path (Split-Path $VHDPath -Parent) $vhdFinalName + Write-W2VTrace "$VHDFormat final path is : $vhdFinalPath" + + if (Test-Path $vhdFinalPath) + { + Write-W2VInfo "Deleting pre-existing $VHDFormat : $(Split-Path $vhdFinalPath -Leaf)..." + Remove-Item -Path $vhdFinalPath -Force + } + + Write-W2VTrace -Text "Renaming $VHDFormat at $VHDPath to $vhdFinalName" + Rename-Item -Path (Resolve-Path $VHDPath).Path -NewName $vhdFinalName -Force + $vhd += Get-DiskImage -ImagePath $vhdFinalPath + + $vhdFinalName = $null + } + catch + { + Write-W2VError $_ + Write-W2VInfo "Log folder is $logFolder" + } + finally + { + # If we still have a WIM image open, close it. + if ($openWim -ne $null) + { + Write-W2VInfo "Closing Windows image..." + $openWim.Close() + } + + # If we still have a registry hive mounted, dismount it. + if ($mountedHive -ne $null) + { + Write-W2VInfo "Closing registry hive..." + Dismount-RegistryHive -HiveMountPoint $mountedHive + } + + # If VHD is mounted, unmount it + if (Test-Path $VHDPath) + { + if ($hyperVEnabled) + { + if ((Get-VHD -Path $VHDPath).Attached) + { + Dismount-VHD -Path $VHDPath + } + } + else + { + Dismount-DiskImage -ImagePath $VHDPath + } + } + + # If we still have an ISO open, close it. + if ($openIso -ne $null) + { + Write-W2VInfo "Closing ISO..." + Dismount-DiskImage $ISOPath + } + + if (-not $CacheSource) + { + if ($tempSource -and (Test-Path $tempSource)) + { + Remove-Item -Path $tempSource -Force + } + } + + # Close out the transcript and tell the user we're done. + Write-W2VInfo "Done." + if ($transcripting) + { + $null = Stop-Transcript + } + } + } + + End + { + if ($Passthru) + { + return $vhd + } + } + #endregion Code + +} + + +function +Add-WindowsImageTypes +{ + $code = @" +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.ComponentModel; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Runtime.InteropServices; +using System.Security; +using System.Text; +using System.Text.RegularExpressions; +using System.Threading; +using System.Xml.Linq; +using System.Xml.XPath; +using Microsoft.Win32.SafeHandles; + +namespace WIM2VHD +{ + +/// +/// P/Invoke methods and associated enums, flags, and structs. +/// +public class +NativeMethods +{ + + #region Delegates and Callbacks + #region WIMGAPI + + /// + ///User-defined function used with the RegisterMessageCallback or UnregisterMessageCallback function. + /// + ///Specifies the message being sent. + ///Specifies additional message information. The contents of this parameter depend on the value of the + ///MessageId parameter. + ///Specifies additional message information. The contents of this parameter depend on the value of the + ///MessageId parameter. + ///Specifies the user-defined value passed to RegisterCallback. + /// + ///To indicate success and to enable other subscribers to process the message return WIM_MSG_SUCCESS. + ///To prevent other subscribers from receiving the message, return WIM_MSG_DONE. + ///To cancel an image apply or capture, return WIM_MSG_ABORT_IMAGE when handling the WIM_MSG_PROCESS message. + /// + public delegate uint + WimMessageCallback( + uint MessageId, + IntPtr wParam, + IntPtr lParam, + IntPtr UserData + ); + + public static void + RegisterMessageCallback( + WimFileHandle hWim, + WimMessageCallback callback) + { + + uint _callback = NativeMethods.WimRegisterMessageCallback(hWim, callback, IntPtr.Zero); + int rc = Marshal.GetLastWin32Error(); + if (0 != rc) + { + // Throw an exception if something bad happened on the Win32 end. + throw + new InvalidOperationException( + string.Format( + CultureInfo.CurrentCulture, + "Unable to register message callback." + )); + } + } + + public static void + UnregisterMessageCallback( + WimFileHandle hWim, + WimMessageCallback registeredCallback) + { + + bool status = NativeMethods.WimUnregisterMessageCallback(hWim, registeredCallback); + int rc = Marshal.GetLastWin32Error(); + if (!status) + { + throw + new InvalidOperationException( + string.Format( + CultureInfo.CurrentCulture, + "Unable to unregister message callback." + )); + } + } + + #endregion WIMGAPI + #endregion Delegates and Callbacks + + #region Constants + + #region VDiskInterop + + /// + /// The default depth in a VHD parent chain that this library will search through. + /// If you want to go more than one disk deep into the parent chain, provide a different value. + /// + public const uint OPEN_VIRTUAL_DISK_RW_DEFAULT_DEPTH = 0x00000001; + + public const uint DEFAULT_BLOCK_SIZE = 0x00080000; + public const uint DISK_SECTOR_SIZE = 0x00000200; + + internal const uint ERROR_VIRTDISK_NOT_VIRTUAL_DISK = 0xC03A0015; + internal const uint ERROR_NOT_FOUND = 0x00000490; + internal const uint ERROR_IO_PENDING = 0x000003E5; + internal const uint ERROR_INSUFFICIENT_BUFFER = 0x0000007A; + internal const uint ERROR_ERROR_DEV_NOT_EXIST = 0x00000037; + internal const uint ERROR_BAD_COMMAND = 0x00000016; + internal const uint ERROR_SUCCESS = 0x00000000; + + public const uint GENERIC_READ = 0x80000000; + public const uint GENERIC_WRITE = 0x40000000; + public const short FILE_ATTRIBUTE_NORMAL = 0x00000080; + public const uint CREATE_NEW = 0x00000001; + public const uint CREATE_ALWAYS = 0x00000002; + public const uint OPEN_EXISTING = 0x00000003; + public const short INVALID_HANDLE_VALUE = -1; + + internal static Guid VirtualStorageTypeVendorUnknown = new Guid("00000000-0000-0000-0000-000000000000"); + internal static Guid VirtualStorageTypeVendorMicrosoft = new Guid("EC984AEC-A0F9-47e9-901F-71415A66345B"); + + #endregion VDiskInterop + + #region WIMGAPI + + public const uint WIM_FLAG_VERIFY = 0x00000002; + public const uint WIM_FLAG_INDEX = 0x00000004; + + public const uint WM_APP = 0x00008000; + + #endregion WIMGAPI + + #endregion Constants + + #region Enums and Flags + + #region VDiskInterop + + /// + /// Indicates the version of the virtual disk to create. + /// + public enum CreateVirtualDiskVersion : int + { + VersionUnspecified = 0x00000000, + Version1 = 0x00000001, + Version2 = 0x00000002 + } + + public enum OpenVirtualDiskVersion : int + { + VersionUnspecified = 0x00000000, + Version1 = 0x00000001, + Version2 = 0x00000002 + } + + /// + /// Contains the version of the virtual hard disk (VHD) ATTACH_VIRTUAL_DISK_PARAMETERS structure to use in calls to VHD functions. + /// + public enum AttachVirtualDiskVersion : int + { + VersionUnspecified = 0x00000000, + Version1 = 0x00000001, + Version2 = 0x00000002 + } + + public enum CompactVirtualDiskVersion : int + { + VersionUnspecified = 0x00000000, + Version1 = 0x00000001 + } + + /// + /// Contains the type and provider (vendor) of the virtual storage device. + /// + public enum VirtualStorageDeviceType : int + { + /// + /// The storage type is unknown or not valid. + /// + Unknown = 0x00000000, + /// + /// For internal use only. This type is not supported. + /// + ISO = 0x00000001, + /// + /// Virtual Hard Disk device type. + /// + VHD = 0x00000002, + /// + /// Virtual Hard Disk v2 device type. + /// + VHDX = 0x00000003 + } + + /// + /// Contains virtual hard disk (VHD) open request flags. + /// + [Flags] + public enum OpenVirtualDiskFlags + { + /// + /// No flags. Use system defaults. + /// + None = 0x00000000, + /// + /// Open the VHD file (backing store) without opening any differencing-chain parents. Used to correct broken parent links. + /// + NoParents = 0x00000001, + /// + /// Reserved. + /// + BlankFile = 0x00000002, + /// + /// Reserved. + /// + BootDrive = 0x00000004, + } + + /// + /// Contains the bit mask for specifying access rights to a virtual hard disk (VHD). + /// + [Flags] + public enum VirtualDiskAccessMask + { + /// + /// Only Version2 of OpenVirtualDisk API accepts this parameter + /// + None = 0x00000000, + /// + /// Open the virtual disk for read-only attach access. The caller must have READ access to the virtual disk image file. + /// + /// + /// If used in a request to open a virtual disk that is already open, the other handles must be limited to either + /// VIRTUAL_DISK_ACCESS_DETACH or VIRTUAL_DISK_ACCESS_GET_INFO access, otherwise the open request with this flag will fail. + /// + AttachReadOnly = 0x00010000, + /// + /// Open the virtual disk for read-write attaching access. The caller must have (READ | WRITE) access to the virtual disk image file. + /// + /// + /// If used in a request to open a virtual disk that is already open, the other handles must be limited to either + /// VIRTUAL_DISK_ACCESS_DETACH or VIRTUAL_DISK_ACCESS_GET_INFO access, otherwise the open request with this flag will fail. + /// If the virtual disk is part of a differencing chain, the disk for this request cannot be less than the readWriteDepth specified + /// during the prior open request for that differencing chain. + /// + AttachReadWrite = 0x00020000, + /// + /// Open the virtual disk to allow detaching of an attached virtual disk. The caller must have + /// (FILE_READ_ATTRIBUTES | FILE_READ_DATA) access to the virtual disk image file. + /// + Detach = 0x00040000, + /// + /// Information retrieval access to the virtual disk. The caller must have READ access to the virtual disk image file. + /// + GetInfo = 0x00080000, + /// + /// Virtual disk creation access. + /// + Create = 0x00100000, + /// + /// Open the virtual disk to perform offline meta-operations. The caller must have (READ | WRITE) access to the virtual + /// disk image file, up to readWriteDepth if working with a differencing chain. + /// + /// + /// If the virtual disk is part of a differencing chain, the backing store (host volume) is opened in RW exclusive mode up to readWriteDepth. + /// + MetaOperations = 0x00200000, + /// + /// Reserved. + /// + Read = 0x000D0000, + /// + /// Allows unrestricted access to the virtual disk. The caller must have unrestricted access rights to the virtual disk image file. + /// + All = 0x003F0000, + /// + /// Reserved. + /// + Writable = 0x00320000 + } + + /// + /// Contains virtual hard disk (VHD) creation flags. + /// + [Flags] + public enum CreateVirtualDiskFlags + { + /// + /// Contains virtual hard disk (VHD) creation flags. + /// + None = 0x00000000, + /// + /// Pre-allocate all physical space necessary for the size of the virtual disk. + /// + /// + /// The CREATE_VIRTUAL_DISK_FLAG_FULL_PHYSICAL_ALLOCATION flag is used for the creation of a fixed VHD. + /// + FullPhysicalAllocation = 0x00000001 + } + + /// + /// Contains virtual disk attach request flags. + /// + [Flags] + public enum AttachVirtualDiskFlags + { + /// + /// No flags. Use system defaults. + /// + None = 0x00000000, + /// + /// Attach the virtual disk as read-only. + /// + ReadOnly = 0x00000001, + /// + /// No drive letters are assigned to the disk's volumes. + /// + /// Oddly enough, this doesn't apply to NTFS mount points. + NoDriveLetter = 0x00000002, + /// + /// Will decouple the virtual disk lifetime from that of the VirtualDiskHandle. + /// The virtual disk will be attached until the Detach() function is called, even if all open handles to the virtual disk are closed. + /// + PermanentLifetime = 0x00000004, + /// + /// Reserved. + /// + NoLocalHost = 0x00000008 + } + + [Flags] + public enum DetachVirtualDiskFlag + { + None = 0x00000000 + } + + [Flags] + public enum CompactVirtualDiskFlags + { + None = 0x00000000, + NoZeroScan = 0x00000001, + NoBlockMoves = 0x00000002 + } + + #endregion VDiskInterop + + #region WIMGAPI + + [FlagsAttribute] + internal enum + WimCreateFileDesiredAccess : uint + { + WimQuery = 0x00000000, + WimGenericRead = 0x80000000 + } + + public enum WimMessage : uint + { + WIM_MSG = WM_APP + 0x1476, + WIM_MSG_TEXT, + /// + ///Indicates an update in the progress of an image application. + /// + WIM_MSG_PROGRESS, + /// + ///Enables the caller to prevent a file or a directory from being captured or applied. + /// + WIM_MSG_PROCESS, + /// + ///Indicates that volume information is being gathered during an image capture. + /// + WIM_MSG_SCANNING, + /// + ///Indicates the number of files that will be captured or applied. + /// + WIM_MSG_SETRANGE, + /// + ///Indicates the number of files that have been captured or applied. + /// + WIM_MSG_SETPOS, + /// + ///Indicates that a file has been either captured or applied. + /// + WIM_MSG_STEPIT, + /// + ///Enables the caller to prevent a file resource from being compressed during a capture. + /// + WIM_MSG_COMPRESS, + /// + ///Alerts the caller that an error has occurred while capturing or applying an image. + /// + WIM_MSG_ERROR, + /// + ///Enables the caller to align a file resource on a particular alignment boundary. + /// + WIM_MSG_ALIGNMENT, + WIM_MSG_RETRY, + /// + ///Enables the caller to align a file resource on a particular alignment boundary. + /// + WIM_MSG_SPLIT, + WIM_MSG_SUCCESS = 0x00000000, + WIM_MSG_ABORT_IMAGE = 0xFFFFFFFF + } + + internal enum + WimCreationDisposition : uint + { + WimOpenExisting = 0x00000003, + } + + internal enum + WimActionFlags : uint + { + WimIgnored = 0x00000000 + } + + internal enum + WimCompressionType : uint + { + WimIgnored = 0x00000000 + } + + internal enum + WimCreationResult : uint + { + WimCreatedNew = 0x00000000, + WimOpenedExisting = 0x00000001 + } + + #endregion WIMGAPI + + #endregion Enums and Flags + + #region Structs + + [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] + public struct CreateVirtualDiskParameters + { + /// + /// A CREATE_VIRTUAL_DISK_VERSION enumeration that specifies the version of the CREATE_VIRTUAL_DISK_PARAMETERS structure being passed to or from the virtual hard disk (VHD) functions. + /// + public CreateVirtualDiskVersion Version; + + /// + /// Unique identifier to assign to the virtual disk object. If this member is set to zero, a unique identifier is created by the system. + /// + public Guid UniqueId; + + /// + /// The maximum virtual size of the virtual disk object. Must be a multiple of 512. + /// If a ParentPath is specified, this value must be zero. + /// If a SourcePath is specified, this value can be zero to specify the size of the source VHD to be used, otherwise the size specified must be greater than or equal to the size of the source disk. + /// + public ulong MaximumSize; + + /// + /// Internal size of the virtual disk object blocks. + /// The following are predefined block sizes and their behaviors. For a fixed VHD type, this parameter must be zero. + /// + public uint BlockSizeInBytes; + + /// + /// Internal size of the virtual disk object sectors. Must be set to 512. + /// + public uint SectorSizeInBytes; + + /// + /// Optional path to a parent virtual disk object. Associates the new virtual disk with an existing virtual disk. + /// If this parameter is not NULL, SourcePath must be NULL. + /// + public string ParentPath; + + /// + /// Optional path to pre-populate the new virtual disk object with block data from an existing disk. This path may refer to a VHD or a physical disk. + /// If this parameter is not NULL, ParentPath must be NULL. + /// + public string SourcePath; + + /// + /// Flags for opening the VHD + /// + public OpenVirtualDiskFlags OpenFlags; + + /// + /// GetInfoOnly flag for V2 handles + /// + public bool GetInfoOnly; + + /// + /// Virtual Storage Type of the parent disk + /// + public VirtualStorageType ParentVirtualStorageType; + + /// + /// Virtual Storage Type of the source disk + /// + public VirtualStorageType SourceVirtualStorageType; + + /// + /// A GUID to use for fallback resiliency over SMB. + /// + public Guid ResiliencyGuid; + } + + [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] + public struct VirtualStorageType + { + public VirtualStorageDeviceType DeviceId; + public Guid VendorId; + } + + [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] + public struct SecurityDescriptor + { + public byte revision; + public byte size; + public short control; + public IntPtr owner; + public IntPtr group; + public IntPtr sacl; + public IntPtr dacl; + } + + #endregion Structs + + #region VirtDisk.DLL P/Invoke + + [DllImport("virtdisk.dll", CharSet = CharSet.Unicode)] + public static extern uint + CreateVirtualDisk( + [In, Out] ref VirtualStorageType VirtualStorageType, + [In] string Path, + [In] VirtualDiskAccessMask VirtualDiskAccessMask, + [In, Out] ref SecurityDescriptor SecurityDescriptor, + [In] CreateVirtualDiskFlags Flags, + [In] uint ProviderSpecificFlags, + [In, Out] ref CreateVirtualDiskParameters Parameters, + [In] IntPtr Overlapped, + [Out] out SafeFileHandle Handle); + + #endregion VirtDisk.DLL P/Invoke + + #region Win32 P/Invoke + + [DllImport("advapi32", SetLastError = true)] + public static extern bool InitializeSecurityDescriptor( + [Out] out SecurityDescriptor pSecurityDescriptor, + [In] uint dwRevision); + + #endregion Win32 P/Invoke + + #region WIMGAPI P/Invoke + + #region SafeHandle wrappers for WimFileHandle and WimImageHandle + + public sealed class WimFileHandle : SafeHandle + { + + public WimFileHandle( + string wimPath) + : base(IntPtr.Zero, true) + { + + if (String.IsNullOrEmpty(wimPath)) + { + throw new ArgumentNullException("wimPath"); + } + + if (!File.Exists(Path.GetFullPath(wimPath))) + { + throw new FileNotFoundException((new FileNotFoundException()).Message, wimPath); + } + + NativeMethods.WimCreationResult creationResult; + + this.handle = NativeMethods.WimCreateFile( + wimPath, + NativeMethods.WimCreateFileDesiredAccess.WimGenericRead, + NativeMethods.WimCreationDisposition.WimOpenExisting, + NativeMethods.WimActionFlags.WimIgnored, + NativeMethods.WimCompressionType.WimIgnored, + out creationResult + ); + + // Check results. + if (creationResult != NativeMethods.WimCreationResult.WimOpenedExisting) + { + throw new Win32Exception(); + } + + if (this.handle == IntPtr.Zero) + { + throw new Win32Exception(); + } + + // Set the temporary path. + NativeMethods.WimSetTemporaryPath( + this, + Environment.ExpandEnvironmentVariables("%TEMP%") + ); + } + + protected override bool ReleaseHandle() + { + return NativeMethods.WimCloseHandle(this.handle); + } + + public override bool IsInvalid + { + get { return this.handle == IntPtr.Zero; } + } + } + + public sealed class WimImageHandle : SafeHandle + { + public WimImageHandle( + WimFile Container, + uint ImageIndex) + : base(IntPtr.Zero, true) + { + + if (null == Container) + { + throw new ArgumentNullException("Container"); + } + + if ((Container.Handle.IsClosed) || (Container.Handle.IsInvalid)) + { + throw new ArgumentNullException("The handle to the WIM file has already been closed, or is invalid.", "Container"); + } + + if (ImageIndex > Container.ImageCount) + { + throw new ArgumentOutOfRangeException("ImageIndex", "The index does not exist in the specified WIM file."); + } + + this.handle = NativeMethods.WimLoadImage( + Container.Handle.DangerousGetHandle(), + ImageIndex); + } + + protected override bool ReleaseHandle() + { + return NativeMethods.WimCloseHandle(this.handle); + } + + public override bool IsInvalid + { + get { return this.handle == IntPtr.Zero; } + } + } + + #endregion SafeHandle wrappers for WimFileHandle and WimImageHandle + + [DllImport("Wimgapi.dll", CharSet = CharSet.Unicode, SetLastError = true, EntryPoint = "WIMCreateFile")] + internal static extern IntPtr + WimCreateFile( + [In, MarshalAs(UnmanagedType.LPWStr)] string WimPath, + [In] WimCreateFileDesiredAccess DesiredAccess, + [In] WimCreationDisposition CreationDisposition, + [In] WimActionFlags FlagsAndAttributes, + [In] WimCompressionType CompressionType, + [Out, Optional] out WimCreationResult CreationResult + ); + + [DllImport("Wimgapi.dll", CharSet = CharSet.Unicode, SetLastError = true, EntryPoint = "WIMCloseHandle")] + [return: MarshalAs(UnmanagedType.Bool)] + internal static extern bool + WimCloseHandle( + [In] IntPtr Handle + ); + + [DllImport("Wimgapi.dll", CharSet = CharSet.Unicode, SetLastError = true, EntryPoint = "WIMLoadImage")] + internal static extern IntPtr + WimLoadImage( + [In] IntPtr Handle, + [In] uint ImageIndex + ); + + [DllImport("Wimgapi.dll", CharSet = CharSet.Unicode, SetLastError = true, EntryPoint = "WIMGetImageCount")] + internal static extern uint + WimGetImageCount( + [In] WimFileHandle Handle + ); + + [DllImport("Wimgapi.dll", CharSet = CharSet.Unicode, SetLastError = true, EntryPoint = "WIMGetImageInformation")] + [return: MarshalAs(UnmanagedType.Bool)] + internal static extern bool + WimGetImageInformation( + [In] SafeHandle Handle, + [Out] out StringBuilder ImageInfo, + [Out] out uint SizeOfImageInfo + ); + + [DllImport("Wimgapi.dll", CharSet = CharSet.Unicode, SetLastError = true, EntryPoint = "WIMSetTemporaryPath")] + [return: MarshalAs(UnmanagedType.Bool)] + internal static extern bool + WimSetTemporaryPath( + [In] WimFileHandle Handle, + [In] string TempPath + ); + + [DllImport("Wimgapi.dll", CharSet = CharSet.Unicode, SetLastError = true, EntryPoint = "WIMRegisterMessageCallback", CallingConvention = CallingConvention.StdCall)] + internal static extern uint + WimRegisterMessageCallback( + [In, Optional] WimFileHandle hWim, + [In] WimMessageCallback MessageProc, + [In, Optional] IntPtr ImageInfo + ); + + [DllImport("Wimgapi.dll", CharSet = CharSet.Unicode, SetLastError = true, EntryPoint = "WIMUnregisterMessageCallback", CallingConvention = CallingConvention.StdCall)] + [return: MarshalAs(UnmanagedType.Bool)] + internal static extern bool + WimUnregisterMessageCallback( + [In, Optional] WimFileHandle hWim, + [In] WimMessageCallback MessageProc + ); + + + #endregion WIMGAPI P/Invoke +} + +#region WIM Interop + +public class WimFile +{ + + internal XDocument m_xmlInfo; + internal List m_imageList; + + private static NativeMethods.WimMessageCallback wimMessageCallback; + + #region Events + + /// + /// DefaultImageEvent handler + /// + public delegate void DefaultImageEventHandler(object sender, DefaultImageEventArgs e); + + /// + ///ProcessFileEvent handler + /// + public delegate void ProcessFileEventHandler(object sender, ProcessFileEventArgs e); + + /// + ///Enable the caller to prevent a file resource from being compressed during a capture. + /// + public event ProcessFileEventHandler ProcessFileEvent; + + /// + ///Indicate an update in the progress of an image application. + /// + public event DefaultImageEventHandler ProgressEvent; + + /// + ///Alert the caller that an error has occurred while capturing or applying an image. + /// + public event DefaultImageEventHandler ErrorEvent; + + /// + ///Indicate that a file has been either captured or applied. + /// + public event DefaultImageEventHandler StepItEvent; + + /// + ///Indicate the number of files that will be captured or applied. + /// + public event DefaultImageEventHandler SetRangeEvent; + + /// + ///Indicate the number of files that have been captured or applied. + /// + public event DefaultImageEventHandler SetPosEvent; + + #endregion Events + + private + enum + ImageEventMessage : uint + { + /// + ///Enables the caller to prevent a file or a directory from being captured or applied. + /// + Progress = NativeMethods.WimMessage.WIM_MSG_PROGRESS, + /// + ///Notification sent to enable the caller to prevent a file or a directory from being captured or applied. + ///To prevent a file or a directory from being captured or applied, call WindowsImageContainer.SkipFile(). + /// + Process = NativeMethods.WimMessage.WIM_MSG_PROCESS, + /// + ///Enables the caller to prevent a file resource from being compressed during a capture. + /// + Compress = NativeMethods.WimMessage.WIM_MSG_COMPRESS, + /// + ///Alerts the caller that an error has occurred while capturing or applying an image. + /// + Error = NativeMethods.WimMessage.WIM_MSG_ERROR, + /// + ///Enables the caller to align a file resource on a particular alignment boundary. + /// + Alignment = NativeMethods.WimMessage.WIM_MSG_ALIGNMENT, + /// + ///Enables the caller to align a file resource on a particular alignment boundary. + /// + Split = NativeMethods.WimMessage.WIM_MSG_SPLIT, + /// + ///Indicates that volume information is being gathered during an image capture. + /// + Scanning = NativeMethods.WimMessage.WIM_MSG_SCANNING, + /// + ///Indicates the number of files that will be captured or applied. + /// + SetRange = NativeMethods.WimMessage.WIM_MSG_SETRANGE, + /// + ///Indicates the number of files that have been captured or applied. + /// + SetPos = NativeMethods.WimMessage.WIM_MSG_SETPOS, + /// + ///Indicates that a file has been either captured or applied. + /// + StepIt = NativeMethods.WimMessage.WIM_MSG_STEPIT, + /// + ///Success. + /// + Success = NativeMethods.WimMessage.WIM_MSG_SUCCESS, + /// + ///Abort. + /// + Abort = NativeMethods.WimMessage.WIM_MSG_ABORT_IMAGE + } + + /// + ///Event callback to the Wimgapi events + /// + private + uint + ImageEventMessagePump( + uint MessageId, + IntPtr wParam, + IntPtr lParam, + IntPtr UserData) + { + + uint status = (uint) NativeMethods.WimMessage.WIM_MSG_SUCCESS; + + DefaultImageEventArgs eventArgs = new DefaultImageEventArgs(wParam, lParam, UserData); + + switch ((ImageEventMessage)MessageId) + { + + case ImageEventMessage.Progress: + ProgressEvent(this, eventArgs); + break; + + case ImageEventMessage.Process: + if (null != ProcessFileEvent) + { + string fileToImage = Marshal.PtrToStringUni(wParam); + ProcessFileEventArgs fileToProcess = new ProcessFileEventArgs(fileToImage, lParam); + ProcessFileEvent(this, fileToProcess); + + if (fileToProcess.Abort == true) + { + status = (uint)ImageEventMessage.Abort; + } + } + break; + + case ImageEventMessage.Error: + if (null != ErrorEvent) + { + ErrorEvent(this, eventArgs); + } + break; + + case ImageEventMessage.SetRange: + if (null != SetRangeEvent) + { + SetRangeEvent(this, eventArgs); + } + break; + + case ImageEventMessage.SetPos: + if (null != SetPosEvent) + { + SetPosEvent(this, eventArgs); + } + break; + + case ImageEventMessage.StepIt: + if (null != StepItEvent) + { + StepItEvent(this, eventArgs); + } + break; + + default: + break; + } + return status; + + } + + /// + /// Constructor. + /// + /// Path to the WIM container. + public + WimFile(string wimPath) + { + if (string.IsNullOrEmpty(wimPath)) + { + throw new ArgumentNullException("wimPath"); + } + + if (!File.Exists(Path.GetFullPath(wimPath))) + { + throw new FileNotFoundException((new FileNotFoundException()).Message, wimPath); + } + + Handle = new NativeMethods.WimFileHandle(wimPath); + + // Hook up the events before we return. + //wimMessageCallback = new NativeMethods.WimMessageCallback(ImageEventMessagePump); + //NativeMethods.RegisterMessageCallback(this.Handle, wimMessageCallback); + } + + /// + /// Closes the WIM file. + /// + public void + Close() + { + foreach (WimImage image in Images) + { + image.Close(); + } + + if (null != wimMessageCallback) + { + NativeMethods.UnregisterMessageCallback(this.Handle, wimMessageCallback); + wimMessageCallback = null; + } + + if ((!Handle.IsClosed) && (!Handle.IsInvalid)) + { + Handle.Close(); + } + } + + /// + /// Provides a list of WimImage objects, representing the images in the WIM container file. + /// + public List + Images + { + get + { + if (null == m_imageList) + { + + int imageCount = (int)ImageCount; + m_imageList = new List(imageCount); + for (int i = 0; i < imageCount; i++) + { + + // Load up each image so it's ready for us. + m_imageList.Add( + new WimImage(this, (uint)i + 1)); + } + } + + return m_imageList; + } + } + + /// + /// Provides a list of names of the images in the specified WIM container file. + /// + public List + ImageNames + { + get + { + List nameList = new List(); + foreach (WimImage image in Images) + { + nameList.Add(image.ImageName); + } + return nameList; + } + } + + /// + /// Indexer for WIM images inside the WIM container, indexed by the image number. + /// The list of Images is 0-based, but the WIM container is 1-based, so we automatically compensate for that. + /// this[1] returns the 0th image in the WIM container. + /// + /// The 1-based index of the image to retrieve. + /// WinImage object. + public WimImage + this[int ImageIndex] + { + get { return Images[ImageIndex - 1]; } + } + + /// + /// Indexer for WIM images inside the WIM container, indexed by the image name. + /// WIMs created by different processes sometimes contain different information - including the name. + /// Some images have their name stored in the Name field, some in the Flags field, and some in the EditionID field. + /// We take all of those into account in while searching the WIM. + /// + /// + /// + public WimImage + this[string ImageName] + { + get + { + return + Images.Where(i => ( + i.ImageName.ToUpper() == ImageName.ToUpper() || + i.ImageFlags.ToUpper() == ImageName.ToUpper() )) + .DefaultIfEmpty(null) + .FirstOrDefault(); + } + } + + /// + /// Returns the number of images in the WIM container. + /// + internal uint + ImageCount + { + get { return NativeMethods.WimGetImageCount(Handle); } + } + + /// + /// Returns an XDocument representation of the XML metadata for the WIM container and associated images. + /// + internal XDocument + XmlInfo + { + get + { + + if (null == m_xmlInfo) + { + StringBuilder builder; + uint bytes; + if (!NativeMethods.WimGetImageInformation(Handle, out builder, out bytes)) + { + throw new Win32Exception(); + } + + // Ensure the length of the returned bytes to avoid garbage characters at the end. + int charCount = (int)bytes / sizeof(char); + if (null != builder) + { + // Get rid of the unicode file marker at the beginning of the XML. + builder.Remove(0, 1); + builder.EnsureCapacity(charCount - 1); + builder.Length = charCount - 1; + + // This isn't likely to change while we have the image open, so cache it. + m_xmlInfo = XDocument.Parse(builder.ToString().Trim()); + } + else + { + m_xmlInfo = null; + } + } + + return m_xmlInfo; + } + } + + public NativeMethods.WimFileHandle Handle + { + get; + private set; + } +} + +public class +WimImage +{ + + internal XDocument m_xmlInfo; + + public + WimImage( + WimFile Container, + uint ImageIndex) + { + + if (null == Container) + { + throw new ArgumentNullException("Container"); + } + + if ((Container.Handle.IsClosed) || (Container.Handle.IsInvalid)) + { + throw new ArgumentNullException("The handle to the WIM file has already been closed, or is invalid.", "Container"); + } + + if (ImageIndex > Container.ImageCount) + { + throw new ArgumentOutOfRangeException("ImageIndex", "The index does not exist in the specified WIM file."); + } + + Handle = new NativeMethods.WimImageHandle(Container, ImageIndex); + } + + public enum + Architectures : uint + { + x86 = 0x0, + ARM = 0x5, + IA64 = 0x6, + AMD64 = 0x9, + ARM64 = 0xC + } + + public void + Close() + { + if ((!Handle.IsClosed) && (!Handle.IsInvalid)) + { + Handle.Close(); + } + } + + public NativeMethods.WimImageHandle + Handle + { + get; + private set; + } + + internal XDocument + XmlInfo + { + get + { + + if (null == m_xmlInfo) + { + StringBuilder builder; + uint bytes; + if (!NativeMethods.WimGetImageInformation(Handle, out builder, out bytes)) + { + throw new Win32Exception(); + } + + // Ensure the length of the returned bytes to avoid garbage characters at the end. + int charCount = (int)bytes / sizeof(char); + if (null != builder) + { + // Get rid of the unicode file marker at the beginning of the XML. + builder.Remove(0, 1); + builder.EnsureCapacity(charCount - 1); + builder.Length = charCount - 1; + + // This isn't likely to change while we have the image open, so cache it. + m_xmlInfo = XDocument.Parse(builder.ToString().Trim()); + } + else + { + m_xmlInfo = null; + } + } + + return m_xmlInfo; + } + } + + public string + ImageIndex + { + get { return XmlInfo.Element("IMAGE").Attribute("INDEX").Value; } + } + + public string + ImageName + { + get { return XmlInfo.XPathSelectElement("/IMAGE/NAME").Value; } + } + + public string + ImageEditionId + { + get { return XmlInfo.XPathSelectElement("/IMAGE/WINDOWS/EDITIONID").Value; } + } + + public string + ImageFlags + { + get + { + string flagValue = String.Empty; + + try + { + flagValue = XmlInfo.XPathSelectElement("/IMAGE/FLAGS").Value; + } + catch + { + + // Some WIM files don't contain a FLAGS element in the metadata. + // In an effort to support those WIMs too, inherit the EditionId if there + // are no Flags. + + if (String.IsNullOrEmpty(flagValue)) + { + flagValue = this.ImageEditionId; + + // Check to see if the EditionId is "ServerHyper". If so, + // tweak it to be "ServerHyperCore" instead. + + if (0 == String.Compare("serverhyper", flagValue, true)) + { + flagValue = "ServerHyperCore"; + } + } + + } + + return flagValue; + } + } + + public string + ImageProductType + { + get { return XmlInfo.XPathSelectElement("/IMAGE/WINDOWS/PRODUCTTYPE").Value; } + } + + public string + ImageInstallationType + { + get { return XmlInfo.XPathSelectElement("/IMAGE/WINDOWS/INSTALLATIONTYPE").Value; } + } + + public string + ImageDescription + { + get { return XmlInfo.XPathSelectElement("/IMAGE/DESCRIPTION").Value; } + } + + public ulong + ImageSize + { + get { return ulong.Parse(XmlInfo.XPathSelectElement("/IMAGE/TOTALBYTES").Value); } + } + + public Architectures + ImageArchitecture + { + get + { + int arch = -1; + try + { + arch = int.Parse(XmlInfo.XPathSelectElement("/IMAGE/WINDOWS/ARCH").Value); + } + catch { } + + return (Architectures)arch; + } + } + + public string + ImageDefaultLanguage + { + get + { + string lang = null; + try + { + lang = XmlInfo.XPathSelectElement("/IMAGE/WINDOWS/LANGUAGES/DEFAULT").Value; + } + catch { } + + return lang; + } + } + + public Version + ImageVersion + { + get + { + int major = 0; + int minor = 0; + int build = 0; + int revision = 0; + + try + { + major = int.Parse(XmlInfo.XPathSelectElement("/IMAGE/WINDOWS/VERSION/MAJOR").Value); + minor = int.Parse(XmlInfo.XPathSelectElement("/IMAGE/WINDOWS/VERSION/MINOR").Value); + build = int.Parse(XmlInfo.XPathSelectElement("/IMAGE/WINDOWS/VERSION/BUILD").Value); + revision = int.Parse(XmlInfo.XPathSelectElement("/IMAGE/WINDOWS/VERSION/SPBUILD").Value); + } + catch { } + + return (new Version(major, minor, build, revision)); + } + } + + public string + ImageDisplayName + { + get { return XmlInfo.XPathSelectElement("/IMAGE/DISPLAYNAME").Value; } + } + + public string + ImageDisplayDescription + { + get { return XmlInfo.XPathSelectElement("/IMAGE/DISPLAYDESCRIPTION").Value; } + } +} + +/// +///Describes the file that is being processed for the ProcessFileEvent. +/// +public class +DefaultImageEventArgs : EventArgs +{ + /// + ///Default constructor. + /// + public + DefaultImageEventArgs( + IntPtr wideParameter, + IntPtr leftParameter, + IntPtr userData) + { + + WideParameter = wideParameter; + LeftParameter = leftParameter; + UserData = userData; + } + + /// + ///wParam + /// + public IntPtr WideParameter + { + get; + private set; + } + + /// + ///lParam + /// + public IntPtr LeftParameter + { + get; + private set; + } + + /// + ///UserData + /// + public IntPtr UserData + { + get; + private set; + } +} + +/// +///Describes the file that is being processed for the ProcessFileEvent. +/// +public class +ProcessFileEventArgs : EventArgs +{ + /// + ///Default constructor. + /// + ///Fully qualified path and file name. For example: c:\file.sys. + ///Default is false - skip file and continue. + ///Set to true to abort the entire image capture. + public + ProcessFileEventArgs( + string file, + IntPtr skipFileFlag) + { + + m_FilePath = file; + m_SkipFileFlag = skipFileFlag; + } + + /// + ///Skip file from being imaged. + /// + public void + SkipFile() + { + byte[] byteBuffer = + { + 0 + }; + int byteBufferSize = byteBuffer.Length; + Marshal.Copy(byteBuffer, 0, m_SkipFileFlag, byteBufferSize); + } + + /// + ///Fully qualified path and file name. + /// + public string + FilePath + { + get + { + string stringToReturn = ""; + if (m_FilePath != null) + { + stringToReturn = m_FilePath; + } + return stringToReturn; + } + } + + /// + ///Flag to indicate if the entire image capture should be aborted. + ///Default is false - skip file and continue. Setting to true will + ///abort the entire image capture. + /// + public bool Abort + { + set { m_Abort = value; } + get { return m_Abort; } + } + + private string m_FilePath; + private bool m_Abort; + private IntPtr m_SkipFileFlag; + +} + +#endregion WIM Interop + +#region VHD Interop +// Based on code written by the Hyper-V Test team. +/// +/// The Virtual Hard Disk class provides methods for creating and manipulating Virtual Hard Disk files. +/// +public class +VirtualHardDisk +{ + #region Static Methods + + #region Sparse Disks + + /// + /// Abbreviated signature of CreateSparseDisk so it's easier to use from WIM2VHD. + /// + /// The type of disk to create, VHD or VHDX. + /// The path of the disk to create. + /// The maximum size of the disk to create. + /// Overwrite the VHD if it already exists. + public static void + CreateSparseDisk( + NativeMethods.VirtualStorageDeviceType virtualStorageDeviceType, + string path, + ulong size, + bool overwrite) + { + + CreateSparseDisk( + path, + size, + overwrite, + null, + IntPtr.Zero, + (virtualStorageDeviceType == NativeMethods.VirtualStorageDeviceType.VHD) + ? NativeMethods.DEFAULT_BLOCK_SIZE + : 0, + virtualStorageDeviceType, + NativeMethods.DISK_SECTOR_SIZE); + } + + /// + /// Creates a new sparse (dynamically expanding) virtual hard disk (.vhd). Supports both sync and async modes. + /// The VHD image file uses only as much space on the backing store as needed to store the actual data the VHD currently contains. + /// + /// The path and name of the VHD to create. + /// The size of the VHD to create in bytes. + /// When creating this type of VHD, the VHD API does not test for free space on the physical backing store based on the maximum size requested, + /// therefore it is possible to successfully create a dynamic VHD with a maximum size larger than the available physical disk free space. + /// The maximum size of a dynamic VHD is 2,040 GB. The minimum size is 3 MB. + /// Optional path to pre-populate the new virtual disk object with block data from an existing disk + /// This path may refer to a VHD or a physical disk. Use NULL if you don't want a source. + /// If the VHD exists, setting this parameter to 'True' will delete it and create a new one. + /// If not null, the operation runs in async mode + /// Block size for the VHD. + /// VHD format version (VHD1 or VHD2) + /// Sector size for the VHD. + /// Thrown when an invalid size is specified + /// Thrown when source VHD is not found. + /// Thrown when there was an error while creating the default security descriptor. + /// Thrown when an error occurred while creating the VHD. + public static void + CreateSparseDisk( + string path, + ulong size, + bool overwrite, + string source, + IntPtr overlapped, + uint blockSizeInBytes, + NativeMethods.VirtualStorageDeviceType virtualStorageDeviceType, + uint sectorSizeInBytes) + { + + // Validate the virtualStorageDeviceType + if (virtualStorageDeviceType != NativeMethods.VirtualStorageDeviceType.VHD && virtualStorageDeviceType != NativeMethods.VirtualStorageDeviceType.VHDX) + { + + throw ( + new ArgumentOutOfRangeException( + "virtualStorageDeviceType", + virtualStorageDeviceType, + "VirtualStorageDeviceType must be VHD or VHDX." + )); + } + + // Validate size. It needs to be a multiple of DISK_SECTOR_SIZE (512)... + if ((size % NativeMethods.DISK_SECTOR_SIZE) != 0) + { + + throw ( + new ArgumentOutOfRangeException( + "size", + size, + "The size of the virtual disk must be a multiple of 512." + )); + } + + if ((!String.IsNullOrEmpty(source)) && (!System.IO.File.Exists(source))) + { + + throw ( + new System.IO.FileNotFoundException( + "Unable to find the source file.", + source + )); + } + + if ((overwrite) && (System.IO.File.Exists(path))) + { + + System.IO.File.Delete(path); + } + + NativeMethods.CreateVirtualDiskParameters createParams = new NativeMethods.CreateVirtualDiskParameters(); + + // Select the correct version. + createParams.Version = (virtualStorageDeviceType == NativeMethods.VirtualStorageDeviceType.VHD) + ? NativeMethods.CreateVirtualDiskVersion.Version1 + : NativeMethods.CreateVirtualDiskVersion.Version2; + + createParams.UniqueId = Guid.NewGuid(); + createParams.MaximumSize = size; + createParams.BlockSizeInBytes = blockSizeInBytes; + createParams.SectorSizeInBytes = sectorSizeInBytes; + createParams.ParentPath = null; + createParams.SourcePath = source; + createParams.OpenFlags = NativeMethods.OpenVirtualDiskFlags.None; + createParams.GetInfoOnly = false; + createParams.ParentVirtualStorageType = new NativeMethods.VirtualStorageType(); + createParams.SourceVirtualStorageType = new NativeMethods.VirtualStorageType(); + + // + // Create and init a security descriptor. + // Since we're creating an essentially blank SD to use with CreateVirtualDisk + // the VHD will take on the security values from the parent directory. + // + + NativeMethods.SecurityDescriptor securityDescriptor; + if (!NativeMethods.InitializeSecurityDescriptor(out securityDescriptor, 1)) + { + + throw ( + new SecurityException( + "Unable to initialize the security descriptor for the virtual disk." + )); + } + + NativeMethods.VirtualStorageType virtualStorageType = new NativeMethods.VirtualStorageType(); + virtualStorageType.DeviceId = virtualStorageDeviceType; + virtualStorageType.VendorId = NativeMethods.VirtualStorageTypeVendorMicrosoft; + + SafeFileHandle vhdHandle; + + uint returnCode = NativeMethods.CreateVirtualDisk( + ref virtualStorageType, + path, + (virtualStorageDeviceType == NativeMethods.VirtualStorageDeviceType.VHD) + ? NativeMethods.VirtualDiskAccessMask.All + : NativeMethods.VirtualDiskAccessMask.None, + ref securityDescriptor, + NativeMethods.CreateVirtualDiskFlags.None, + 0, + ref createParams, + overlapped, + out vhdHandle); + + vhdHandle.Close(); + + if (NativeMethods.ERROR_SUCCESS != returnCode && NativeMethods.ERROR_IO_PENDING != returnCode) + { + + throw ( + new Win32Exception( + (int)returnCode + )); + } + } + + #endregion Sparse Disks + + #endregion Static Methods + +} +#endregion VHD Interop +} +"@ + + Add-Type -TypeDefinition $code -ReferencedAssemblies "System.Xml","System.Linq","System.Xml.Linq" -ErrorAction SilentlyContinue +} diff --git a/deployment/dsc/azshcihost/Hyper-ConvertImage/10.2/Hyper-ConvertImage.psd1 b/deployment/dsc/azshcihost/Hyper-ConvertImage/10.2/Hyper-ConvertImage.psd1 new file mode 100644 index 0000000..63fe5f2 Binary files /dev/null and b/deployment/dsc/azshcihost/Hyper-ConvertImage/10.2/Hyper-ConvertImage.psd1 differ diff --git a/deployment/dsc/azshcihost/Hyper-ConvertImage/10.2/Hyper-ConvertImage.psm1 b/deployment/dsc/azshcihost/Hyper-ConvertImage/10.2/Hyper-ConvertImage.psm1 new file mode 100644 index 0000000..8bb5095 --- /dev/null +++ b/deployment/dsc/azshcihost/Hyper-ConvertImage/10.2/Hyper-ConvertImage.psm1 @@ -0,0 +1,4550 @@ +<# + This is PS Module — the new long-term home for Convert-WindowsImage and sister functions. + + Copyright (c) Microsoft Corporation. All rights reserved. + + .NOTES + Use of this sample source code is subject to the terms of the Microsoft + license agreement under which you licensed this sample source code. If + you did not accept the terms of the license agreement, you are not + authorized to use this sample source code. For the terms of the license, + please see the license agreement between you and Microsoft or, if applicable, + see the LICENSE.RTF on your install media or the root of your tools installation. + THE SAMPLE SOURCE CODE IS PROVIDED "AS IS", WITH NO WARRANTIES. + + .SYNOPSIS + Creates a bootable VHD(X) based on Windows 7,8, 10 or Windows Server 2012, 2012R2, 2016, 2019 installation media. + + .DESCRIPTION + Creates a bootable VHD(X) based on Windows 7,8, 10 or Windows Server 2012, 2012R2, 2016, 2019 installation media. + + .PARAMETER SourcePath + The complete path to the WIM or ISO file that will be converted to a Virtual Hard Disk. + The ISO file must be valid Windows installation media to be recognized successfully. + + .PARAMETER CacheSource + If the source WIM/ISO was copied locally, we delete it by default. + Pass $true to cache the source image from the temp directory. + + .PARAMETER VHDPath + The name and path of the Virtual Hard Disk to create. + Omitting this parameter will create the Virtual Hard Disk is the current directory, (or, + if specified by the -WorkingDirectory parameter, the working directory) and will automatically + name the file in the following format: + + ....___. + i.e.: + 9200.0.amd64fre.winmain_win8rtm.120725-1247_client_professional_en-us.vhd(x) + + .PARAMETER WorkingDirectory + Specifies the directory where the VHD(X) file should be generated. + If specified along with -VHDPath, the -WorkingDirectory value is ignored. + The default value is the current directory ($pwd). + + .PARAMETER TempDirectory + Specifies the directory where the logs and ISO files should be placed. + The default value is the temp directory ($env:Temp). + + .PARAMETER SizeBytes + The size of the Virtual Hard Disk to create. + For fixed disks, the VHD(X) file will be allocated all of this space immediately. + For dynamic disks, this will be the maximum size that the VHD(X) can grow to. + The default value is 40GB. + + .PARAMETER VhdFormat + Specifies whether to create a VHD or VHDX formatted Virtual Hard Disk. + The default is AUTO, which will create a VHD if using the BIOS disk layout or + VHDX if using UEFI or WindowsToGo layouts. + + .PARAMETER DiskLayout + Specifies whether to build the image for BIOS (MBR), UEFI (GPT), or WindowsToGo (MBR). + Generation 1 VMs require BIOS (MBR) images. Generation 2 VMs require UEFI (GPT) images. + Windows To Go images will boot in UEFI or BIOS but are not technically supported (upgrade + doesn't work) + + .PARAMETER UnattendPath + The complete path to an unattend.xml file that can be injected into the VHD(X). + + .PARAMETER Edition + The name or image index of the image to apply from the WIM. + + .PARAMETER Passthru + Specifies that the full path to the VHD(X) that is created should be + returned on the pipeline. + + .PARAMETER BCDBoot + By default, the version of BCDBOOT.EXE that is present in \Windows\System32 + is used by Convert-WindowsImage. If you need to specify an alternate version, + use this parameter to do so. + + .PARAMETER MergeFolder + Specifies additional MergeFolder path to be added to the root of the VHD(X) + + .PARAMETER BCDinVHD + Specifies the purpose of the VHD(x). Use NativeBoot to skip cration of BCD store + inside the VHD(x). Use VirtualMachine (or do not specify this option) to ensure + the BCD store is created inside the VHD(x). + + .PARAMETER Driver + Full path to driver(s) (.inf files) to inject to the OS inside the VHD(x). + + .PARAMETER ExpandOnNativeBoot + Specifies whether to expand the VHD(x) to its maximum suze upon native boot. + The default is True. Set to False to disable expansion. + + .PARAMETER RemoteDesktopEnable + Enable Remote Desktop to connect to the OS inside the VHD(x) upon provisioning. + Does not include Windows Firewall rules (firewall exceptions). The default is False. + + .PARAMETER Feature + Enables specified Windows Feature(s). Note that you need to specify the Internal names + understood by DISM and DISM CMDLets (e.g. NetFx3) instead of the "Friendly" names + from Server Manager CMDLets (e.g. NET-Framework-Core). + + .PARAMETER Package + Injects specified Windows Package(s). Accepts path to either a directory or individual + CAB or MSU file. + + .PARAMETER ShowUI + Specifies that the Graphical User Interface should be displayed. + + .PARAMETER EnableDebugger + Configures kernel debugging for the VHD(X) being created. + EnableDebugger takes a single argument which specifies the debugging transport to use. + Valid transports are: None, Serial, 1394, USB, Network, Local. + + Depending on the type of transport selected, additional configuration parameters will become + available. + + Serial: + -ComPort - The COM port number to use while communicating with the debugger. + The default value is 1 (indicating COM1). + -BaudRate - The baud rate (in bps) to use while communicating with the debugger. + The default value is 115200, valid values are: + 9600, 19200, 38400, 56700, 115200 + + 1394: + -Channel - The 1394 channel used to communicate with the debugger. + The default value is 10. + + USB: + -Target - The target name used for USB debugging. + The default value is "debugging". + + Network: + -IPAddress - The IP address of the debugging host computer. + -Port - The port on which to connect to the debugging host. + The default value is 50000, with a minimum value of 49152. + -Key - The key used to encrypt the connection. Only [0-9] and [a-z] are allowed. + -nodhcp - Prevents the use of DHCP to obtain the target IP address. + -newkey - Specifies that a new encryption key should be generated for the connection. + + .PARAMETER DismPath + Full Path to an alternative version of the Dism.exe tool. The default is the current OS version. + + .PARAMETER ApplyEA + Specifies that any EAs captured in the WIM should be applied to the VHD. + The default is False. + + .EXAMPLE + Convert-WindowsImage -SourcePath D:\foo\install.wim -Edition Professional -WorkingDirectory D:\foo + + This command will create a 40GB dynamically expanding VHD in the D:\foo folder. + The VHD will be based on the Professional edition from D:\foo\install.wim, + and will be named automatically. + + .EXAMPLE + Convert-WindowsImage -SourcePath D:\foo\Win7SP1.iso -Edition Ultimate -VHDPath D:\foo\Win7_Ultimate_SP1.vhd + + This command will parse the ISO file D:\foo\Win7SP1.iso and try to locate + \sources\install.wim. If that file is found, it will be used to create a + dynamically-expanding 40GB VHD containing the Ultimate SKU, and will be + named D:\foo\Win7_Ultimate_SP1.vhd + + .EXAMPLE + Convert-WindowsImage -SourcePath D:\foo\install.wim -Edition Professional -EnableDebugger Serial -ComPort 2 -BaudRate 38400 + + This command will create a VHD from D:\foo\install.wim of the Professional SKU. + Serial debugging will be enabled in the VHD via COM2 at a baud rate of 38400bps. + + .OUTPUTS + System.IO.FileInfo +#> + +function +Convert-WindowsImage { + [CmdletBinding( + + DefaultParameterSetName = "DiskLayout", + HelpURI = + "https://github.com/Microsoft/Virtualization-Documentation/tree/master/hyperv-tools/Convert-WindowsImage" + )] + + param( + [Parameter( + ParameterSetName = "DiskLayout", + Mandatory = $True, + ValueFromPipeline = $True)] + [Parameter( + ParameterSetName = "PartitionStyle", + Mandatory = $True, + ValueFromPipeline = $True)] + [Alias("WIM")] + [string] + [ValidateNotNullOrEmpty()] + [ValidateScript( + { + + # This helps to work around the issue when PowerShell does not immediately + # recognize newly mounted drives + $Drive = Get-PSDrive + Test-Path -Path ( Resolve-Path -Path $PSItem ).Path + } + )] + $SourcePath, + + [Parameter(ParameterSetName = "DiskLayout")] + [Parameter(ParameterSetName = "PartitionStyle")] + [switch] + $CacheSource = $false, + + [Parameter(ParameterSetName = "DiskLayout")] + [Parameter(ParameterSetName = "PartitionStyle")] + [Alias("SKU")] + [string[]] + [ValidateNotNullOrEmpty()] + $Edition = 1, + + [Parameter(ParameterSetName = "DiskLayout")] + [Parameter(ParameterSetName = "PartitionStyle")] + [Alias("WorkDir")] + [string] + [ValidateNotNullOrEmpty()] + [ValidateScript( + { Test-Path -Path $PSItem } + )] + $WorkingDirectory = $pwd, + + [Parameter(ParameterSetName = "DiskLayout")] + [Parameter(ParameterSetName = "PartitionStyle")] + [Alias("TempDir")] + [string] + [ValidateNotNullOrEmpty()] + $TempDirectory = $env:Temp, + + [Parameter(ParameterSetName = "DiskLayout")] + [Parameter(ParameterSetName = "PartitionStyle")] + [Alias("VHD")] + [string] + [ValidateNotNullOrEmpty()] + $VhdPath, + + [Parameter(ParameterSetName = "DiskLayout")] + [Parameter(ParameterSetName = "PartitionStyle")] + [Alias("Size")] + [UInt64] + [ValidateNotNullOrEmpty()] + [ValidateRange( 512MB, 64TB )] + $SizeBytes = 25GB, + + [Parameter(ParameterSetName = "DiskLayout")] + [Parameter(ParameterSetName = "PartitionStyle")] + [Alias("Format")] + [string] + [ValidateNotNullOrEmpty()] + [ValidateSet( + "VHD", + "VHDX", + "AUTO" + )] + $VhdFormat = "AUTO", + + [Parameter(ParameterSetName = "DiskLayout")] + [Parameter(ParameterSetName = "PartitionStyle")] + [Alias("DiskType")] + [string] + [ValidateNotNullOrEmpty()] + [ValidateSet("Dynamic", "Fixed")] + $VhdType = "Dynamic", + + [Parameter(ParameterSetName = "DiskLayout")] + [Parameter(ParameterSetName = "PartitionStyle")] + [Alias("MergeFolder")] + [string] + [ValidateNotNullOrEmpty()] + $MergeFolderPath = "", + + [Parameter( + ParameterSetName = "DiskLayout", + Mandatory = $True + )] + [Alias("Layout")] + [string] + [ValidateNotNullOrEmpty()] + [ValidateSet( + "BIOS", + "UEFI", + "WindowsToGo" + )] + $DiskLayout, + + [Parameter( + ParameterSetName = "PartitionStyle", + Mandatory = $True + )] + [string] + [ValidateNotNullOrEmpty()] + [ValidateSet( + "MBR", + "GPT", + "MBRforWindowsToGo" + )] + $VhdPartitionStyle, + + [Parameter(ParameterSetName = "DiskLayout")] + [Parameter(ParameterSetName = "PartitionStyle")] + [string] + [ValidateNotNullOrEmpty()] + [ValidateSet( "NativeBoot", "VirtualMachine" )] + $BcdInVhd = "VirtualMachine", + + [Parameter(ParameterSetName = "DiskLayout")] + [Parameter(ParameterSetName = "PartitionStyle")] + [Parameter(ParameterSetName = "UI")] + [string] + $BcdBoot = "bcdboot.exe", + + [Parameter(ParameterSetName = "DiskLayout")] + [Parameter(ParameterSetName = "PartitionStyle")] + [Parameter(ParameterSetName = "UI")] + [string] + [ValidateNotNullOrEmpty()] + [ValidateSet( + "None", + "Serial", + "1394", + "USB", + "Local", + "Network" + )] + $EnableDebugger = "None", + + [Parameter(ParameterSetName = "DiskLayout")] + [Parameter(ParameterSetName = "PartitionStyle")] + [string[]] + [ValidateNotNullOrEmpty()] + $Feature, + + [Parameter(ParameterSetName = "DiskLayout")] + [Parameter(ParameterSetName = "PartitionStyle")] + [string[]] + [ValidateNotNullOrEmpty()] + [ValidateScript( + { Test-Path -Path ( Resolve-Path -Path $PSItem ).Path } + )] + $Driver, + + [Parameter(ParameterSetName = "DiskLayout")] + [Parameter(ParameterSetName = "PartitionStyle")] + [string[]] + [ValidateNotNullOrEmpty()] + [ValidateScript( + { Test-Path -Path ( Resolve-Path -Path $PSItem ).Path } + )] + $Package, + + [Parameter(ParameterSetName = "DiskLayout")] + [Parameter(ParameterSetName = "PartitionStyle")] + [switch] + $ExpandOnNativeBoot = $true, + + [Parameter(ParameterSetName = "DiskLayout")] + [Parameter(ParameterSetName = "PartitionStyle")] + [switch] + $RemoteDesktopEnable = $false, + + [Parameter(ParameterSetName = "DiskLayout")] + [Parameter(ParameterSetName = "PartitionStyle")] + [Alias("Unattend")] + [string] + [ValidateNotNullOrEmpty()] + [ValidateScript( + { Test-Path -Path ( Resolve-Path -Path $PSItem ).Path } + )] + $UnattendPath, + + [Parameter(ParameterSetName = "DiskLayout")] + [Parameter(ParameterSetName = "PartitionStyle")] + [Parameter(ParameterSetName = "UI")] + [switch] + $Passthru, + + [Parameter(ParameterSetName = "DiskLayout")] + [Parameter(ParameterSetName = "PartitionStyle")] + [string] + [ValidateNotNullOrEmpty()] + [ValidateScript( + { Test-Path -Path ( Resolve-Path -Path $PSItem ).Path } + )] + $DismPath, + + [Parameter(ParameterSetName = "DiskLayout")] + [Parameter(ParameterSetName = "PartitionStyle")] + [switch] + $ApplyEA = $false, + + [Parameter(ParameterSetName = "UI")] + [switch] + $ShowUI, + + [Parameter(ParameterSetName = "DiskLayout")] + [Parameter(ParameterSetName = "PartitionStyle")] + [System.Globalization.CultureInfo] + [ValidateNotNullOrEmpty()] + $BcdLocale + ) + + #region Code + + # Begin Dynamic Parameters + # Create the parameters for the various types of debugging. + + DynamicParam { + + # Set up the dynamic parameters. + # Dynamic parameters are only available if certain conditions are met, so they'll only show up + # as valid parameters when those conditions apply. Here, the conditions are based on the value of + # the EnableDebugger parameter. Depending on which of a set of values is the specified argument + # for EnableDebugger, different parameters will light up, as outlined below. + + $parameterDictionary = New-Object -TypeName "System.Management.Automation.RuntimeDefinedParameterDictionary" + + If (Test-Path -Path "Variable:\EnableDebugger") { + Switch ($EnableDebugger) { + "Serial" { + #region ComPort + + $ComPortAttr = New-Object System.Management.Automation.ParameterAttribute + $ComPortAttr.ParameterSetName = "__AllParameterSets" + $ComPortAttr.Mandatory = $false + + $ComPortValidator = New-Object System.Management.Automation.ValidateRangeAttribute( + 1, + 10 # Is that a good maximum? + ) + + $ComPortNotNull = New-Object System.Management.Automation.ValidateNotNullOrEmptyAttribute + + $ComPortAttrCollection = New-Object System.Collections.ObjectModel.Collection[System.Attribute] + $ComPortAttrCollection.Add($ComPortAttr) + $ComPortAttrCollection.Add($ComPortValidator) + $ComPortAttrCollection.Add($ComPortNotNull) + + $ComPort = New-Object System.Management.Automation.RuntimeDefinedParameter( + "ComPort", + [UInt16], + $ComPortAttrCollection + ) + + # By default, use COM1 + $ComPort.Value = 1 + $parameterDictionary.Add("ComPort", $ComPort) + #endregion ComPort + + #region BaudRate + $BaudRateAttr = New-Object System.Management.Automation.ParameterAttribute + $BaudRateAttr.ParameterSetName = "__AllParameterSets" + $BaudRateAttr.Mandatory = $false + + $BaudRateValidator = New-Object System.Management.Automation.ValidateSetAttribute( + 9600, 19200, 38400, 57600, 115200 + ) + + $BaudRateNotNull = New-Object System.Management.Automation.ValidateNotNullOrEmptyAttribute + + $BaudRateAttrCollection = New-Object System.Collections.ObjectModel.Collection[System.Attribute] + $BaudRateAttrCollection.Add($BaudRateAttr) + $BaudRateAttrCollection.Add($BaudRateValidator) + $BaudRateAttrCollection.Add($BaudRateNotNull) + + $BaudRate = New-Object System.Management.Automation.RuntimeDefinedParameter( + "BaudRate", + [UInt32], + $BaudRateAttrCollection + ) + + # By default, use 115,200. + $BaudRate.Value = 115200 + $parameterDictionary.Add("BaudRate", $BaudRate) + #endregion BaudRate + + break + } + + "1394" { + $ChannelAttr = New-Object System.Management.Automation.ParameterAttribute + $ChannelAttr.ParameterSetName = "__AllParameterSets" + $ChannelAttr.Mandatory = $false + + $ChannelValidator = New-Object System.Management.Automation.ValidateRangeAttribute( + 0, + 62 + ) + + $ChannelNotNull = New-Object System.Management.Automation.ValidateNotNullOrEmptyAttribute + + $ChannelAttrCollection = New-Object System.Collections.ObjectModel.Collection[System.Attribute] + $ChannelAttrCollection.Add($ChannelAttr) + $ChannelAttrCollection.Add($ChannelValidator) + $ChannelAttrCollection.Add($ChannelNotNull) + + $Channel = New-Object System.Management.Automation.RuntimeDefinedParameter( + "Channel", + [UInt16], + $ChannelAttrCollection + ) + + # By default, use channel 10 + $Channel.Value = 10 + $parameterDictionary.Add("Channel", $Channel) + break + } + + "USB" { + $TargetAttr = New-Object System.Management.Automation.ParameterAttribute + $TargetAttr.ParameterSetName = "__AllParameterSets" + $TargetAttr.Mandatory = $false + + $TargetNotNull = New-Object System.Management.Automation.ValidateNotNullOrEmptyAttribute + + $TargetAttrCollection = New-Object System.Collections.ObjectModel.Collection[System.Attribute] + $TargetAttrCollection.Add($TargetAttr) + $TargetAttrCollection.Add($TargetNotNull) + + $Target = New-Object System.Management.Automation.RuntimeDefinedParameter( + "Target", + [string], + $TargetAttrCollection + ) + + # By default, use target = "debugging" + $Target.Value = "Debugging" + $parameterDictionary.Add("Target", $Target) + break + } + + "Network" { + #region IP + $IpAttr = New-Object System.Management.Automation.ParameterAttribute + $IpAttr.ParameterSetName = "__AllParameterSets" + $IpAttr.Mandatory = $true + + $IpValidator = New-Object System.Management.Automation.ValidatePatternAttribute( + "\b(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\b" + ) + $IpNotNull = New-Object System.Management.Automation.ValidateNotNullOrEmptyAttribute + + $IpAttrCollection = New-Object System.Collections.ObjectModel.Collection[System.Attribute] + $IpAttrCollection.Add($IpAttr) + $IpAttrCollection.Add($IpValidator) + $IpAttrCollection.Add($IpNotNull) + + $IP = New-Object System.Management.Automation.RuntimeDefinedParameter( + "IPAddress", + [string], + $IpAttrCollection + ) + + # There's no good way to set a default value for this. + $parameterDictionary.Add("IPAddress", $IP) + #endregion IP + + #region Port + $PortAttr = New-Object System.Management.Automation.ParameterAttribute + $PortAttr.ParameterSetName = "__AllParameterSets" + $PortAttr.Mandatory = $false + + $PortValidator = New-Object System.Management.Automation.ValidateRangeAttribute( + 49152, + 50039 + ) + + $PortNotNull = New-Object System.Management.Automation.ValidateNotNullOrEmptyAttribute + + $PortAttrCollection = New-Object System.Collections.ObjectModel.Collection[System.Attribute] + $PortAttrCollection.Add($PortAttr) + $PortAttrCollection.Add($PortValidator) + $PortAttrCollection.Add($PortNotNull) + + + $Port = New-Object System.Management.Automation.RuntimeDefinedParameter( + "Port", + [UInt16], + $PortAttrCollection + ) + + # By default, use port 50000 + $Port.Value = 50000 + $parameterDictionary.Add("Port", $Port) + #endregion Port + + #region Key + $KeyAttr = New-Object System.Management.Automation.ParameterAttribute + $KeyAttr.ParameterSetName = "__AllParameterSets" + $KeyAttr.Mandatory = $true + + $KeyValidator = New-Object System.Management.Automation.ValidatePatternAttribute( + "\b([A-Z0-9]+).([A-Z0-9]+).([A-Z0-9]+).([A-Z0-9]+)\b" + ) + + $KeyNotNull = New-Object System.Management.Automation.ValidateNotNullOrEmptyAttribute + + $KeyAttrCollection = New-Object System.Collections.ObjectModel.Collection[System.Attribute] + $KeyAttrCollection.Add($KeyAttr) + $KeyAttrCollection.Add($KeyValidator) + $KeyAttrCollection.Add($KeyNotNull) + + $Key = New-Object System.Management.Automation.RuntimeDefinedParameter( + "Key", + [string], + $KeyAttrCollection + ) + + # Don't set a default key. + $parameterDictionary.Add("Key", $Key) + #endregion Key + + #region NoDHCP + $NoDHCPAttr = New-Object System.Management.Automation.ParameterAttribute + $NoDHCPAttr.ParameterSetName = "__AllParameterSets" + $NoDHCPAttr.Mandatory = $false + + $NoDHCPAttrCollection = New-Object System.Collections.ObjectModel.Collection[System.Attribute] + $NoDHCPAttrCollection.Add($NoDHCPAttr) + + $NoDHCP = New-Object System.Management.Automation.RuntimeDefinedParameter( + "NoDHCP", + [switch], + $NoDHCPAttrCollection + ) + + $parameterDictionary.Add("NoDHCP", $NoDHCP) + #endregion NoDHCP + + #region NewKey + $NewKeyAttr = New-Object System.Management.Automation.ParameterAttribute + $NewKeyAttr.ParameterSetName = "__AllParameterSets" + $NewKeyAttr.Mandatory = $false + + $NewKeyAttrCollection = New-Object System.Collections.ObjectModel.Collection[System.Attribute] + $NewKeyAttrCollection.Add($NewKeyAttr) + + $NewKey = New-Object System.Management.Automation.RuntimeDefinedParameter( + "NewKey", + [switch], + $NewKeyAttrCollection + ) + + # Don't set a default key. + $parameterDictionary.Add("NewKey", $NewKey) + #endregion NewKey + + break + } + + # There's nothing to do for local debugging. + # Synthetic debugging is not yet implemented. + + default { + break + } + } + } + return $parameterDictionary + } + + Begin { + + Set-StrictMode -Version 3 + $Module = Import-ModuleEx -Name "Dism" + $Module = Import-ModuleEx -Name "Storage" + + #region Constants and Pseudo-Constants + + # Name of the script, obviously. + $scriptName = "Convert-WindowsImage" + + # Session key, used for keeping records unique between multiple runs. + $sessionKey = [Guid]::NewGuid().ToString() + + # Log folder path. + $logFolder = [io.path]::Combine( $TempDirectory, $scriptName, $sessionKey ) + + # Maximum size for VHD is ~2040GB. + $vhdMaxSize = 2040GB + + # Maximum size for VHDX is ~64TB. + $vhdxMaxSize = 64TB + + # The lowest supported *image* version; making sure we don't run against Vista/2k8. + $lowestSupportedVersion = New-Object -TypeName "Version" -ArgumentList "6.1" + + # The lowest supported *host* build. Set to Win8 CP. + $lowestSupportedBuild = 9200 + + # Keeps track on whether the script itself enabled Transcript + # (vs. it was enabled by user) + $Transcripting = $false + + # Since we use the VhdFormat in output, make it uppercase. + # We'll make it lowercase again when we use it as a file extension. + $VhdFormat = $VhdFormat.ToUpper() + + If (Test-Path -Path "Variable:\VhdPartitionStyle") { + Switch ($VhdPartitionStyle) { + "Mbr" { + $DiskLayout = "Bios" + } + + "Gpt" { + $DiskLayout = "Uefi" + } + + "MBRforWindowsToGo" { + $DiskLayout = "WindowsToGo" + } + } + } + + If (Get-Command -Name "Get-WindowsOptionalFeature" -ErrorAction "SilentlyContinue") { + Try { + $OSVersion = (Get-ItemProperty -Path "HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion" -Name ProductName).ProductName + if ($OSVersion -like "Windows Server*") { + $hyperVEnabled = + [bool]( Get-WindowsOptionalFeature -Online -FeatureName "Microsoft-Hyper-V" -Verbose:$False ).State -and + [bool]( Get-WindowsOptionalFeature -Online -FeatureName "Microsoft-Hyper-V-Management-PowerShell" -Verbose:$False ).State + } + else { + $hyperVEnabled = + [bool]( Get-WindowsOptionalFeature -Online -FeatureName "Microsoft-Hyper-V-Services" -Verbose:$False ).State -and + [bool]( Get-WindowsOptionalFeature -Online -FeatureName "Microsoft-Hyper-V-Management-PowerShell" -Verbose:$False ).State + } + } + Catch { + + # WinPE DISM does not support online queries. This will throw on non-WinPE machines + + $winpeVersion = (Get-ItemProperty -Path 'HKLM:\Software\Microsoft\Windows NT\CurrentVersion\WinPE').Version + + Write-Verbose -Message "Running WinPE version $winpeVersion" + + $hyperVEnabled = $False + } + } + Else { + $hyperVEnabled = $False + } + + #endregion Constants and Pseudo-Constants + + #region Here Strings + + # Banner text displayed during each run. + $Header = @" + +Windows(R) Image to Virtual Hard Disk Converter for Windows(R) 10 +Copyright (C) Microsoft Corporation. All rights reserved. + +"@ + + # Text used as the banner in the UI. + $UiHeader = @" +You can use the fields below to configure the VHD or VHDX that you want to create! +"@ + + #endregion Here Strings + } + + Process { + + $disk = $null + $openWim = $null + $openIso = $null + $openImage = $null + $vhdFinalName = $null + $vhdFinalPath = $null + $mountedHive = $null + $IsoPath = $null + $tempSource = $null + $vhd = @() + + Write-Information -MessageData $header + + Try { + #region Prepare variables + + # Create log folder + If (Test-Path -Path $logFolder) { + $Item = Remove-Item -Path $logFolder -Force -Recurse + } + + $Item = New-Item -Path $logFolder -ItemType "Directory" -Force + + # Try to start transcripting. If it's already running, we'll get an exception and swallow it. + Try { + $TranscriptPath = Join-Path -Path $logFolder -ChildPath "Convert-WindowsImageTranscript.txt" + $Transcript = Start-Transcript -Path $TranscriptPath -Force -ErrorAction "SilentlyContinue" + $Transcripting = $True + } + catch { + Write-Warning -Message "Transcription is already running. No Convert-WindowsImage-specific transcript will be created." + $Transcripting = $Talse + } + + # Add types + Add-WindowsImageTypes + + # Check to make sure we're running as Admin. + If (-Not ( Test-Admin )) { + Throw "Images can only be applied by an administrator. Please launch PowerShell elevated and run this script again." + } + + # Check to make sure we're running (at least) on Win8. + If (-Not ( Test-WindowsVersion )) { + Throw "$scriptName requires Windows 8 Consumer Preview or higher. Please use WIM2VHD.WSF (http://code.msdn.microsoft.com/wim2vhd) if you need to create VHDs from Windows 7." + } + + # Resolve the path for the unattend file. + If (-Not [string]::IsNullOrEmpty( $UnattendPath )) { + $UnattendPath = ( Resolve-Path -Path $UnattendPath ).Path + } + + # Note: UI code is deprecated and not maintained anymore. + If ($ShowUI) { + Write-Verbose -Message "Launching UI..." + Add-Type -AssemblyName System.Drawing, System.Windows.Forms + + #region Form Objects + $frmMain = New-Object System.Windows.Forms.Form + $groupBox4 = New-Object System.Windows.Forms.GroupBox + $btnGo = New-Object System.Windows.Forms.Button + $groupBox3 = New-Object System.Windows.Forms.GroupBox + $txtVhdName = New-Object System.Windows.Forms.TextBox + $label6 = New-Object System.Windows.Forms.Label + $btnWrkBrowse = New-Object System.Windows.Forms.Button + $cmbVhdSizeUnit = New-Object System.Windows.Forms.ComboBox + $numVhdSize = New-Object System.Windows.Forms.NumericUpDown + $cmbVhdFormat = New-Object System.Windows.Forms.ComboBox + $label5 = New-Object System.Windows.Forms.Label + $txtWorkingDirectory = New-Object System.Windows.Forms.TextBox + $label4 = New-Object System.Windows.Forms.Label + $label3 = New-Object System.Windows.Forms.Label + $label2 = New-Object System.Windows.Forms.Label + $label7 = New-Object System.Windows.Forms.Label + $txtUnattendFile = New-Object System.Windows.Forms.TextBox + $btnUnattendBrowse = New-Object System.Windows.Forms.Button + $groupBox2 = New-Object System.Windows.Forms.GroupBox + $cmbSkuList = New-Object System.Windows.Forms.ComboBox + $label1 = New-Object System.Windows.Forms.Label + $groupBox1 = New-Object System.Windows.Forms.GroupBox + $txtSourcePath = New-Object System.Windows.Forms.TextBox + $btnBrowseWim = New-Object System.Windows.Forms.Button + $openFileDialog1 = New-Object System.Windows.Forms.OpenFileDialog + $openFolderDialog1 = New-Object System.Windows.Forms.FolderBrowserDialog + $InitialFormWindowState = New-Object System.Windows.Forms.FormWindowState + + #endregion Form Objects + + #region Event scriptblocks. + + $btnGo_OnClick = { + $frmMain.Close() + } + + $btnWrkBrowse_OnClick = { + $openFolderDialog1.RootFolder = "Desktop" + $openFolderDialog1.Description = "Select the folder you'd like your VHD(X) to be created in." + $openFolderDialog1.SelectedPath = $WorkingDirectory + + $ret = $openFolderDialog1.ShowDialog() + + if ($ret -ilike "ok") { + $WorkingDirectory = $txtWorkingDirectory = $openFolderDialog1.SelectedPath + Write-Verbose -Message "Selected Working Directory is $WorkingDirectory..." + } + } + + $btnUnattendBrowse_OnClick = { + $openFileDialog1.InitialDirectory = $pwd + $openFileDialog1.Filter = "XML files (*.xml)|*.XML|All files (*.*)|*.*" + $openFileDialog1.FilterIndex = 1 + $openFileDialog1.CheckFileExists = $true + $openFileDialog1.CheckPathExists = $true + $openFileDialog1.FileName = $null + $openFileDialog1.ShowHelp = $false + $openFileDialog1.Title = "Select an unattend file..." + + $ret = $openFileDialog1.ShowDialog() + + if ($ret -ilike "ok") { + $UnattendPath = $txtUnattendFile.Text = $openFileDialog1.FileName + } + } + + $btnBrowseWim_OnClick = { + $openFileDialog1.InitialDirectory = $pwd + $openFileDialog1.Filter = "All compatible files (*.ISO, *.WIM)|*.ISO;*.WIM|All files (*.*)|*.*" + $openFileDialog1.FilterIndex = 1 + $openFileDialog1.CheckFileExists = $true + $openFileDialog1.CheckPathExists = $true + $openFileDialog1.FileName = $null + $openFileDialog1.ShowHelp = $false + $openFileDialog1.Title = "Select a source file..." + + $ret = $openFileDialog1.ShowDialog() + + if ($ret -ilike "ok") { + + if (([IO.FileInfo]$openFileDialog1.FileName).Extension -ilike ".iso") { + + if (Test-IsNetworkLocation $openFileDialog1.FileName) { + Write-Verbose -Message "Copying ISO $(Split-Path $openFileDialog1.FileName -Leaf) to temp folder..." + Write-Warning -Message "The UI may become non-responsive while this copy takes place..." + Copy-Item -Path $openFileDialog1.FileName -Destination $TempDirectory -Force + $openFileDialog1.FileName = "$($TempDirectory)\$(Split-Path $openFileDialog1.FileName -Leaf)" + } + + $txtSourcePath.Text = $IsoPath = (Resolve-Path $openFileDialog1.FileName).Path + Write-Verbose -Message "Opening ISO $(Split-Path $IsoPath -Leaf)..." + + $openIso = Mount-DiskImage -ImagePath $IsoPath -StorageType ISO -PassThru + + # Refresh the DiskImage and Drive object so we can get the real information about it. I assume this is a bug. + Get-PSDrive -PSProvider FileSystem | Out-Null + $openIso = Get-DiskImage -ImagePath $IsoPath + $driveLetter = ( Get-Volume -DiskImage $openIso ).DriveLetter + + $script:SourcePath = "$($driveLetter):\sources\install.wim" + + # Check to see if there's a WIM file we can muck about with. + Write-Verbose -Message "Looking for $($SourcePath)..." + if (!(Test-Path $SourcePath)) { + throw "The specified ISO does not appear to be valid Windows installation media." + } + } + else { + $txtSourcePath.Text = $script:SourcePath = $openFileDialog1.FileName + } + + # Check to see if the WIM is local, or on a network location. If the latter, copy it locally. + if (Test-IsNetworkLocation $SourcePath) { + Write-Verbose -Message "Copying WIM $(Split-Path $SourcePath -Leaf) to temp folder..." + Write-Warning -Message "The UI may become non-responsive while this copy takes place..." + Copy-Item -Path $SourcePath -Destination $TempDirectory -Force + $txtSourcePath.Text = $script:SourcePath = "$($TempDirectory)\$(Split-Path $SourcePath -Leaf)" + } + + $script:SourcePath = (Resolve-Path $SourcePath).Path + + Write-Verbose -Message "Scanning WIM metadata..." + + $tempOpenWim = $null + + try { + $tempOpenWim = New-Object WIM2VHD.WimFile $SourcePath + + # Let's see if we're running against an unstaged build. If we are, we need to blow up. + if ($tempOpenWim.ImageNames.Contains("Windows Longhorn Client") -or + $tempOpenWim.ImageNames.Contains("Windows Longhorn Server") -or + $tempOpenWim.ImageNames.Contains("Windows Longhorn Server Core")) { + [Windows.Forms.MessageBox]::Show( + "Convert-WindowsImage cannot run against unstaged builds. Please try again with a staged build.", + "WIM is incompatible!", + "OK", + "Error" + ) + + return + } + else { + $tempOpenWim.Images | % { $cmbSkuList.Items.Add($_.ImageFlags) } + $cmbSkuList.SelectedIndex = 0 + } + + } + catch { + throw "Unable to load WIM metadata!" + } + finally { + $tempOpenWim.Close() + Write-Debug -Message " Closing WIM metadata..." + } + } + } + + $OnLoadForm_StateCorrection = { + + # Correct the initial state of the form to prevent the .Net maximized form issue + $frmMain.WindowState = $InitialFormWindowState + } + + #endregion Event scriptblocks + + # Figure out VHD size and size unit. + $unit = $null + switch ([Math]::Round($SizeBytes.ToString().Length / 3)) { + 3 { $unit = "MB"; break } + 4 { $unit = "GB"; break } + 5 { $unit = "TB"; break } + default { $unit = ""; break } + } + + $quantity = Invoke-Expression -Command "$($SizeBytes) / 1$($unit)" + + #region Form Code + #region frmMain + $frmMain.DataBindings.DefaultDataSourceUpdateMode = 0 + $System_Drawing_Size = New-Object System.Drawing.Size + $System_Drawing_Size.Height = 579 + $System_Drawing_Size.Width = 512 + $frmMain.ClientSize = $System_Drawing_Size + $frmMain.Font = New-Object System.Drawing.Font("Segoe UI", 10, 0, 3, 1) + $frmMain.FormBorderStyle = 1 + $frmMain.MaximizeBox = $False + $frmMain.MinimizeBox = $False + $frmMain.Name = "frmMain" + $frmMain.StartPosition = 1 + $frmMain.Text = "Convert-WindowsImage UI" + #endregion frmMain + + #region groupBox4 + $groupBox4.DataBindings.DefaultDataSourceUpdateMode = 0 + $System_Drawing_Point = New-Object System.Drawing.Point + $System_Drawing_Point.X = 10 + $System_Drawing_Point.Y = 498 + $groupBox4.Location = $System_Drawing_Point + $groupBox4.Name = "groupBox4" + $System_Drawing_Size = New-Object System.Drawing.Size + $System_Drawing_Size.Height = 69 + $System_Drawing_Size.Width = 489 + $groupBox4.Size = $System_Drawing_Size + $groupBox4.TabIndex = 8 + $groupBox4.TabStop = $False + $groupBox4.Text = "4. Make the VHD!" + + $frmMain.Controls.Add($groupBox4) + #endregion groupBox4 + + #region btnGo + $btnGo.DataBindings.DefaultDataSourceUpdateMode = 0 + $System_Drawing_Point = New-Object System.Drawing.Point + $System_Drawing_Point.X = 39 + $System_Drawing_Point.Y = 24 + $btnGo.Location = $System_Drawing_Point + $btnGo.Name = "btnGo" + $System_Drawing_Size = New-Object System.Drawing.Size + $System_Drawing_Size.Height = 33 + $System_Drawing_Size.Width = 415 + $btnGo.Size = $System_Drawing_Size + $btnGo.TabIndex = 0 + $btnGo.Text = "&Make my VHD" + $btnGo.UseVisualStyleBackColor = $True + $btnGo.DialogResult = "OK" + $btnGo.add_Click($btnGo_OnClick) + + $groupBox4.Controls.Add($btnGo) + $frmMain.AcceptButton = $btnGo + #endregion btnGo + + #region groupBox3 + $groupBox3.DataBindings.DefaultDataSourceUpdateMode = 0 + $System_Drawing_Point = New-Object System.Drawing.Point + $System_Drawing_Point.X = 10 + $System_Drawing_Point.Y = 243 + $groupBox3.Location = $System_Drawing_Point + $groupBox3.Name = "groupBox3" + $System_Drawing_Size = New-Object System.Drawing.Size + $System_Drawing_Size.Height = 245 + $System_Drawing_Size.Width = 489 + $groupBox3.Size = $System_Drawing_Size + $groupBox3.TabIndex = 7 + $groupBox3.TabStop = $False + $groupBox3.Text = "3. Choose configuration options" + + $frmMain.Controls.Add($groupBox3) + #endregion groupBox3 + + #region txtVhdName + $txtVhdName.DataBindings.DefaultDataSourceUpdateMode = 0 + $System_Drawing_Point = New-Object System.Drawing.Point + $System_Drawing_Point.X = 25 + $System_Drawing_Point.Y = 150 + $txtVhdName.Location = $System_Drawing_Point + $txtVhdName.Name = "txtVhdName" + $System_Drawing_Size = New-Object System.Drawing.Size + $System_Drawing_Size.Height = 25 + $System_Drawing_Size.Width = 418 + $txtVhdName.Size = $System_Drawing_Size + $txtVhdName.TabIndex = 10 + + $groupBox3.Controls.Add($txtVhdName) + #endregion txtVhdName + + #region txtUnattendFile + $txtUnattendFile.DataBindings.DefaultDataSourceUpdateMode = 0 + $System_Drawing_Point = New-Object System.Drawing.Point + $System_Drawing_Point.X = 25 + $System_Drawing_Point.Y = 198 + $txtUnattendFile.Location = $System_Drawing_Point + $txtUnattendFile.Name = "txtUnattendFile" + $System_Drawing_Size = New-Object System.Drawing.Size + $System_Drawing_Size.Height = 25 + $System_Drawing_Size.Width = 418 + $txtUnattendFile.Size = $System_Drawing_Size + $txtUnattendFile.TabIndex = 11 + + $groupBox3.Controls.Add($txtUnattendFile) + #endregion txtUnattendFile + + #region label7 + $label7.DataBindings.DefaultDataSourceUpdateMode = 0 + $System_Drawing_Point = New-Object System.Drawing.Point + $System_Drawing_Point.X = 23 + $System_Drawing_Point.Y = 180 + $label7.Location = $System_Drawing_Point + $label7.Name = "label7" + $System_Drawing_Size = New-Object System.Drawing.Size + $System_Drawing_Size.Height = 23 + $System_Drawing_Size.Width = 175 + $label7.Size = $System_Drawing_Size + $label7.Text = "Unattend File (Optional)" + + $groupBox3.Controls.Add($label7) + #endregion label7 + + #region label6 + $label6.DataBindings.DefaultDataSourceUpdateMode = 0 + $System_Drawing_Point = New-Object System.Drawing.Point + $System_Drawing_Point.X = 23 + $System_Drawing_Point.Y = 132 + $label6.Location = $System_Drawing_Point + $label6.Name = "label6" + $System_Drawing_Size = New-Object System.Drawing.Size + $System_Drawing_Size.Height = 23 + $System_Drawing_Size.Width = 175 + $label6.Size = $System_Drawing_Size + $label6.Text = "VHD Name (Optional)" + + $groupBox3.Controls.Add($label6) + #endregion label6 + + #region btnUnattendBrowse + $btnUnattendBrowse.DataBindings.DefaultDataSourceUpdateMode = 0 + $System_Drawing_Point = New-Object System.Drawing.Point + $System_Drawing_Point.X = 449 + $System_Drawing_Point.Y = 199 + $btnUnattendBrowse.Location = $System_Drawing_Point + $btnUnattendBrowse.Name = "btnUnattendBrowse" + $System_Drawing_Size = New-Object System.Drawing.Size + $System_Drawing_Size.Height = 25 + $System_Drawing_Size.Width = 27 + $btnUnattendBrowse.Size = $System_Drawing_Size + $btnUnattendBrowse.TabIndex = 9 + $btnUnattendBrowse.Text = "..." + $btnUnattendBrowse.UseVisualStyleBackColor = $True + $btnUnattendBrowse.add_Click($btnUnattendBrowse_OnClick) + + $groupBox3.Controls.Add($btnUnattendBrowse) + #endregion btnUnattendBrowse + + #region btnWrkBrowse + $btnWrkBrowse.DataBindings.DefaultDataSourceUpdateMode = 0 + $System_Drawing_Point = New-Object System.Drawing.Point + $System_Drawing_Point.X = 449 + $System_Drawing_Point.Y = 98 + $btnWrkBrowse.Location = $System_Drawing_Point + $btnWrkBrowse.Name = "btnWrkBrowse" + $System_Drawing_Size = New-Object System.Drawing.Size + $System_Drawing_Size.Height = 25 + $System_Drawing_Size.Width = 27 + $btnWrkBrowse.Size = $System_Drawing_Size + $btnWrkBrowse.TabIndex = 9 + $btnWrkBrowse.Text = "..." + $btnWrkBrowse.UseVisualStyleBackColor = $True + $btnWrkBrowse.add_Click($btnWrkBrowse_OnClick) + + $groupBox3.Controls.Add($btnWrkBrowse) + #endregion btnWrkBrowse + + #region cmbVhdSizeUnit + $cmbVhdSizeUnit.DataBindings.DefaultDataSourceUpdateMode = 0 + $cmbVhdSizeUnit.FormattingEnabled = $True + $cmbVhdSizeUnit.Items.Add("MB") | Out-Null + $cmbVhdSizeUnit.Items.Add("GB") | Out-Null + $cmbVhdSizeUnit.Items.Add("TB") | Out-Null + $System_Drawing_Point = New-Object System.Drawing.Point + $System_Drawing_Point.X = 409 + $System_Drawing_Point.Y = 42 + $cmbVhdSizeUnit.Location = $System_Drawing_Point + $cmbVhdSizeUnit.Name = "cmbVhdSizeUnit" + $System_Drawing_Size = New-Object System.Drawing.Size + $System_Drawing_Size.Height = 25 + $System_Drawing_Size.Width = 67 + $cmbVhdSizeUnit.Size = $System_Drawing_Size + $cmbVhdSizeUnit.TabIndex = 5 + $cmbVhdSizeUnit.Text = $unit + + $groupBox3.Controls.Add($cmbVhdSizeUnit) + #endregion cmbVhdSizeUnit + + #region numVhdSize + $numVhdSize.DataBindings.DefaultDataSourceUpdateMode = 0 + $System_Drawing_Point = New-Object System.Drawing.Point + $System_Drawing_Point.X = 340 + $System_Drawing_Point.Y = 42 + $numVhdSize.Location = $System_Drawing_Point + $numVhdSize.Name = "numVhdSize" + $System_Drawing_Size = New-Object System.Drawing.Size + $System_Drawing_Size.Height = 25 + $System_Drawing_Size.Width = 63 + $numVhdSize.Size = $System_Drawing_Size + $numVhdSize.TabIndex = 4 + $numVhdSize.Value = $quantity + + $groupBox3.Controls.Add($numVhdSize) + #endregion numVhdSize + + #region cmbVhdFormat + $cmbVhdFormat.DataBindings.DefaultDataSourceUpdateMode = 0 + $cmbVhdFormat.FormattingEnabled = $True + $cmbVhdFormat.Items.Add("VHD") | Out-Null + $cmbVhdFormat.Items.Add("VHDX") | Out-Null + $System_Drawing_Point = New-Object System.Drawing.Point + $System_Drawing_Point.X = 25 + $System_Drawing_Point.Y = 42 + $cmbVhdFormat.Location = $System_Drawing_Point + $cmbVhdFormat.Name = "cmbVhdFormat" + $System_Drawing_Size = New-Object System.Drawing.Size + $System_Drawing_Size.Height = 25 + $System_Drawing_Size.Width = 136 + $cmbVhdFormat.Size = $System_Drawing_Size + $cmbVhdFormat.TabIndex = 0 + $cmbVhdFormat.Text = $VhdFormat + + $groupBox3.Controls.Add($cmbVhdFormat) + #endregion cmbVhdFormat + + #region label5 + $label5.DataBindings.DefaultDataSourceUpdateMode = 0 + $System_Drawing_Point = New-Object System.Drawing.Point + $System_Drawing_Point.X = 23 + $System_Drawing_Point.Y = 76 + $label5.Location = $System_Drawing_Point + $label5.Name = "label5" + $System_Drawing_Size = New-Object System.Drawing.Size + $System_Drawing_Size.Height = 23 + $System_Drawing_Size.Width = 264 + $label5.Size = $System_Drawing_Size + $label5.TabIndex = 8 + $label5.Text = "Working Directory" + + $groupBox3.Controls.Add($label5) + #endregion label5 + + #region txtWorkingDirectory + $txtWorkingDirectory.DataBindings.DefaultDataSourceUpdateMode = 0 + $System_Drawing_Point = New-Object System.Drawing.Point + $System_Drawing_Point.X = 25 + $System_Drawing_Point.Y = 99 + $txtWorkingDirectory.Location = $System_Drawing_Point + $txtWorkingDirectory.Name = "txtWorkingDirectory" + $System_Drawing_Size = New-Object System.Drawing.Size + $System_Drawing_Size.Height = 25 + $System_Drawing_Size.Width = 418 + $txtWorkingDirectory.Size = $System_Drawing_Size + $txtWorkingDirectory.TabIndex = 7 + $txtWorkingDirectory.Text = $WorkingDirectory + + $groupBox3.Controls.Add($txtWorkingDirectory) + #endregion txtWorkingDirectory + + #region label4 + $label4.DataBindings.DefaultDataSourceUpdateMode = 0 + $System_Drawing_Point = New-Object System.Drawing.Point + $System_Drawing_Point.X = 340 + $System_Drawing_Point.Y = 21 + $label4.Location = $System_Drawing_Point + $label4.Name = "label4" + $System_Drawing_Size = New-Object System.Drawing.Size + $System_Drawing_Size.Height = 27 + $System_Drawing_Size.Width = 86 + $label4.Size = $System_Drawing_Size + $label4.TabIndex = 6 + $label4.Text = "VHD Size" + + $groupBox3.Controls.Add($label4) + #endregion label4 + + #region label3 + $label3.DataBindings.DefaultDataSourceUpdateMode = 0 + $System_Drawing_Point = New-Object System.Drawing.Point + $System_Drawing_Point.X = 176 + $System_Drawing_Point.Y = 21 + $label3.Location = $System_Drawing_Point + $label3.Name = "label3" + $System_Drawing_Size = New-Object System.Drawing.Size + $System_Drawing_Size.Height = 27 + $System_Drawing_Size.Width = 92 + $label3.Size = $System_Drawing_Size + $label3.TabIndex = 3 + $label3.Text = "VHD Type" + + $groupBox3.Controls.Add($label3) + #endregion label3 + + #region label2 + $label2.DataBindings.DefaultDataSourceUpdateMode = 0 + $System_Drawing_Point = New-Object System.Drawing.Point + $System_Drawing_Point.X = 25 + $System_Drawing_Point.Y = 21 + $label2.Location = $System_Drawing_Point + $label2.Name = "label2" + $System_Drawing_Size = New-Object System.Drawing.Size + $System_Drawing_Size.Height = 30 + $System_Drawing_Size.Width = 118 + $label2.Size = $System_Drawing_Size + $label2.TabIndex = 1 + $label2.Text = "VHD Format" + + $groupBox3.Controls.Add($label2) + #endregion label2 + + #region groupBox2 + $groupBox2.DataBindings.DefaultDataSourceUpdateMode = 0 + $System_Drawing_Point = New-Object System.Drawing.Point + $System_Drawing_Point.X = 10 + $System_Drawing_Point.Y = 169 + $groupBox2.Location = $System_Drawing_Point + $groupBox2.Name = "groupBox2" + $System_Drawing_Size = New-Object System.Drawing.Size + $System_Drawing_Size.Height = 68 + $System_Drawing_Size.Width = 490 + $groupBox2.Size = $System_Drawing_Size + $groupBox2.TabIndex = 6 + $groupBox2.TabStop = $False + $groupBox2.Text = "2. Choose a SKU from the list" + + $frmMain.Controls.Add($groupBox2) + #endregion groupBox2 + + #region cmbSkuList + $cmbSkuList.DataBindings.DefaultDataSourceUpdateMode = 0 + $cmbSkuList.FormattingEnabled = $True + $System_Drawing_Point = New-Object System.Drawing.Point + $System_Drawing_Point.X = 25 + $System_Drawing_Point.Y = 24 + $cmbSkuList.Location = $System_Drawing_Point + $cmbSkuList.Name = "cmbSkuList" + $System_Drawing_Size = New-Object System.Drawing.Size + $System_Drawing_Size.Height = 25 + $System_Drawing_Size.Width = 452 + $cmbSkuList.Size = $System_Drawing_Size + $cmbSkuList.TabIndex = 2 + + $groupBox2.Controls.Add($cmbSkuList) + #endregion cmbSkuList + + #region label1 + $label1.DataBindings.DefaultDataSourceUpdateMode = 0 + $System_Drawing_Point = New-Object System.Drawing.Point + $System_Drawing_Point.X = 23 + $System_Drawing_Point.Y = 21 + $label1.Location = $System_Drawing_Point + $label1.Name = "label1" + $System_Drawing_Size = New-Object System.Drawing.Size + $System_Drawing_Size.Height = 71 + $System_Drawing_Size.Width = 464 + $label1.Size = $System_Drawing_Size + $label1.TabIndex = 5 + $label1.Text = $uiHeader + + $frmMain.Controls.Add($label1) + #endregion label1 + + #region groupBox1 + $groupBox1.DataBindings.DefaultDataSourceUpdateMode = 0 + $System_Drawing_Point = New-Object System.Drawing.Point + $System_Drawing_Point.X = 10 + $System_Drawing_Point.Y = 95 + $groupBox1.Location = $System_Drawing_Point + $groupBox1.Name = "groupBox1" + $System_Drawing_Size = New-Object System.Drawing.Size + $System_Drawing_Size.Height = 68 + $System_Drawing_Size.Width = 490 + $groupBox1.Size = $System_Drawing_Size + $groupBox1.TabIndex = 4 + $groupBox1.TabStop = $False + $groupBox1.Text = "1. Choose a source" + + $frmMain.Controls.Add($groupBox1) + #endregion groupBox1 + + #region txtSourcePath + $txtSourcePath.DataBindings.DefaultDataSourceUpdateMode = 0 + $System_Drawing_Point = New-Object System.Drawing.Point + $System_Drawing_Point.X = 25 + $System_Drawing_Point.Y = 24 + $txtSourcePath.Location = $System_Drawing_Point + $txtSourcePath.Name = "txtSourcePath" + $System_Drawing_Size = New-Object System.Drawing.Size + $System_Drawing_Size.Height = 25 + $System_Drawing_Size.Width = 418 + $txtSourcePath.Size = $System_Drawing_Size + $txtSourcePath.TabIndex = 0 + + $groupBox1.Controls.Add($txtSourcePath) + #endregion txtSourcePath + + #region btnBrowseWim + $btnBrowseWim.DataBindings.DefaultDataSourceUpdateMode = 0 + $System_Drawing_Point = New-Object System.Drawing.Point + $System_Drawing_Point.X = 449 + $System_Drawing_Point.Y = 24 + $btnBrowseWim.Location = $System_Drawing_Point + $btnBrowseWim.Name = "btnBrowseWim" + $System_Drawing_Size = New-Object System.Drawing.Size + $System_Drawing_Size.Height = 25 + $System_Drawing_Size.Width = 28 + $btnBrowseWim.Size = $System_Drawing_Size + $btnBrowseWim.TabIndex = 1 + $btnBrowseWim.Text = "..." + $btnBrowseWim.UseVisualStyleBackColor = $True + $btnBrowseWim.add_Click($btnBrowseWim_OnClick) + + $groupBox1.Controls.Add($btnBrowseWim) + #endregion btnBrowseWim + + $openFileDialog1.FileName = "openFileDialog1" + $openFileDialog1.ShowHelp = $True + + #endregion Form Code + + # Save the initial state of the form + $InitialFormWindowState = $frmMain.WindowState + + # Init the OnLoad event to correct the initial state of the form + $frmMain.add_Load($OnLoadForm_StateCorrection) + + # Return the constructed form. + $ret = $frmMain.ShowDialog() + + if (!($ret -ilike "OK")) { + throw "Form session has been cancelled." + } + + if ([string]::IsNullOrEmpty($SourcePath)) { + throw "No source path specified." + } + + # VHD Format + $VhdFormat = $cmbVhdFormat.SelectedItem + + # VHD Size + $SizeBytes = Invoke-Expression "$($numVhdSize.Value)$($cmbVhdSizeUnit.SelectedItem)" + + # Working Directory + $WorkingDirectory = $txtWorkingDirectory.Text + + # VHDPath + if (![string]::IsNullOrEmpty($txtVhdName.Text)) { + $VhdPath = "$($WorkingDirectory)\$($txtVhdName.Text)" + } + + # Edition + if (![string]::IsNullOrEmpty($cmbSkuList.SelectedItem)) { + $Edition = $cmbSkuList.SelectedItem + } + + # Because we used ShowDialog, we need to manually dispose of the form. + # This probably won't make much of a difference, but let's free up all of the resources we can + # before we start the conversion process. + + $frmMain.Dispose() + } + + If ($VhdFormat -ilike "AUTO") { + If ($DiskLayout -eq "Bios") { + $VhdFormat = "VHD" + } + Else { + $VhdFormat = "VHDX" + } + } + + # Choose smallest supported block size for dynamic VHD(X) + $BlockSizeBytes = 1MB + + # There's a difference between the maximum sizes for VHDs and VHDXs. Make sure we follow it. + If ("VHD" -ilike $VhdFormat) { + If ($SizeBytes -gt $vhdMaxSize) { + Write-Warning -Message "For the VHD file format, the maximum file size is ~2040GB. We're automatically setting the size to 2040GB for you." + $SizeBytes = 2040GB + } + $BlockSizeBytes = 512KB + } + + # Check if -VHDPath and -WorkingDirectory were both specified. + If (-Not [String]::IsNullOrEmpty( $VhdPath ) -And + -Not [String]::IsNullOrEmpty( $WorkingDirectory )) { + If ($WorkingDirectory -ne $pwd) { + + # If the WorkingDirectory is anything besides $pwd, tell people that the WorkingDirectory is being ignored. + Write-Warning -Message "Specifying -VHDPath and -WorkingDirectory at the same time is contradictory." + Write-Warning -Message "Ignoring the WorkingDirectory specification." + $WorkingDirectory = Split-Path $VhdPath -Parent + } + } + + If ($VhdPath) { + + # Check to see if there's a conflict between the specified file extension and the VhdFormat being used. + $Ext = ( [IO.FileInfo]$VhdPath ).Extension + + If (-Not ( $Ext -ilike ".$( $VhdFormat )" )) { + Throw "There is a mismatch between the VHDPath file extension ($($ext.ToUpper())), and the VhdFormat (.$($VhdFormat)). Please ensure that these match and try again." + } + } + + # Create a temporary name for the VHD(x). We'll name it properly at the end of the script. + If ([String]::IsNullOrEmpty( $VhdPath )) { + $vhdNameTemp = [system.string]( $sessionKey + "." + $VhdFormat.ToLower() ) + $VhdPath = Join-Path -Path $WorkingDirectory -ChildPath $vhdNameTemp + } + + Else { + + # Since we can't do Resolve-Path against a file that doesn't exist, we need to get creative in determining + # the full path that the user specified (or meant to specify if they gave us a relative path). + # Check to see if the path has a root specified. If it doesn't, use the working directory. + + If (-Not [IO.Path]::IsPathRooted( $VhdPath )) { + $VhdPath = Join-Path -Path $WorkingDirectory -ChildPath $VhdPath + } + + $vhdFinalName = Split-Path -Path $VhdPath -Leaf + $VhdPath = Split-Path -Path $VhdPath -Parent + $vhdNameTemp = [system.string]( $sessionKey + "." + $VhdFormat.ToLower() ) + $VhdPath = Join-Path -Path $VhdPath -ChildPath $vhdNameTemp + } + + Write-Debug -Message " Temporary $VhdFormat path is: `"$VhdPath`"" + + #endregion Prepare variables + + #region Mount source images + + # If we're using an ISO, mount it and get the path to the WIM file. + If (( [IO.FileInfo]$SourcePath ).Extension -ilike ".ISO") { + + # If the ISO isn't local, copy it down so we don't have to worry about resource contention + # or about network latency. + If (Test-IsNetworkLocation -Path $SourcePath) { + $IsoFileName = Split-Path -Path $SourcePath -Leaf + + Write-Verbose -Message "Copying ISO `"$IsoFileName`" to temp folder..." + + # Robocopy.exe $(Split-Path $SourcePath -Parent) $TempDirectory $(Split-Path $SourcePath -Leaf) | Out-Null + $Item = Copy-Item -Path $SourcePath -Destination $TempDirectory -PassThru + + $SourcePath = Join-Path -Path $TempDirectory -ChildPath $IsoFileName + + $tempSource = $SourcePath + } + + $IsoPath = ( Resolve-Path $SourcePath ).Path + $IsoFileName = Split-Path $IsoPath -Leaf + + Write-Verbose -Message "Opening ISO `"$IsoFileName`"..." + $openIso = Mount-DiskImage -ImagePath $IsoPath -StorageType "ISO" -PassThru + + # Refresh the DiskImage and Drive object so we can get the real information about it. I assume this is a bug. + Get-PSDrive -PSProvider FileSystem | Out-Null + $openIso = Get-DiskImage -ImagePath $IsoPath + $driveLetter = ( Get-Volume -DiskImage $openIso ).DriveLetter + + $SourcePath = "$($driveLetter):\sources\install.wim" + + # Check to see if there's a WIM file we can muck about with. + Write-Verbose -Message "Looking for `"$SourcePath`"..." + + If (-Not ( Test-Path -Path $SourcePath )) { + Throw "The specified ISO does not appear to be valid Windows installation media." + } + } + + # Check to see if the WIM is local, or on a network location. If the latter, copy it locally. + If (Test-IsNetworkLocation -Path $SourcePath) { + $WimFileName = Split-Path -Path $SourcePath -Leaf + + Write-Verbose -Message "Copying WIM $WimFileName to temp folder..." + + # robocopy $(Split-Path $SourcePath -Parent) $TempDirectory $(Split-Path $SourcePath -Leaf) | Out-Null + $Item = Copy-Item -Path $SourcePath -Destination $TempDirectory -PassThru + + $SourcePath = Join-Path -Path $TempDirectory -ChildPath $WimFileName + + $tempSource = $SourcePath + } + + $SourcePath = ( Resolve-Path $SourcePath ).Path + + # QUERY WIM INFORMATION AND EXTRACT THE INDEX OF TARGETED IMAGE + + Write-Verbose -Message "Looking for the requested Windows image in the WIM file" + + $WindowsImage = Get-WindowsImage -ImagePath $SourcePath -Verbose:$False + + # We're good. Open the WIM container. + + # NOTE: this is only required because we want to get the XML-based meta-data at the end. Is there a better way? + # If we can get this information from DISM cmdlets, we can remove the openWim constructs + + $openWim = New-Object -TypeName "WIM2VHD.WimFile" -ArgumentList $SourcePath + + #endregion Mount source images + + $Edition | ForEach-Object -Process { + + #region Select Image + + $Edition = $PSItem + + # WIM may have multiple images. Filter on Edition (can be index or name) and try to find a unique image + + Write-Verbose -Message ( [system.string]::Empty ) + + $EditionIndex = 0; + + If ([Int32]::TryParse( $Edition, [ref]$EditionIndex )) { + $WindowsImage = Get-WindowsImage -ImagePath $SourcePath -Index $EditionIndex -Verbose:$False + } + Else { + $WindowsImage = Get-WindowsImage -ImagePath $SourcePath -Verbose:$False | Where-Object { $PSItem.ImageName -ilike "*$($Edition)" } + } + + If (-Not $WindowsImage) { + Throw "Requested windows Image was not found on the WIM file!" + } + + If ($WindowsImage -is [System.Array]) { + $ImageCount = $($WindowsImage.Count) + + Write-Verbose -Message "WIM file has the following $ImageCount images that match filter *$Edition" + Get-WindowsImage -ImagePath $SourcePath -Verbose:$False + + Write-Error -Message "You must specify an Edition or SKU index, since the WIM has more than one image." + Throw "There are more than one images that match ImageName filter *$Edition" + } + + $ImageIndex = $WindowsImage[0].ImageIndex + + $openImage = $openWim[[Int32]$ImageIndex] + + If ($Null -eq $openImage) { + Write-Error -Message "The specified edition does not appear to exist in the specified WIM." + Write-Error -Message "Valid edition names are:" + + $openWim.Images | ForEach-Object -Process { Write-Error -Message "$PSItem.ImageFlags" } + Throw + } + + $OpenImageIndex = $openImage.ImageIndex + $OpenImageFlags = $openImage.ImageFlags + $OpenImageVersion = $openImage.ImageVersion + + Write-Verbose -Message "Image $OpenImageIndex selected: `"$OpenImageFlags`"..." + + # Check to make sure that the image we're applying is Windows 7 or greater. + If ($OpenImageVersion -lt $lowestSupportedVersion) { + If ($OpenImageVersion -eq "0.0.0.0") { + Write-Warning -Message "The specified WIM does not encode the Windows version." + } + Else { + Throw "Convert-WindowsImage only supports Windows 7 and Windows 8 WIM files. The specified image (version $OpenImageVersion) does not appear to contain one of those operating systems." + } + } + + #endregion Select Image + + #region Create and partition VHD + + If ($hyperVEnabled) { + Write-Verbose -Message "Creating sparse disk..." + + $NewVhdParam = @{ + + Path = $VhdPath + SizeBytes = $SizeBytes + BlockSizeBytes = $BlockSizeBytes + } + + Switch ($VhdType) { + "Dynamic" { + $NewVhdParam.Add( + "Dynamic", $True + ) + } + + "Fixed" + {} + } + + $newVhd = New-VHD @NewVhdParam + + Write-Verbose -Message "Mounting $VhdFormat..." + $disk = $newVhd | Mount-VHD -Passthru | Get-Disk + } + Else { + <# + Create the VHD using the VirtDisk Win32 API. + So, why not use the New-VHD cmdlet here? + + New-VHD depends on the Hyper-V Cmdlets, which aren't installed by default. + Installing those cmdlets isn't a big deal, but they depend on the Hyper-V WMI + APIs, which in turn depend on Hyper-V. In order to prevent Convert-WindowsImage + from being dependent on Hyper-V (and thus, x64 systems only), we're using the + VirtDisk APIs directly. + #> + + Switch ($VhdType) { + "Dynamic" { + Write-Verbose -Message "Creating sparse disk..." + + [WIM2VHD.VirtualHardDisk]::CreateSparseDisk( + $VhdFormat, + $VhdPath, + $SizeBytes, + $true + ) + } + + "Fixed" { + Write-Verbose -Message "Creating fixed disk..." + + [WIM2VHD.VirtualHardDisk]::CreateFixedDisk( + $VhdFormat, + $VhdPath, + $SizeBytes, + $true + ) + } + } + + # Attach the VHD. + + Write-Verbose -Message "Attaching $VhdFormat..." + $disk = Mount-DiskImage -ImagePath $VhdPath -PassThru | Get-DiskImage | Get-Disk + } + + Switch ($DiskLayout) { + "BIOS" { + Write-Verbose -Message "Initializing disk..." + Initialize-Disk -Number $disk.Number -PartitionStyle MBR + + # Create the Windows/system partition + Write-Verbose -Message "Creating single partition..." + + $systemPartition = New-Partition -DiskNumber $disk.Number -UseMaximumSize -MbrType IFS -IsActive + $windowsPartition = $systemPartition + + Write-Verbose -Message "Formatting windows volume..." + + $systemVolume = Format-Volume -Partition $systemPartition -FileSystem NTFS -Force -Confirm:$false + $windowsVolume = $systemVolume + } + + "UEFI" { + Write-Verbose -Message "Initializing disk..." + Initialize-Disk -Number $disk.Number -PartitionStyle GPT + + If ($BcdInVhd -eq "VirtualMachine") { + If (( Get-WindowsBuildNumber ) -ge 10240) { + + # Create the system partition. Create a data partition so we can format it, then change to ESP + # Size should be at least 260 MB to accomodate for Native 4K drives. + Write-Verbose -Message "Creating EFI System partition (ESP)..." + $systemPartition = New-Partition -DiskNumber $disk.Number -Size 260MB -GptType '{ebd0a0a2-b9e5-4433-87c0-68b6b72699c7}' + + Write-Verbose -Message " Formatting System volume..." + $systemVolume = Format-Volume -Partition $systemPartition -FileSystem FAT32 -Force -Confirm:$false + + Write-Verbose -Message " Setting partition type to ESP..." + $systemPartition | Set-Partition -GptType '{c12a7328-f81f-11d2-ba4b-00a0c93ec93b}' + $systemPartition | Add-PartitionAccessPath -AssignDriveLetter + } + else { + + # Create the system partition + Write-Verbose -Message "Creating EFI system partition (ESP)..." + $systemPartition = New-Partition -DiskNumber $disk.Number -Size 260MB -GptType '{c12a7328-f81f-11d2-ba4b-00a0c93ec93b}' -AssignDriveLetter + + Write-Verbose -Message " Formatting ESP..." + $formatArgs = @( + "$($systemPartition.DriveLetter):", # Partition drive letter + "/FS:FAT32", # File system + "/Q", # Quick format + "/Y" # Suppress prompt + ) + + Start-Executable -Executable format -Arguments $formatArgs + } + } + Else { + Write-Verbose -Message "The disk is intended for Native Boot. There will be no EFI System partition (ESP)." + } + + # Create the Windows partition + Write-Verbose -Message "Creating Boot (`"Windows`") partition..." + $windowsPartition = New-Partition -DiskNumber $disk.Number -UseMaximumSize -GptType '{ebd0a0a2-b9e5-4433-87c0-68b6b72699c7}' + + Write-Verbose -Message " Formatting Boot (`"Windows`") volume..." + $windowsVolume = Format-Volume -Partition $windowsPartition -FileSystem NTFS -Force -Confirm:$false + } + + "WindowsToGo" { + Write-Verbose -Message "Initializing disk..." + Initialize-Disk -Number $disk.Number -PartitionStyle MBR + + # Create the system partition + Write-Verbose -Message "Creating system partition..." + $systemPartition = New-Partition -DiskNumber $disk.Number -Size 260MB -MbrType FAT32 -IsActive + + Write-Verbose -Message " Formatting system volume..." + $systemVolume = Format-Volume -Partition $systemPartition -FileSystem FAT32 -Force -Confirm:$false + + # Create the Windows partition + Write-Verbose -Message "Creating windows partition..." + $windowsPartition = New-Partition -DiskNumber $disk.Number -UseMaximumSize -MbrType IFS + + Write-Verbose -Message " Formatting windows volume..." + $windowsVolume = Format-Volume -Partition $windowsPartition -FileSystem NTFS -Force -Confirm:$false + } + } + + If (( $DiskLayout -eq "UEFI" -and $BcdInVhd -eq "VirtualMachine" ) -or + ( $DiskLayout -eq "WindowsToGo") -or + ( $DiskLayout -eq "BIOS")) { + # Retreive access path for System partition. + $systemPartition = Get-Partition -UniqueId $systemPartition.UniqueId + $systemDrive = $systemPartition.AccessPaths[0].trimend("\").replace("\?", "??") + Write-Verbose -Message "System volume path: `"$systemDrive`"" + } + + # Assign drive letter to Boot partition. This is required for bcdboot + $attempts = 1 + $assigned = $false + + Do { + $windowsPartition | Add-PartitionAccessPath -AssignDriveLetter + $windowsPartition = $windowsPartition | Get-Partition + + If ($windowsPartition.DriveLetter -ne 0) { + $assigned = $true + } + else { + + # sleep for up to 10 seconds and retry + Get-Random -Minimum 1 -Maximum 10 | Start-Sleep + + $attempts++ + } + } + While ($attempts -le 100 -and -not $assigned) + + If (-Not $assigned) { + Throw "Unable to get Partition after retry" + } + + $windowsDrive = ( Get-Partition -Volume $windowsVolume ).AccessPaths[0].substring(0, 2) + + # This is to workaround "No such drive exists" error in PowerShell + $Drive = Get-PSDrive + + $windowsDrive = ( Resolve-Path -Path $windowsDrive ).Path + + Write-Verbose -Message "Boot volume path: `"$windowsDrive`". (Took $attempts attempt(s) to assign.)" + + #endregion Create and partition VHD + + #region APPLY IMAGE FROM WIM TO THE NEW VHD + + Write-Verbose -Message "Applying image to $VhdFormat. This could take a while..." + + If (( Get-Command -Name "Expand-WindowsImage" -ErrorAction "SilentlyContinue" ) -and + ( -not $ApplyEA -and [string]::IsNullOrEmpty( $DismPath ) )) { + Expand-WindowsImage -ApplyPath $windowsDrive -ImagePath $SourcePath -Index $ImageIndex -LogPath "$($logFolder)\DismLogs.log" -Verbose:$False | Out-Null + } + Else { + If ([string]::IsNullOrEmpty( $DismPath )) { + $DismPath = Join-Path -Path $env:windir -ChildPath "system32\dism.exe" + } + + $applyImage = "/Apply-Image" + + If ($ApplyEA) { + $applyImage = $applyImage + " /EA" + } + + $dismArgs = @("$applyImage /ImageFile:`"$SourcePath`" /Index:$ImageIndex /ApplyDir:$windowsDrive /LogPath:`"$($logFolder)\DismLogs.log`"") + + Write-Verbose -Message "Applying image: $dismPath $dismArgs" + + $process = Start-Process -PassThru -Wait -NoNewWindow -FilePath $dismPath -ArgumentList $dismArgs + + If ($process.ExitCode -ne 0) { + Throw "Image Apply failed! See DismImageApply logs for details" + } + } + Write-Verbose -Message "Image was applied successfully. " + + # Here we copy in the unattend file (if specified by the command line) + + If (-Not [string]::IsNullOrEmpty( $UnattendPath )) { + Write-Verbose -Message "Applying unattend file ($(Split-Path $UnattendPath -Leaf))..." + + $UnattendDestination = Join-Path -Path $windowsDrive -ChildPath "unattend.xml" + Copy-Item -Path $UnattendPath -Destination $UnattendDestination -Force + } + + # Added to handle merge folders + + If (-Not [string]::IsNullOrEmpty( $MergeFolderPath )) { + Write-Verbose -Message "Applying merge folder ($MergeFolderPath)..." + + $MergeSourcePath = Join-Path -Path $MergeFolderPath -ChildPath "*" + + Copy-Item -Recurse -Path $MergeFolderPath -Destination $windowsDrive -Force + } + + #endregion APPLY IMAGE FROM WIM TO THE NEW VHD + + #region BCD manipulation + + If (( $openImage.ImageArchitecture -ne "ARM" ) -and # No virtualization platform for ARM images + ( $BcdInVhd -ne "NativeBoot" )) { # User asked for a non-bootable image + If (Test-Path -Path "$($systemDrive)\boot\bcd") { + Write-Verbose -Message "Image already has BIOS BCD store..." + } + ElseIf (Test-Path -Path "$($systemDrive)\efi\microsoft\boot\bcd") { + Write-Verbose -Message "Image already has EFI BCD store..." + } + Else { + Write-Verbose -Message "Making image bootable..." + + $WindowsPath = Join-Path -Path $windowsDrive -ChildPath "Windows" + + $bcdBootArgs = @( + + "$WindowsPath", # 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. + ) + + $bcdPath = @() + + Switch ($DiskLayout) { + "BIOS" { + $bcdBootArgs += "/f BIOS" # Specifies the firmware type of the target system partition + $bcdPath += Join-Path -Path $systemDrive -ChildPath "boot\bcd" + } + + "UEFI" { + $bcdBootArgs += "/f UEFI" # Specifies the firmware type of the target system partition + $bcdPath += Join-Path -Path $systemDrive -ChildPath "efi\microsoft\boot\bcd" + } + + "WindowsToGo" { + + # Create entries for both UEFI and BIOS if possible + + If (Test-Path -Path "$($windowsDrive)\Windows\boot\EFI\bootmgfw.efi") { + $bcdBootArgs += "/f ALL" + $bcdPath += Join-Path -Path $systemDrive -ChildPath "boot\bcd" + $bcdPath += Join-Path -Path $systemDrive -ChildPath "efi\microsoft\boot\bcd" + } + Else { + $bcdBootArgs += "/f BIOS" # Specifies the firmware type of the target system partition + $bcdPath += Join-Path -Path $systemDrive -ChildPath "boot\bcd" + } + } + } + + If (Test-Path -Path "Variable:\BcdLocale") { + $bcdBootArgs += "/l $BcdLocale.Name" + } + + Start-Executable -Executable $BCDBoot -Arguments $bcdBootArgs + + # The following is added to mitigate the VMM diff disk handling + # We're going to change from MBRBootOption to LocateBootOption. + + Write-Verbose -Message "Fixing the Device ID in the BCD store on $($VhdFormat)..." + + $bcdPath | ForEach-Object -Process { + + Start-Executable -Executable "BCDEDIT.EXE" -Arguments ( + "/store $PSItem", + "/set `{bootmgr`} device locate" + ) + Start-Executable -Executable "BCDEDIT.EXE" -Arguments ( + "/store $PSItem", + "/set `{default`} device locate" + ) + Start-Executable -Executable "BCDEDIT.EXE" -Arguments ( + "/store $PSItem", + "/set `{default`} osdevice locate" + ) + } + } + + Write-Verbose -Message "Drive is bootable." + + # Are we turning the debugger on? + If ($EnableDebugger -inotlike "None") { + $bcdEditArgs = $null; + + # Configure the specified debugging transport and other settings. + Switch ($EnableDebugger) { + "Serial" { + $bcdEditArgs = @( + + "/dbgsettings SERIAL", + "DEBUGPORT:$($ComPort.Value)", + "BAUDRATE:$($BaudRate.Value)" + ) + } + + "1394" { + $bcdEditArgs = @( + + "/dbgsettings 1394", + "CHANNEL:$($Channel.Value)" + ) + } + + "USB" { + $bcdEditArgs = @( + + "/dbgsettings USB", + "TARGETNAME:$($Target.Value)" + ) + } + + "Local" { + $bcdEditArgs = @( + + "/dbgsettings LOCAL" + ) + } + + "Network" { + $bcdEditArgs = @( + + "/dbgsettings NET", + "HOSTIP:$($IP.Value)", + "PORT:$($Port.Value)", + "KEY:$($Key.Value)" + ) + } + } + + $bcdStores = @( + + "$($systemDrive)\boot\bcd", + "$($systemDrive)\efi\microsoft\boot\bcd" + ) + + Foreach ($bcdStore In $bcdStores) { + If (Test-Path -Path $bcdStore) { + Write-Verbose -Message "Turning kernel debugging on in the $VhdFormat for $bcdStore..." + + Start-Executable -Executable "BCDEDIT.EXE" -Arguments ( + + "/store $($bcdStore)", + "/set `{default`} debug on" + ) + + $bcdEditArguments = @("/store $($bcdStore)") + $bcdEditArgs + + Start-Executable -Executable "BCDEDIT.EXE" -Arguments $bcdEditArguments + } + } + } + } + + Else { + + # Don't bother to check on debugging. We can't boot WoA VHDs in VMs, and + # if we're native booting, the changes need to be made to the BCD store on the + # physical computer's boot volume. + + Write-Verbose -Message "Image applied. It is not bootable." + } + + #endregion BCD manipulation + + #region Additional image enhancements + + If ($RemoteDesktopEnable -or -not $ExpandOnNativeBoot) { + $hivePath = Join-Path -Path $windowsDrive -ChildPath "Windows\System32\Config\System" + + $hive = Mount-RegistryHive -Hive $hivePath + + If ($RemoteDesktopEnable) { + Write-Verbose -Message "Enabling Remote Desktop" + + Set-ItemProperty -Path "HKLM:\$($hive)\ControlSet001\Control\Terminal Server" -Name "fDenyTSConnections" -Value 0 + } + + If (-not $ExpandOnNativeBoot) { + Write-Verbose -Message "Disabling automatic $VhdFormat expansion for Native Boot" + + Set-ItemProperty -Path "HKLM:\$($hive)\ControlSet001\Services\FsDepends\Parameters" -Name "VirtualDiskExpandOnMount" -Value 4 + } + + Dismount-RegistryHive -HiveMountPoint $hive + } + + If ($Driver) { + Write-Verbose -Message "Adding Windows Drivers to the Image" + + $Driver | ForEach-Object -Process { + + Write-Verbose -Message "Driver path: $PSItem" + Add-WindowsDriver -Path $windowsDrive -Recurse -Driver $PSItem -Verbose:$False | Out-Null + } + } + + If ($Feature) { + Write-Verbose -Message "Installing Windows Feature(s) $Feature to the Image" + + $FeatureSourcePath = Join-Path -Path "$($driveLetter):" -ChildPath "sources\sxs" + Write-Verbose -Message "From $FeatureSourcePath" + Enable-WindowsOptionalFeature -FeatureName $Feature -Source $FeatureSourcePath -Path $windowsDrive -All -Verbose:$False | Out-Null + } + + If ($Package) { + Write-Verbose -Message "Adding Windows Packages to the Image" + + $Package | ForEach-Object -Process { + + Write-Verbose -Message "Package path: $PSItem" + Add-WindowsPackage -Path $windowsDrive -PackagePath $PSItem -Verbose:$False | Out-Null + } + } + + #endregion Additional image enhancements + + #region Dispose paths and dismount VHD + + # Remove system partition access path, if necessary + If ($DiskLayout -eq "UEFI" -and $BcdInVhd -eq "VirtualMachine") { + Remove-PartitionAccessPath -InputObject $systemPartition -AccessPath $systemPartition.AccessPaths[0] -PassThru | Out-Null + } + + If ([String]::IsNullOrEmpty( $vhdFinalName )) { + + # We need to generate a file name. + + Write-Verbose -Message "Generating name for $VhdFormat..." + + $HivePath = Join-Path -Path $windowsDrive -ChildPath "Windows\System32\Config\Software" + + $hive = Mount-RegistryHive -Hive $hivePath + + $buildLabEx = ( Get-ItemProperty "HKLM:\$($hive)\Microsoft\Windows NT\CurrentVersion" ).BuildLabEx + $installType = ( Get-ItemProperty "HKLM:\$($hive)\Microsoft\Windows NT\CurrentVersion" ).InstallationType + $editionId = ( Get-ItemProperty "HKLM:\$($hive)\Microsoft\Windows NT\CurrentVersion" ).EditionID + $skuFamily = $null + + Dismount-RegistryHive -HiveMountPoint $hive + + # Is this ServerCore? + # Since we're only doing this string comparison against the InstallType key, we won't get + # false positives with the Core SKU. + + If ($installType.ToUpper().Contains("CORE")) { + $editionId += "Core" + } + + # What type of SKU are we? + + If ($installType.ToUpper().Contains("SERVER")) { + $skuFamily = "Server" + } + ElseIf ($installType.ToUpper().Contains("CLIENT")) { + $skuFamily = "Client" + } + Else { + $skuFamily = "Unknown" + } + + # ISSUE - do we want VL here? + + $vhdFinalName = "$($buildLabEx)_$($skuFamily)_$($editionId)_$($openImage.ImageDefaultLanguage).$($VhdFormat.ToLower())" + Write-Debug -Message " $VhdFormat final name is: `"$vhdFinalName`"" + } + + If ($hyperVEnabled) { + Write-Verbose -Message "Dismounting $VhdFormat..." + Dismount-VHD -Path $VhdPath + } + Else { + Write-Verbose -Message "Closing $VhdFormat..." + If ((Get-Command Dismount-DiskImage).parameters.Keys.Contains("PassThru")) { + $DismountDiskImage = Dismount-DiskImage -ImagePath $VhdPath -PassThru + } + Else { + $DismountDiskImage = Dismount-DiskImage -ImagePath $VhdPath + } + } + + $vhdParentPath = Split-Path -Path $VhdPath -Parent + $vhdFinalPath = Join-Path -Path $vhdParentPath -ChildPath $vhdFinalName + + Write-Debug -Message " $VhdFormat final path is: `"$vhdFinalPath`"" + + If (Test-Path -Path $vhdFinalPath) { + $VhdNameOld = Split-Path -Path $vhdFinalPath -Leaf + + Write-Verbose -Message "Deleting pre-existing $($VhdFormat): `"$VhdNameOld`"..." + + Remove-Item -Path $vhdFinalPath -Force + } + + Write-Debug -Message " Renaming $VhdFormat at `"$VhdPath`"." + + $VhdPathFull = ( Resolve-Path -Path $VhdPath ).Path + + $RenameItem = Rename-Item -Path $VhdPathFull -NewName $vhdFinalName -Force -PassThru + + $vhd += Get-DiskImage -ImagePath $vhdFinalPath + + $vhdFinalName = $null + + #endregion Dispose paths and dismount images + } + } + Catch { + Write-Verbose -Message ( [system.string]::Empty ) + Write-Error -Message $PSItem + Write-Verbose -Message "Log folder is `"$logFolder`"" + } + Finally { + Write-Verbose -Message ( [system.string]::Empty ) + + # If we still have a WIM image open, close it. + If ($openWim -ne $null) { + Write-Verbose -Message "Closing Windows image..." + + $openWim.Close() + } + + # If we still have a registry hive mounted, dismount it. + If ($mountedHive -ne $null) { + Write-Verbose -Message "Closing registry hive..." + + Dismount-RegistryHive -HiveMountPoint $mountedHive + } + + # If VHD is mounted, unmount it + If (Test-Path -Path $VhdPath) { + If ($hyperVEnabled) { + If (( Get-VHD -Path $VhdPath ).Attached) { + Dismount-VHD -Path $VhdPath + } + } + Else { + If ((Get-Command Dismount-DiskImage).parameters.Keys.Contains("PassThru")) { + $DismountDiskImage = Dismount-DiskImage -ImagePath $VhdPath -PassThru + } + Else { + $DismountDiskImage = Dismount-DiskImage -ImagePath $VhdPath + } + } + } + + # If we still have an ISO open, close it. + If ($openIso -ne $Null) { + Write-Verbose -Message "Closing ISO..." + If ((Get-Command Dismount-DiskImage).parameters.Keys.Contains("PassThru")) { + $DismountDiskImage = Dismount-DiskImage -ImagePath $IsoPath -PassThru + } + Else { + $DismountDiskImage = Dismount-DiskImage -ImagePath $IsoPath + } + } + + If (-not $CacheSource) { + If ($tempSource -and ( Test-Path -Path $tempSource )) { + $Item = Remove-Item -Path $tempSource -Force + } + } + + # Close out the transcript and tell the user we're done. + Write-Verbose -Message "Done." + + If ($transcripting) { + $Null = Stop-Transcript + } + } + } + End { + + If ($Passthru) { + Return $vhd + } + } + + #endregion Code +} + +# Helper functions. Not intended to be called outside of Convert-WindowsImage. + +Function +Add-WindowsImageTypes { + $Code = @" + +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.ComponentModel; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Runtime.InteropServices; +using System.Security; +using System.Text; +using System.Text.RegularExpressions; +using System.Threading; +using System.Xml.Linq; +using System.Xml.XPath; +using Microsoft.Win32.SafeHandles; + +namespace WIM2VHD +{ + +/// +/// P/Invoke methods and associated enums, flags, and structs. +/// +public class +NativeMethods +{ + + #region Delegates and Callbacks + #region WIMGAPI + + /// + ///User-defined function used with the RegisterMessageCallback or UnregisterMessageCallback function. + /// + ///Specifies the message being sent. + ///Specifies additional message information. The contents of this parameter depend on the value of the + ///MessageId parameter. + ///Specifies additional message information. The contents of this parameter depend on the value of the + ///MessageId parameter. + ///Specifies the user-defined value passed to RegisterCallback. + /// + ///To indicate success and to enable other subscribers to process the message return WIM_MSG_SUCCESS. + ///To prevent other subscribers from receiving the message, return WIM_MSG_DONE. + ///To cancel an image apply or capture, return WIM_MSG_ABORT_IMAGE when handling the WIM_MSG_PROCESS message. + /// + public delegate uint + WimMessageCallback( + uint MessageId, + IntPtr wParam, + IntPtr lParam, + IntPtr UserData + ); + + public static void + RegisterMessageCallback( + WimFileHandle hWim, + WimMessageCallback callback) + { + + uint _callback = NativeMethods.WimRegisterMessageCallback(hWim, callback, IntPtr.Zero); + int rc = Marshal.GetLastWin32Error(); + if (0 != rc) + { + // Throw an exception if something bad happened on the Win32 end. + throw + new InvalidOperationException( + string.Format( + CultureInfo.CurrentCulture, + "Unable to register message callback." + )); + } + } + + public static void + UnregisterMessageCallback( + WimFileHandle hWim, + WimMessageCallback registeredCallback) + { + + bool status = NativeMethods.WimUnregisterMessageCallback(hWim, registeredCallback); + int rc = Marshal.GetLastWin32Error(); + if (!status) + { + throw + new InvalidOperationException( + string.Format( + CultureInfo.CurrentCulture, + "Unable to unregister message callback." + )); + } + } + + #endregion WIMGAPI + #endregion Delegates and Callbacks + + #region Constants + + #region VDiskInterop + + /// + /// The default depth in a VHD parent chain that this library will search through. + /// If you want to go more than one disk deep into the parent chain, provide a different value. + /// + public const uint OPEN_VIRTUAL_DISK_RW_DEFAULT_DEPTH = 0x00000001; + + public const uint DEFAULT_BLOCK_SIZE = 0x00080000; + public const uint DISK_SECTOR_SIZE = 0x00000200; + + internal const uint ERROR_VIRTDISK_NOT_VIRTUAL_DISK = 0xC03A0015; + internal const uint ERROR_NOT_FOUND = 0x00000490; + internal const uint ERROR_IO_PENDING = 0x000003E5; + internal const uint ERROR_INSUFFICIENT_BUFFER = 0x0000007A; + internal const uint ERROR_ERROR_DEV_NOT_EXIST = 0x00000037; + internal const uint ERROR_BAD_COMMAND = 0x00000016; + internal const uint ERROR_SUCCESS = 0x00000000; + + public const uint GENERIC_READ = 0x80000000; + public const uint GENERIC_WRITE = 0x40000000; + public const short FILE_ATTRIBUTE_NORMAL = 0x00000080; + public const uint CREATE_NEW = 0x00000001; + public const uint CREATE_ALWAYS = 0x00000002; + public const uint OPEN_EXISTING = 0x00000003; + public const short INVALID_HANDLE_VALUE = -1; + + internal static Guid VirtualStorageTypeVendorUnknown = new Guid("00000000-0000-0000-0000-000000000000"); + internal static Guid VirtualStorageTypeVendorMicrosoft = new Guid("EC984AEC-A0F9-47e9-901F-71415A66345B"); + + #endregion VDiskInterop + + #region WIMGAPI + + public const uint WIM_FLAG_VERIFY = 0x00000002; + public const uint WIM_FLAG_INDEX = 0x00000004; + + public const uint WM_APP = 0x00008000; + + #endregion WIMGAPI + + #endregion Constants + + #region Enums and Flags + + #region VDiskInterop + + /// + /// Indicates the version of the virtual disk to create. + /// + public enum CreateVirtualDiskVersion : int + { + VersionUnspecified = 0x00000000, + Version1 = 0x00000001, + Version2 = 0x00000002 + } + + public enum OpenVirtualDiskVersion : int + { + VersionUnspecified = 0x00000000, + Version1 = 0x00000001, + Version2 = 0x00000002 + } + + /// + /// Contains the version of the virtual hard disk (VHD) ATTACH_VIRTUAL_DISK_PARAMETERS structure to use in calls to VHD functions. + /// + public enum AttachVirtualDiskVersion : int + { + VersionUnspecified = 0x00000000, + Version1 = 0x00000001, + Version2 = 0x00000002 + } + + public enum CompactVirtualDiskVersion : int + { + VersionUnspecified = 0x00000000, + Version1 = 0x00000001 + } + + /// + /// Contains the type and provider (vendor) of the virtual storage device. + /// + public enum VirtualStorageDeviceType : int + { + /// + /// The storage type is unknown or not valid. + /// + Unknown = 0x00000000, + /// + /// For internal use only. This type is not supported. + /// + ISO = 0x00000001, + /// + /// Virtual Hard Disk device type. + /// + VHD = 0x00000002, + /// + /// Virtual Hard Disk v2 device type. + /// + VHDX = 0x00000003 + } + + /// + /// Contains virtual hard disk (VHD) open request flags. + /// + [Flags] + public enum OpenVirtualDiskFlags + { + /// + /// No flags. Use system defaults. + /// + None = 0x00000000, + /// + /// Open the VHD file (backing store) without opening any differencing-chain parents. Used to correct broken parent links. + /// + NoParents = 0x00000001, + /// + /// Reserved. + /// + BlankFile = 0x00000002, + /// + /// Reserved. + /// + BootDrive = 0x00000004, + } + + /// + /// Contains the bit mask for specifying access rights to a virtual hard disk (VHD). + /// + [Flags] + public enum VirtualDiskAccessMask + { + /// + /// Only Version2 of OpenVirtualDisk API accepts this parameter + /// + None = 0x00000000, + /// + /// Open the virtual disk for read-only attach access. The caller must have READ access to the virtual disk image file. + /// + /// + /// If used in a request to open a virtual disk that is already open, the other handles must be limited to either + /// VIRTUAL_DISK_ACCESS_DETACH or VIRTUAL_DISK_ACCESS_GET_INFO access, otherwise the open request with this flag will fail. + /// + AttachReadOnly = 0x00010000, + /// + /// Open the virtual disk for read-write attaching access. The caller must have (READ | WRITE) access to the virtual disk image file. + /// + /// + /// If used in a request to open a virtual disk that is already open, the other handles must be limited to either + /// VIRTUAL_DISK_ACCESS_DETACH or VIRTUAL_DISK_ACCESS_GET_INFO access, otherwise the open request with this flag will fail. + /// If the virtual disk is part of a differencing chain, the disk for this request cannot be less than the readWriteDepth specified + /// during the prior open request for that differencing chain. + /// + AttachReadWrite = 0x00020000, + /// + /// Open the virtual disk to allow detaching of an attached virtual disk. The caller must have + /// (FILE_READ_ATTRIBUTES | FILE_READ_DATA) access to the virtual disk image file. + /// + Detach = 0x00040000, + /// + /// Information retrieval access to the virtual disk. The caller must have READ access to the virtual disk image file. + /// + GetInfo = 0x00080000, + /// + /// Virtual disk creation access. + /// + Create = 0x00100000, + /// + /// Open the virtual disk to perform offline meta-operations. The caller must have (READ | WRITE) access to the virtual + /// disk image file, up to readWriteDepth if working with a differencing chain. + /// + /// + /// If the virtual disk is part of a differencing chain, the backing store (host volume) is opened in RW exclusive mode up to readWriteDepth. + /// + MetaOperations = 0x00200000, + /// + /// Reserved. + /// + Read = 0x000D0000, + /// + /// Allows unrestricted access to the virtual disk. The caller must have unrestricted access rights to the virtual disk image file. + /// + All = 0x003F0000, + /// + /// Reserved. + /// + Writable = 0x00320000 + } + + /// + /// Contains virtual hard disk (VHD) creation flags. + /// + [Flags] + public enum CreateVirtualDiskFlags + { + /// + /// Contains virtual hard disk (VHD) creation flags. + /// + None = 0x00000000, + /// + /// Pre-allocate all physical space necessary for the size of the virtual disk. + /// + /// + /// The CREATE_VIRTUAL_DISK_FLAG_FULL_PHYSICAL_ALLOCATION flag is used for the creation of a fixed VHD. + /// + FullPhysicalAllocation = 0x00000001 + } + + /// + /// Contains virtual disk attach request flags. + /// + [Flags] + public enum AttachVirtualDiskFlags + { + /// + /// No flags. Use system defaults. + /// + None = 0x00000000, + /// + /// Attach the virtual disk as read-only. + /// + ReadOnly = 0x00000001, + /// + /// No drive letters are assigned to the disk's volumes. + /// + /// Oddly enough, this doesn't apply to NTFS mount points. + NoDriveLetter = 0x00000002, + /// + /// Will decouple the virtual disk lifetime from that of the VirtualDiskHandle. + /// The virtual disk will be attached until the Detach() function is called, even if all open handles to the virtual disk are closed. + /// + PermanentLifetime = 0x00000004, + /// + /// Reserved. + /// + NoLocalHost = 0x00000008 + } + + [Flags] + public enum DetachVirtualDiskFlag + { + None = 0x00000000 + } + + [Flags] + public enum CompactVirtualDiskFlags + { + None = 0x00000000, + NoZeroScan = 0x00000001, + NoBlockMoves = 0x00000002 + } + + #endregion VDiskInterop + + #region WIMGAPI + + [FlagsAttribute] + internal enum + WimCreateFileDesiredAccess : uint + { + WimQuery = 0x00000000, + WimGenericRead = 0x80000000 + } + + public enum WimMessage : uint + { + WIM_MSG = WM_APP + 0x1476, + WIM_MSG_TEXT, + /// + ///Indicates an update in the progress of an image application. + /// + WIM_MSG_PROGRESS, + /// + ///Enables the caller to prevent a file or a directory from being captured or applied. + /// + WIM_MSG_PROCESS, + /// + ///Indicates that volume information is being gathered during an image capture. + /// + WIM_MSG_SCANNING, + /// + ///Indicates the number of files that will be captured or applied. + /// + WIM_MSG_SETRANGE, + /// + ///Indicates the number of files that have been captured or applied. + /// + WIM_MSG_SETPOS, + /// + ///Indicates that a file has been either captured or applied. + /// + WIM_MSG_STEPIT, + /// + ///Enables the caller to prevent a file resource from being compressed during a capture. + /// + WIM_MSG_COMPRESS, + /// + ///Alerts the caller that an error has occurred while capturing or applying an image. + /// + WIM_MSG_ERROR, + /// + ///Enables the caller to align a file resource on a particular alignment boundary. + /// + WIM_MSG_ALIGNMENT, + WIM_MSG_RETRY, + /// + ///Enables the caller to align a file resource on a particular alignment boundary. + /// + WIM_MSG_SPLIT, + WIM_MSG_SUCCESS = 0x00000000, + WIM_MSG_ABORT_IMAGE = 0xFFFFFFFF + } + + internal enum + WimCreationDisposition : uint + { + WimOpenExisting = 0x00000003, + } + + internal enum + WimActionFlags : uint + { + WimIgnored = 0x00000000 + } + + internal enum + WimCompressionType : uint + { + WimIgnored = 0x00000000 + } + + internal enum + WimCreationResult : uint + { + WimCreatedNew = 0x00000000, + WimOpenedExisting = 0x00000001 + } + + #endregion WIMGAPI + + #endregion Enums and Flags + + #region Structs + + [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] + public struct CreateVirtualDiskParameters + { + /// + /// A CREATE_VIRTUAL_DISK_VERSION enumeration that specifies the version of the CREATE_VIRTUAL_DISK_PARAMETERS structure being passed to or from the virtual hard disk (VHD) functions. + /// + public CreateVirtualDiskVersion Version; + + /// + /// Unique identifier to assign to the virtual disk object. If this member is set to zero, a unique identifier is created by the system. + /// + public Guid UniqueId; + + /// + /// The maximum virtual size of the virtual disk object. Must be a multiple of 512. + /// If a ParentPath is specified, this value must be zero. + /// If a SourcePath is specified, this value can be zero to specify the size of the source VHD to be used, otherwise the size specified must be greater than or equal to the size of the source disk. + /// + public ulong MaximumSize; + + /// + /// Internal size of the virtual disk object blocks. + /// The following are predefined block sizes and their behaviors. For a fixed VHD type, this parameter must be zero. + /// + public uint BlockSizeInBytes; + + /// + /// Internal size of the virtual disk object sectors. Must be set to 512. + /// + public uint SectorSizeInBytes; + + /// + /// Optional path to a parent virtual disk object. Associates the new virtual disk with an existing virtual disk. + /// If this parameter is not NULL, SourcePath must be NULL. + /// + public string ParentPath; + + /// + /// Optional path to pre-populate the new virtual disk object with block data from an existing disk. This path may refer to a VHD or a physical disk. + /// If this parameter is not NULL, ParentPath must be NULL. + /// + public string SourcePath; + + /// + /// Flags for opening the VHD + /// + public OpenVirtualDiskFlags OpenFlags; + + /// + /// GetInfoOnly flag for V2 handles + /// + public bool GetInfoOnly; + + /// + /// Virtual Storage Type of the parent disk + /// + public VirtualStorageType ParentVirtualStorageType; + + /// + /// Virtual Storage Type of the source disk + /// + public VirtualStorageType SourceVirtualStorageType; + + /// + /// A GUID to use for fallback resiliency over SMB. + /// + public Guid ResiliencyGuid; + } + + [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] + public struct VirtualStorageType + { + public VirtualStorageDeviceType DeviceId; + public Guid VendorId; + } + + [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] + public struct SecurityDescriptor + { + public byte revision; + public byte size; + public short control; + public IntPtr owner; + public IntPtr group; + public IntPtr sacl; + public IntPtr dacl; + } + + #endregion Structs + + #region VirtDisk.DLL P/Invoke + + [DllImport("virtdisk.dll", CharSet = CharSet.Unicode)] + public static extern uint + CreateVirtualDisk( + [In, Out] ref VirtualStorageType VirtualStorageType, + [In] string Path, + [In] VirtualDiskAccessMask VirtualDiskAccessMask, + [In, Out] ref SecurityDescriptor SecurityDescriptor, + [In] CreateVirtualDiskFlags Flags, + [In] uint ProviderSpecificFlags, + [In, Out] ref CreateVirtualDiskParameters Parameters, + [In] IntPtr Overlapped, + [Out] out SafeFileHandle Handle); + + #endregion VirtDisk.DLL P/Invoke + + #region Win32 P/Invoke + + [DllImport("advapi32", SetLastError = true)] + public static extern bool InitializeSecurityDescriptor( + [Out] out SecurityDescriptor pSecurityDescriptor, + [In] uint dwRevision); + + #endregion Win32 P/Invoke + + #region WIMGAPI P/Invoke + + #region SafeHandle wrappers for WimFileHandle and WimImageHandle + + public sealed class WimFileHandle : SafeHandle + { + + public WimFileHandle( + string wimPath) + : base(IntPtr.Zero, true) + { + + if (String.IsNullOrEmpty(wimPath)) + { + throw new ArgumentNullException("wimPath"); + } + + if (!File.Exists(Path.GetFullPath(wimPath))) + { + throw new FileNotFoundException((new FileNotFoundException()).Message, wimPath); + } + + NativeMethods.WimCreationResult creationResult; + + this.handle = NativeMethods.WimCreateFile( + wimPath, + NativeMethods.WimCreateFileDesiredAccess.WimGenericRead, + NativeMethods.WimCreationDisposition.WimOpenExisting, + NativeMethods.WimActionFlags.WimIgnored, + NativeMethods.WimCompressionType.WimIgnored, + out creationResult + ); + + // Check results. + if (creationResult != NativeMethods.WimCreationResult.WimOpenedExisting) + { + throw new Win32Exception(); + } + + if (this.handle == IntPtr.Zero) + { + throw new Win32Exception(); + } + + // Set the temporary path. + NativeMethods.WimSetTemporaryPath( + this, + Environment.ExpandEnvironmentVariables("%TEMP%") + ); + } + + protected override bool ReleaseHandle() + { + return NativeMethods.WimCloseHandle(this.handle); + } + + public override bool IsInvalid + { + get { return this.handle == IntPtr.Zero; } + } + } + + public sealed class WimImageHandle : SafeHandle + { + public WimImageHandle( + WimFile Container, + uint ImageIndex) + : base(IntPtr.Zero, true) + { + + if (null == Container) + { + throw new ArgumentNullException("Container"); + } + + if ((Container.Handle.IsClosed) || (Container.Handle.IsInvalid)) + { + throw new ArgumentNullException("The handle to the WIM file has already been closed, or is invalid.", "Container"); + } + + if (ImageIndex > Container.ImageCount) + { + throw new ArgumentOutOfRangeException("ImageIndex", "The index does not exist in the specified WIM file."); + } + + this.handle = NativeMethods.WimLoadImage( + Container.Handle.DangerousGetHandle(), + ImageIndex); + } + + protected override bool ReleaseHandle() + { + return NativeMethods.WimCloseHandle(this.handle); + } + + public override bool IsInvalid + { + get { return this.handle == IntPtr.Zero; } + } + } + + #endregion SafeHandle wrappers for WimFileHandle and WimImageHandle + + [DllImport("Wimgapi.dll", CharSet = CharSet.Unicode, SetLastError = true, EntryPoint = "WIMCreateFile")] + internal static extern IntPtr + WimCreateFile( + [In, MarshalAs(UnmanagedType.LPWStr)] string WimPath, + [In] WimCreateFileDesiredAccess DesiredAccess, + [In] WimCreationDisposition CreationDisposition, + [In] WimActionFlags FlagsAndAttributes, + [In] WimCompressionType CompressionType, + [Out, Optional] out WimCreationResult CreationResult + ); + + [DllImport("Wimgapi.dll", CharSet = CharSet.Unicode, SetLastError = true, EntryPoint = "WIMCloseHandle")] + [return: MarshalAs(UnmanagedType.Bool)] + internal static extern bool + WimCloseHandle( + [In] IntPtr Handle + ); + + [DllImport("Wimgapi.dll", CharSet = CharSet.Unicode, SetLastError = true, EntryPoint = "WIMLoadImage")] + internal static extern IntPtr + WimLoadImage( + [In] IntPtr Handle, + [In] uint ImageIndex + ); + + [DllImport("Wimgapi.dll", CharSet = CharSet.Unicode, SetLastError = true, EntryPoint = "WIMGetImageCount")] + internal static extern uint + WimGetImageCount( + [In] WimFileHandle Handle + ); + + [DllImport("Wimgapi.dll", CharSet = CharSet.Unicode, SetLastError = true, EntryPoint = "WIMGetImageInformation")] + [return: MarshalAs(UnmanagedType.Bool)] + internal static extern bool + WimGetImageInformation( + [In] SafeHandle Handle, + [Out] out StringBuilder ImageInfo, + [Out] out uint SizeOfImageInfo + ); + + [DllImport("Wimgapi.dll", CharSet = CharSet.Unicode, SetLastError = true, EntryPoint = "WIMSetTemporaryPath")] + [return: MarshalAs(UnmanagedType.Bool)] + internal static extern bool + WimSetTemporaryPath( + [In] WimFileHandle Handle, + [In] string TempPath + ); + + [DllImport("Wimgapi.dll", CharSet = CharSet.Unicode, SetLastError = true, EntryPoint = "WIMRegisterMessageCallback", CallingConvention = CallingConvention.StdCall)] + internal static extern uint + WimRegisterMessageCallback( + [In, Optional] WimFileHandle hWim, + [In] WimMessageCallback MessageProc, + [In, Optional] IntPtr ImageInfo + ); + + [DllImport("Wimgapi.dll", CharSet = CharSet.Unicode, SetLastError = true, EntryPoint = "WIMUnregisterMessageCallback", CallingConvention = CallingConvention.StdCall)] + [return: MarshalAs(UnmanagedType.Bool)] + internal static extern bool + WimUnregisterMessageCallback( + [In, Optional] WimFileHandle hWim, + [In] WimMessageCallback MessageProc + ); + + + #endregion WIMGAPI P/Invoke +} + +#region WIM Interop + +public class WimFile +{ + + internal XDocument m_xmlInfo; + internal List m_imageList; + + private static NativeMethods.WimMessageCallback wimMessageCallback; + + #region Events + + /// + /// DefaultImageEvent handler + /// + public delegate void DefaultImageEventHandler(object sender, DefaultImageEventArgs e); + + /// + ///ProcessFileEvent handler + /// + public delegate void ProcessFileEventHandler(object sender, ProcessFileEventArgs e); + + /// + ///Enable the caller to prevent a file resource from being compressed during a capture. + /// + public event ProcessFileEventHandler ProcessFileEvent; + + /// + ///Indicate an update in the progress of an image application. + /// + public event DefaultImageEventHandler ProgressEvent; + + /// + ///Alert the caller that an error has occurred while capturing or applying an image. + /// + public event DefaultImageEventHandler ErrorEvent; + + /// + ///Indicate that a file has been either captured or applied. + /// + public event DefaultImageEventHandler StepItEvent; + + /// + ///Indicate the number of files that will be captured or applied. + /// + public event DefaultImageEventHandler SetRangeEvent; + + /// + ///Indicate the number of files that have been captured or applied. + /// + public event DefaultImageEventHandler SetPosEvent; + + #endregion Events + + private + enum + ImageEventMessage : uint + { + /// + ///Enables the caller to prevent a file or a directory from being captured or applied. + /// + Progress = NativeMethods.WimMessage.WIM_MSG_PROGRESS, + /// + ///Notification sent to enable the caller to prevent a file or a directory from being captured or applied. + ///To prevent a file or a directory from being captured or applied, call WindowsImageContainer.SkipFile(). + /// + Process = NativeMethods.WimMessage.WIM_MSG_PROCESS, + /// + ///Enables the caller to prevent a file resource from being compressed during a capture. + /// + Compress = NativeMethods.WimMessage.WIM_MSG_COMPRESS, + /// + ///Alerts the caller that an error has occurred while capturing or applying an image. + /// + Error = NativeMethods.WimMessage.WIM_MSG_ERROR, + /// + ///Enables the caller to align a file resource on a particular alignment boundary. + /// + Alignment = NativeMethods.WimMessage.WIM_MSG_ALIGNMENT, + /// + ///Enables the caller to align a file resource on a particular alignment boundary. + /// + Split = NativeMethods.WimMessage.WIM_MSG_SPLIT, + /// + ///Indicates that volume information is being gathered during an image capture. + /// + Scanning = NativeMethods.WimMessage.WIM_MSG_SCANNING, + /// + ///Indicates the number of files that will be captured or applied. + /// + SetRange = NativeMethods.WimMessage.WIM_MSG_SETRANGE, + /// + ///Indicates the number of files that have been captured or applied. + /// + SetPos = NativeMethods.WimMessage.WIM_MSG_SETPOS, + /// + ///Indicates that a file has been either captured or applied. + /// + StepIt = NativeMethods.WimMessage.WIM_MSG_STEPIT, + /// + ///Success. + /// + Success = NativeMethods.WimMessage.WIM_MSG_SUCCESS, + /// + ///Abort. + /// + Abort = NativeMethods.WimMessage.WIM_MSG_ABORT_IMAGE + } + + /// + ///Event callback to the Wimgapi events + /// + private + uint + ImageEventMessagePump( + uint MessageId, + IntPtr wParam, + IntPtr lParam, + IntPtr UserData) + { + + uint status = (uint) NativeMethods.WimMessage.WIM_MSG_SUCCESS; + + DefaultImageEventArgs eventArgs = new DefaultImageEventArgs(wParam, lParam, UserData); + + switch ((ImageEventMessage)MessageId) + { + + case ImageEventMessage.Progress: + ProgressEvent(this, eventArgs); + break; + + case ImageEventMessage.Process: + if (null != ProcessFileEvent) + { + string fileToImage = Marshal.PtrToStringUni(wParam); + ProcessFileEventArgs fileToProcess = new ProcessFileEventArgs(fileToImage, lParam); + ProcessFileEvent(this, fileToProcess); + + if (fileToProcess.Abort == true) + { + status = (uint)ImageEventMessage.Abort; + } + } + break; + + case ImageEventMessage.Error: + if (null != ErrorEvent) + { + ErrorEvent(this, eventArgs); + } + break; + + case ImageEventMessage.SetRange: + if (null != SetRangeEvent) + { + SetRangeEvent(this, eventArgs); + } + break; + + case ImageEventMessage.SetPos: + if (null != SetPosEvent) + { + SetPosEvent(this, eventArgs); + } + break; + + case ImageEventMessage.StepIt: + if (null != StepItEvent) + { + StepItEvent(this, eventArgs); + } + break; + + default: + break; + } + return status; + + } + + /// + /// Constructor. + /// + /// Path to the WIM container. + public + WimFile(string wimPath) + { + if (string.IsNullOrEmpty(wimPath)) + { + throw new ArgumentNullException("wimPath"); + } + + if (!File.Exists(Path.GetFullPath(wimPath))) + { + throw new FileNotFoundException((new FileNotFoundException()).Message, wimPath); + } + + Handle = new NativeMethods.WimFileHandle(wimPath); + + // Hook up the events before we return. + //wimMessageCallback = new NativeMethods.WimMessageCallback(ImageEventMessagePump); + //NativeMethods.RegisterMessageCallback(this.Handle, wimMessageCallback); + } + + /// + /// Closes the WIM file. + /// + public void + Close() + { + foreach (WimImage image in Images) + { + image.Close(); + } + + if (null != wimMessageCallback) + { + NativeMethods.UnregisterMessageCallback(this.Handle, wimMessageCallback); + wimMessageCallback = null; + } + + if ((!Handle.IsClosed) && (!Handle.IsInvalid)) + { + Handle.Close(); + } + } + + /// + /// Provides a list of WimImage objects, representing the images in the WIM container file. + /// + public List + Images + { + get + { + if (null == m_imageList) + { + + int imageCount = (int)ImageCount; + m_imageList = new List(imageCount); + for (int i = 0; i < imageCount; i++) + { + + // Load up each image so it's ready for us. + m_imageList.Add( + new WimImage(this, (uint)i + 1)); + } + } + + return m_imageList; + } + } + + /// + /// Provides a list of names of the images in the specified WIM container file. + /// + public List + ImageNames + { + get + { + List nameList = new List(); + foreach (WimImage image in Images) + { + nameList.Add(image.ImageName); + } + return nameList; + } + } + + /// + /// Indexer for WIM images inside the WIM container, indexed by the image number. + /// The list of Images is 0-based, but the WIM container is 1-based, so we automatically compensate for that. + /// this[1] returns the 0th image in the WIM container. + /// + /// The 1-based index of the image to retrieve. + /// WinImage object. + public WimImage + this[int ImageIndex] + { + get { return Images[ImageIndex - 1]; } + } + + /// + /// Indexer for WIM images inside the WIM container, indexed by the image name. + /// WIMs created by different processes sometimes contain different information - including the name. + /// Some images have their name stored in the Name field, some in the Flags field, and some in the EditionID field. + /// We take all of those into account in while searching the WIM. + /// + /// + /// + public WimImage + this[string ImageName] + { + get + { + return + Images.Where(i => ( + i.ImageName.ToUpper() == ImageName.ToUpper() || + i.ImageFlags.ToUpper() == ImageName.ToUpper() )) + .DefaultIfEmpty(null) + .FirstOrDefault(); + } + } + + /// + /// Returns the number of images in the WIM container. + /// + internal uint + ImageCount + { + get { return NativeMethods.WimGetImageCount(Handle); } + } + + /// + /// Returns an XDocument representation of the XML metadata for the WIM container and associated images. + /// + internal XDocument + XmlInfo + { + get + { + + if (null == m_xmlInfo) + { + StringBuilder builder; + uint bytes; + if (!NativeMethods.WimGetImageInformation(Handle, out builder, out bytes)) + { + throw new Win32Exception(); + } + + // Ensure the length of the returned bytes to avoid garbage characters at the end. + int charCount = (int)bytes / sizeof(char); + if (null != builder) + { + // Get rid of the unicode file marker at the beginning of the XML. + builder.Remove(0, 1); + builder.EnsureCapacity(charCount - 1); + builder.Length = charCount - 1; + + // This isn't likely to change while we have the image open, so cache it. + m_xmlInfo = XDocument.Parse(builder.ToString().Trim()); + } + else + { + m_xmlInfo = null; + } + } + + return m_xmlInfo; + } + } + + public NativeMethods.WimFileHandle Handle + { + get; + private set; + } +} + +public class +WimImage +{ + + internal XDocument m_xmlInfo; + + public + WimImage( + WimFile Container, + uint ImageIndex) + { + + if (null == Container) + { + throw new ArgumentNullException("Container"); + } + + if ((Container.Handle.IsClosed) || (Container.Handle.IsInvalid)) + { + throw new ArgumentNullException("The handle to the WIM file has already been closed, or is invalid.", "Container"); + } + + if (ImageIndex > Container.ImageCount) + { + throw new ArgumentOutOfRangeException("ImageIndex", "The index does not exist in the specified WIM file."); + } + + Handle = new NativeMethods.WimImageHandle(Container, ImageIndex); + } + + public enum + Architectures : uint + { + x86 = 0x0, + ARM = 0x5, + IA64 = 0x6, + AMD64 = 0x9, + ARM64 = 0xC + } + + public void + Close() + { + if ((!Handle.IsClosed) && (!Handle.IsInvalid)) + { + Handle.Close(); + } + } + + public NativeMethods.WimImageHandle + Handle + { + get; + private set; + } + + internal XDocument + XmlInfo + { + get + { + + if (null == m_xmlInfo) + { + StringBuilder builder; + uint bytes; + if (!NativeMethods.WimGetImageInformation(Handle, out builder, out bytes)) + { + throw new Win32Exception(); + } + + // Ensure the length of the returned bytes to avoid garbage characters at the end. + int charCount = (int)bytes / sizeof(char); + if (null != builder) + { + // Get rid of the unicode file marker at the beginning of the XML. + builder.Remove(0, 1); + builder.EnsureCapacity(charCount - 1); + builder.Length = charCount - 1; + + // This isn't likely to change while we have the image open, so cache it. + m_xmlInfo = XDocument.Parse(builder.ToString().Trim()); + } + else + { + m_xmlInfo = null; + } + } + + return m_xmlInfo; + } + } + + public string + ImageIndex + { + get { return XmlInfo.Element("IMAGE").Attribute("INDEX").Value; } + } + + public string + ImageName + { + get { return XmlInfo.XPathSelectElement("/IMAGE/NAME").Value; } + } + + public string + ImageEditionId + { + get { return XmlInfo.XPathSelectElement("/IMAGE/WINDOWS/EDITIONID").Value; } + } + + public string + ImageFlags + { + get + { + string flagValue = String.Empty; + + try + { + flagValue = XmlInfo.XPathSelectElement("/IMAGE/FLAGS").Value; + } + catch + { + + // Some WIM files don't contain a FLAGS element in the metadata. + // In an effort to support those WIMs too, inherit the EditionId if there + // are no Flags. + + if (String.IsNullOrEmpty(flagValue)) + { + flagValue = this.ImageEditionId; + + // Check to see if the EditionId is "ServerHyper". If so, + // tweak it to be "ServerHyperCore" instead. + + if (0 == String.Compare("serverhyper", flagValue, true)) + { + flagValue = "ServerHyperCore"; + } + } + + } + + return flagValue; + } + } + + public string + ImageProductType + { + get { return XmlInfo.XPathSelectElement("/IMAGE/WINDOWS/PRODUCTTYPE").Value; } + } + + public string + ImageInstallationType + { + get { return XmlInfo.XPathSelectElement("/IMAGE/WINDOWS/INSTALLATIONTYPE").Value; } + } + + public string + ImageDescription + { + get { return XmlInfo.XPathSelectElement("/IMAGE/DESCRIPTION").Value; } + } + + public ulong + ImageSize + { + get { return ulong.Parse(XmlInfo.XPathSelectElement("/IMAGE/TOTALBYTES").Value); } + } + + public Architectures + ImageArchitecture + { + get + { + int arch = -1; + try + { + arch = int.Parse(XmlInfo.XPathSelectElement("/IMAGE/WINDOWS/ARCH").Value); + } + catch { } + + return (Architectures)arch; + } + } + + public string + ImageDefaultLanguage + { + get + { + string lang = null; + try + { + lang = XmlInfo.XPathSelectElement("/IMAGE/WINDOWS/LANGUAGES/DEFAULT").Value; + } + catch { } + + return lang; + } + } + + public Version + ImageVersion + { + get + { + int major = 0; + int minor = 0; + int build = 0; + int revision = 0; + + try + { + major = int.Parse(XmlInfo.XPathSelectElement("/IMAGE/WINDOWS/VERSION/MAJOR").Value); + minor = int.Parse(XmlInfo.XPathSelectElement("/IMAGE/WINDOWS/VERSION/MINOR").Value); + build = int.Parse(XmlInfo.XPathSelectElement("/IMAGE/WINDOWS/VERSION/BUILD").Value); + revision = int.Parse(XmlInfo.XPathSelectElement("/IMAGE/WINDOWS/VERSION/SPBUILD").Value); + } + catch { } + + return (new Version(major, minor, build, revision)); + } + } + + public string + ImageDisplayName + { + get { return XmlInfo.XPathSelectElement("/IMAGE/DISPLAYNAME").Value; } + } + + public string + ImageDisplayDescription + { + get { return XmlInfo.XPathSelectElement("/IMAGE/DISPLAYDESCRIPTION").Value; } + } +} + +/// +///Describes the file that is being processed for the ProcessFileEvent. +/// +public class +DefaultImageEventArgs : EventArgs +{ + /// + ///Default constructor. + /// + public + DefaultImageEventArgs( + IntPtr wideParameter, + IntPtr leftParameter, + IntPtr userData) + { + + WideParameter = wideParameter; + LeftParameter = leftParameter; + UserData = userData; + } + + /// + ///wParam + /// + public IntPtr WideParameter + { + get; + private set; + } + + /// + ///lParam + /// + public IntPtr LeftParameter + { + get; + private set; + } + + /// + ///UserData + /// + public IntPtr UserData + { + get; + private set; + } +} + +/// +///Describes the file that is being processed for the ProcessFileEvent. +/// +public class +ProcessFileEventArgs : EventArgs +{ + /// + ///Default constructor. + /// + ///Fully qualified path and file name. For example: c:\file.sys. + ///Default is false - skip file and continue. + ///Set to true to abort the entire image capture. + public + ProcessFileEventArgs( + string file, + IntPtr skipFileFlag) + { + + m_FilePath = file; + m_SkipFileFlag = skipFileFlag; + } + + /// + ///Skip file from being imaged. + /// + public void + SkipFile() + { + byte[] byteBuffer = + { + 0 + }; + int byteBufferSize = byteBuffer.Length; + Marshal.Copy(byteBuffer, 0, m_SkipFileFlag, byteBufferSize); + } + + /// + ///Fully qualified path and file name. + /// + public string + FilePath + { + get + { + string stringToReturn = ""; + if (m_FilePath != null) + { + stringToReturn = m_FilePath; + } + return stringToReturn; + } + } + + /// + ///Flag to indicate if the entire image capture should be aborted. + ///Default is false - skip file and continue. Setting to true will + ///abort the entire image capture. + /// + public bool Abort + { + set { m_Abort = value; } + get { return m_Abort; } + } + + private string m_FilePath; + private bool m_Abort; + private IntPtr m_SkipFileFlag; + +} + +#endregion WIM Interop + +#region VHD Interop +// Based on code written by the Hyper-V Test team. +/// +/// The Virtual Hard Disk class provides methods for creating and manipulating Virtual Hard Disk files. +/// +public class +VirtualHardDisk +{ + #region Static Methods + + #region Sparse Disks + + /// + /// Abbreviated signature of CreateSparseDisk so it's easier to use from WIM2VHD. + /// + /// The type of disk to create, VHD or VHDX. + /// The path of the disk to create. + /// The maximum size of the disk to create. + /// Overwrite the VHD if it already exists. + public static void + CreateSparseDisk( + NativeMethods.VirtualStorageDeviceType virtualStorageDeviceType, + string path, + ulong size, + bool overwrite) + { + + CreateSparseDisk( + path, + size, + overwrite, + null, + IntPtr.Zero, + (virtualStorageDeviceType == NativeMethods.VirtualStorageDeviceType.VHD) + ? NativeMethods.DEFAULT_BLOCK_SIZE + : 0, + virtualStorageDeviceType, + NativeMethods.DISK_SECTOR_SIZE); + } + + /// + /// Creates a new sparse (dynamically expanding) virtual hard disk (.vhd). Supports both sync and async modes. + /// The VHD image file uses only as much space on the backing store as needed to store the actual data the VHD currently contains. + /// + /// The path and name of the VHD to create. + /// The size of the VHD to create in bytes. + /// When creating this type of VHD, the VHD API does not test for free space on the physical backing store based on the maximum size requested, + /// therefore it is possible to successfully create a dynamic VHD with a maximum size larger than the available physical disk free space. + /// The maximum size of a dynamic VHD is 2,040 GB. The minimum size is 3 MB. + /// Optional path to pre-populate the new virtual disk object with block data from an existing disk + /// This path may refer to a VHD or a physical disk. Use NULL if you don't want a source. + /// If the VHD exists, setting this parameter to 'True' will delete it and create a new one. + /// If not null, the operation runs in async mode + /// Block size for the VHD. + /// VHD format version (VHD1 or VHD2) + /// Sector size for the VHD. + /// Thrown when an invalid size is specified + /// Thrown when source VHD is not found. + /// Thrown when there was an error while creating the default security descriptor. + /// Thrown when an error occurred while creating the VHD. + public static void + CreateSparseDisk( + string path, + ulong size, + bool overwrite, + string source, + IntPtr overlapped, + uint blockSizeInBytes, + NativeMethods.VirtualStorageDeviceType virtualStorageDeviceType, + uint sectorSizeInBytes) + { + + // Validate the virtualStorageDeviceType + if (virtualStorageDeviceType != NativeMethods.VirtualStorageDeviceType.VHD && virtualStorageDeviceType != NativeMethods.VirtualStorageDeviceType.VHDX) + { + + throw ( + new ArgumentOutOfRangeException( + "virtualStorageDeviceType", + virtualStorageDeviceType, + "VirtualStorageDeviceType must be VHD or VHDX." + )); + } + + // Validate size. It needs to be a multiple of DISK_SECTOR_SIZE (512)... + if ((size % NativeMethods.DISK_SECTOR_SIZE) != 0) + { + + throw ( + new ArgumentOutOfRangeException( + "size", + size, + "The size of the virtual disk must be a multiple of 512." + )); + } + + if ((!String.IsNullOrEmpty(source)) && (!System.IO.File.Exists(source))) + { + + throw ( + new System.IO.FileNotFoundException( + "Unable to find the source file.", + source + )); + } + + if ((overwrite) && (System.IO.File.Exists(path))) + { + + System.IO.File.Delete(path); + } + + NativeMethods.CreateVirtualDiskParameters createParams = new NativeMethods.CreateVirtualDiskParameters(); + + // Select the correct version. + createParams.Version = (virtualStorageDeviceType == NativeMethods.VirtualStorageDeviceType.VHD) + ? NativeMethods.CreateVirtualDiskVersion.Version1 + : NativeMethods.CreateVirtualDiskVersion.Version2; + + createParams.UniqueId = Guid.NewGuid(); + createParams.MaximumSize = size; + createParams.BlockSizeInBytes = blockSizeInBytes; + createParams.SectorSizeInBytes = sectorSizeInBytes; + createParams.ParentPath = null; + createParams.SourcePath = source; + createParams.OpenFlags = NativeMethods.OpenVirtualDiskFlags.None; + createParams.GetInfoOnly = false; + createParams.ParentVirtualStorageType = new NativeMethods.VirtualStorageType(); + createParams.SourceVirtualStorageType = new NativeMethods.VirtualStorageType(); + + // + // Create and init a security descriptor. + // Since we're creating an essentially blank SD to use with CreateVirtualDisk + // the VHD will take on the security values from the parent directory. + // + + NativeMethods.SecurityDescriptor securityDescriptor; + if (!NativeMethods.InitializeSecurityDescriptor(out securityDescriptor, 1)) + { + + throw ( + new SecurityException( + "Unable to initialize the security descriptor for the virtual disk." + )); + } + + NativeMethods.VirtualStorageType virtualStorageType = new NativeMethods.VirtualStorageType(); + virtualStorageType.DeviceId = virtualStorageDeviceType; + virtualStorageType.VendorId = NativeMethods.VirtualStorageTypeVendorMicrosoft; + + SafeFileHandle vhdHandle; + + uint returnCode = NativeMethods.CreateVirtualDisk( + ref virtualStorageType, + path, + (virtualStorageDeviceType == NativeMethods.VirtualStorageDeviceType.VHD) + ? NativeMethods.VirtualDiskAccessMask.All + : NativeMethods.VirtualDiskAccessMask.None, + ref securityDescriptor, + NativeMethods.CreateVirtualDiskFlags.None, + 0, + ref createParams, + overlapped, + out vhdHandle); + + vhdHandle.Close(); + + if (NativeMethods.ERROR_SUCCESS != returnCode && NativeMethods.ERROR_IO_PENDING != returnCode) + { + + throw ( + new Win32Exception( + (int)returnCode + )); + } + } + + #endregion Sparse Disks + + #region Fixed Disks + + /// + /// Abbreviated signature of CreateFixedDisk so it's easier to use from WIM2VHD. + /// + /// The type of disk to create, VHD or VHDX. + /// The path of the disk to create. + /// The maximum size of the disk to create. + /// Overwrite the VHD if it already exists. + public static void + CreateFixedDisk( + NativeMethods.VirtualStorageDeviceType virtualStorageDeviceType, + string path, + ulong size, + bool overwrite) + { + + CreateFixedDisk( + path, + size, + overwrite, + null, + IntPtr.Zero, + 0, + virtualStorageDeviceType, + NativeMethods.DISK_SECTOR_SIZE); + } + + /// + /// Creates a fixed-size Virtual Hard Disk. Supports both sync and async modes. This methods always calls the V2 version of the + /// CreateVirtualDisk API, and creates VHD2. + /// + /// The path and name of the VHD to create. + /// The size of the VHD to create in bytes. + /// The VHD image file is pre-allocated on the backing store for the maximum size requested. + /// The maximum size of a dynamic VHD is 2,040 GB. The minimum size is 3 MB. + /// Optional path to pre-populate the new virtual disk object with block data from an existing disk + /// This path may refer to a VHD or a physical disk. Use NULL if you don't want a source. + /// If the VHD exists, setting this parameter to 'True' will delete it and create a new one. + /// If not null, the operation runs in async mode + /// Block size for the VHD. + /// Virtual storage device type: VHD1 or VHD2. + /// Sector size for the VHD. + /// Creating a fixed disk can be a time consuming process! + /// Thrown when an invalid size or wrong virtual storage device type is specified. + /// Thrown when source VHD is not found. + /// Thrown when there was an error while creating the default security descriptor. + /// Thrown when an error occurred while creating the VHD. + public static void + CreateFixedDisk( + string path, + ulong size, + bool overwrite, + string source, + IntPtr overlapped, + uint blockSizeInBytes, + NativeMethods.VirtualStorageDeviceType virtualStorageDeviceType, + uint sectorSizeInBytes) + { + + // Validate the virtualStorageDeviceType + if (virtualStorageDeviceType != NativeMethods.VirtualStorageDeviceType.VHD && virtualStorageDeviceType != NativeMethods.VirtualStorageDeviceType.VHDX) + { + + throw ( + new ArgumentOutOfRangeException( + "virtualStorageDeviceType", + virtualStorageDeviceType, + "VirtualStorageDeviceType must be VHD or VHDX." + )); + } + + // Validate size. It needs to be a multiple of DISK_SECTOR_SIZE (512)... + if ((size % NativeMethods.DISK_SECTOR_SIZE) != 0) + { + + throw ( + new ArgumentOutOfRangeException( + "size", + size, + "The size of the virtual disk must be a multiple of 512." + )); + } + + if ((!String.IsNullOrEmpty(source)) && (!System.IO.File.Exists(source))) + { + + throw ( + new System.IO.FileNotFoundException( + "Unable to find the source file.", + source + )); + } + + if ((overwrite) && (System.IO.File.Exists(path))) + { + + System.IO.File.Delete(path); + } + + NativeMethods.CreateVirtualDiskParameters createParams = new NativeMethods.CreateVirtualDiskParameters(); + + // Select the correct version. + createParams.Version = (virtualStorageDeviceType == NativeMethods.VirtualStorageDeviceType.VHD) + ? NativeMethods.CreateVirtualDiskVersion.Version1 + : NativeMethods.CreateVirtualDiskVersion.Version2; + + createParams.UniqueId = Guid.NewGuid(); + createParams.MaximumSize = size; + createParams.BlockSizeInBytes = blockSizeInBytes; + createParams.SectorSizeInBytes = sectorSizeInBytes; + createParams.ParentPath = null; + createParams.SourcePath = source; + createParams.OpenFlags = NativeMethods.OpenVirtualDiskFlags.None; + createParams.GetInfoOnly = false; + createParams.ParentVirtualStorageType = new NativeMethods.VirtualStorageType(); + createParams.SourceVirtualStorageType = new NativeMethods.VirtualStorageType(); + + // + // Create and init a security descriptor. + // Since we're creating an essentially blank SD to use with CreateVirtualDisk + // the VHD will take on the security values from the parent directory. + // + + NativeMethods.SecurityDescriptor securityDescriptor; + if (!NativeMethods.InitializeSecurityDescriptor(out securityDescriptor, 1)) + { + throw ( + new SecurityException( + "Unable to initialize the security descriptor for the virtual disk." + )); + } + + NativeMethods.VirtualStorageType virtualStorageType = new NativeMethods.VirtualStorageType(); + virtualStorageType.DeviceId = virtualStorageDeviceType; + virtualStorageType.VendorId = NativeMethods.VirtualStorageTypeVendorMicrosoft; + + SafeFileHandle vhdHandle; + + uint returnCode = NativeMethods.CreateVirtualDisk( + ref virtualStorageType, + path, + (virtualStorageDeviceType == NativeMethods.VirtualStorageDeviceType.VHD) + ? NativeMethods.VirtualDiskAccessMask.All + : NativeMethods.VirtualDiskAccessMask.None, + ref securityDescriptor, + NativeMethods.CreateVirtualDiskFlags.FullPhysicalAllocation, + 0, + ref createParams, + overlapped, + out vhdHandle); + + vhdHandle.Close(); + + if (NativeMethods.ERROR_SUCCESS != returnCode && NativeMethods.ERROR_IO_PENDING != returnCode) + { + + throw ( + new Win32Exception( + (int)returnCode + )); + } + } + + #endregion Fixed Disks + + #endregion Static Methods + +} +#endregion VHD Interop +} +"@ + + Add-Type -TypeDefinition $code -ReferencedAssemblies "System.Xml", "System.Linq", "System.Xml.Linq" -ErrorAction SilentlyContinue +} + +# This is required for renewed "Mount-RegistryHive" and "Dismount-RegistryHive" +# functions using Windows API. Code borrowed from +# http://www.leeholmes.com/blog/2010/09/24/adjusting-token-privileges-in-powershell/ + +Function +Set-TokenPrivilege { + param( + + # The privilege to adjust. This set is taken from + # http://msdn.microsoft.com/library/bb530716 + + [ValidateSet( + + "SeAssignPrimaryTokenPrivilege", + "SeAuditPrivilege", + "SeBackupPrivilege", + "SeChangeNotifyPrivilege", + "SeCreateGlobalPrivilege", + "SeCreatePagefilePrivilege", + "SeCreatePermanentPrivilege", + "SeCreateSymbolicLinkPrivilege", + "SeCreateTokenPrivilege", + "SeDebugPrivilege", + "SeEnableDelegationPrivilege", + "SeImpersonatePrivilege", + "SeIncreaseBasePriorityPrivilege", + "SeIncreaseQuotaPrivilege", + "SeIncreaseWorkingSetPrivilege", + "SeLoadDriverPrivilege", + "SeLockMemoryPrivilege", + "SeMachineAccountPrivilege", + "SeManageVolumePrivilege", + "SeProfileSingleProcessPrivilege", + "SeRelabelPrivilege", + "SeRemoteShutdownPrivilege", + "SeRestorePrivilege", + "SeSecurityPrivilege", + "SeShutdownPrivilege", + "SeSyncAgentPrivilege", + "SeSystemEnvironmentPrivilege", + "SeSystemProfilePrivilege", + "SeSystemtimePrivilege", + "SeTakeOwnershipPrivilege", + "SeTcbPrivilege", + "SeTimeZonePrivilege", + "SeTrustedCredManAccessPrivilege", + "SeUndockPrivilege", + "SeUnsolicitedInputPrivilege" + )] + $Privilege, + + # The process on which to adjust the privilege. Defaults to the current process. + $ProcessId = $pid, + + # Switch to disable the privilege, rather than enable it. + [Switch] + $Disable + ) + + # Taken from P/Invoke.NET with minor adjustments. + $Definition = @' + + using System; + + using System.Runtime.InteropServices; + public class AdjPriv + + { + + [DllImport("advapi32.dll", ExactSpelling = true, SetLastError = true)] + + internal static extern bool AdjustTokenPrivileges(IntPtr htok, bool disall, + + ref TokPriv1Luid newst, int len, IntPtr prev, IntPtr relen); + [DllImport("advapi32.dll", ExactSpelling = true, SetLastError = true)] + + internal static extern bool OpenProcessToken(IntPtr h, int acc, ref IntPtr phtok); + [DllImport("advapi32.dll", SetLastError = true)] + + internal static extern bool LookupPrivilegeValue(string host, string name, ref long pluid); + [StructLayout(LayoutKind.Sequential, Pack = 1)] + + internal struct TokPriv1Luid + + { + public int Count; + public long Luid; + public int Attr; + } + + internal const int SE_PRIVILEGE_ENABLED = 0x00000002; + internal const int SE_PRIVILEGE_DISABLED = 0x00000000; + internal const int TOKEN_QUERY = 0x00000008; + + internal const int TOKEN_ADJUST_PRIVILEGES = 0x00000020; + public static bool EnablePrivilege(long processHandle, string privilege, bool disable) + + { + + bool retVal; + TokPriv1Luid tp; + IntPtr hproc = new IntPtr(processHandle); + IntPtr htok = IntPtr.Zero; + retVal = OpenProcessToken(hproc, TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY, ref htok); + tp.Count = 1; + tp.Luid = 0; + if(disable) + + { + tp.Attr = SE_PRIVILEGE_DISABLED; + } + + else + + { + tp.Attr = SE_PRIVILEGE_ENABLED; + } + + retVal = LookupPrivilegeValue(null, privilege, ref tp.Luid); + retVal = AdjustTokenPrivileges(htok, false, ref tp, 0, IntPtr.Zero, IntPtr.Zero); + return retVal; + } + } +'@ + + $processHandle = ( Get-Process -Id $ProcessId ).Handle + + $Type = Add-Type -TypeDefinition $Definition -PassThru + + $Type[0]::EnablePrivilege( $ProcessHandle, $Privilege, $Disable ) +} + +# In version 10.0.14300.1000, the below two functions were changed from leveraging +# reg.exe to native Windows API. + +Function +Mount-RegistryHive { + [CmdletBinding()] + param( + [Parameter( + Mandatory = $True, + ValueFromPipeline = $True, + Position = 0 + )] + [System.IO.FileInfo] + [ValidateNotNullOrEmpty()] + [ValidateScript( + { $_.Exists } + )] + $Hive + ) + + $mountKey = [System.Guid]::NewGuid().ToString() + + Try { + $Definition = @" + +[DllImport("advapi32.dll", SetLastError=true)] +public static extern long RegLoadKey(int hKey, String lpSubKey, String lpFile); + +"@ + + $TokenPrivilege = Set-TokenPrivilege -Privilege "SeBackupPrivilege" + $TokenPrivilege = Set-TokenPrivilege -Privilege "SeRestorePrivilege" + + $HKLM = 0x80000002 + + $Reg = Add-Type -MemberDefinition $Definition -Name "ClassLoad" -Namespace "Win32Functions" -PassThru + + $Result = $Reg::RegLoadKey( $HKLM, $mountKey, $Hive ) + + } + Catch { + Throw + } + + # Set a global variable containing the name of the mounted registry key + # so we can unmount it if there's an error. + $global:mountedHive = $mountKey + + return $mountKey +} + +Function +Dismount-RegistryHive { + [CmdletBinding()] + param( + [Parameter( + Mandatory = $True, + ValueFromPipeline = $True, + Position = 0 + )] + [string] + [ValidateNotNullOrEmpty()] + $HiveMountPoint + ) + + Try { + $Definition = @" + +[DllImport("advapi32.dll", SetLastError=true)] +public static extern int RegUnLoadKey(Int32 hKey,string lpSubKey); + +"@ + + $TokenPrivilege = Set-TokenPrivilege -Privilege "SeBackupPrivilege" + $TokenPrivilege = Set-TokenPrivilege -Privilege "SeRestorePrivilege" + + $HKLM = 0x80000002 + + $Reg = Add-Type -MemberDefinition $Definition -Name "ClassUnload" -Namespace "Win32Functions" -PassThru + + $Result = $Reg::RegUnLoadKey( $HKLM, $HiveMountPoint ) + + } + Catch { + Throw + } + + $global:mountedHive = $null +} + +<# + .SYNOPSIS + Short function to determine whether the logged-on user is an administrator. + + .EXAMPLE + Do you honestly need one? There are no parameters! + + .OUTPUTS + $true if user is admin. + $false if user is not an admin. +#> + +Function +Test-Admin { + [CmdletBinding()] + param() + + $currentUser = New-Object -TypeName "Security.Principal.WindowsPrincipal" -ArgumentList $( [Security.Principal.WindowsIdentity]::GetCurrent() ) + + $isAdmin = $currentUser.IsInRole([Security.Principal.WindowsBuiltinRole]::Administrator) + + Write-Debug -Message " is current user Admin? $isAdmin" + + Return $isAdmin +} + +Function +Get-WindowsBuildNumber { + $os = Get-CimInstance -ClassName "Win32_OperatingSystem" -Verbose:$False + Return [int]($os.BuildNumber) +} + +Function +Test-WindowsVersion { + $isWin8 = ( Get-WindowsBuildNumber ) -ge [int]$lowestSupportedBuild + + Write-Debug -Message " is current OS supported? $isWin8" + + Return $isWin8 +} + +<# + .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. +#> + +Function +Start-Executable { + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string] + [ValidateNotNullOrEmpty()] + $Executable, + + [Parameter(Mandatory = $true)] + [string[]] + [ValidateNotNullOrEmpty()] + $Arguments, + + [Parameter()] + [int] + [ValidateNotNullOrEmpty()] + $SuccessfulErrorCode = 0 + ) + + Write-Debug -Message " Running `"$Executable`" with parameters: `"$Arguments`"" + + $StartProcessParam = @{ + + FilePath = $Executable + ArgumentList = $Arguments + NoNewWindow = $True + Wait = $True + RedirectStandardOutput = [io.path]::combine( $TempDirectory, $scriptName, $sessionKey, "$Executable-StandardOutput.txt" ) + RedirectStandardError = [io.path]::combine( $TempDirectory, $scriptName, $sessionKey, "$Executable-StandardError.txt" ) + Passthru = $True + } + $ret = Start-Process @StartProcessParam + + Write-Debug -Message " Return code was $($ret.ExitCode)." + + If ($ret.ExitCode -ne $SuccessfulErrorCode) { + throw "$Executable failed with code $($ret.ExitCode)!" + } +} + +<# + .SYNOPSIS + Determines whether or not a given path is a network location or a local drive. + + .DESCRIPTION + Function to determine whether or not a specified path is a local path, a UNC path, + or a mapped network drive. + + .PARAMETER Path + The path that we need to figure stuff out about, +#> + +Function +Test-IsNetworkLocation { + [CmdletBinding()] + param( + [Parameter(ValueFromPipeLine = $true)] + [string] + [ValidateNotNullOrEmpty()] + $Path + ) + + $Result = $False + + If ([Bool]( [URI]$Path ).IsUNC) { + $Result = $True + } + else { + $driveInfo = [IO.DriveInfo]( ( Resolve-Path -Path $Path ).Path ) + + If ($driveInfo.DriveType -eq "Network") { + $Result = $True + } + } + + Return $Result +} + +# Import module silently + +Function +Import-ModuleEx { + + #region Data + + [cmdletbinding()] + + Param( + + [parameter( + ParameterSetName = "Name", + Mandatory = $True + )] + [System.String] + $Name, + [parameter( + ParameterSetName = "ModuleInfo", + Mandatory = $True + )] + [System.Management.Automation.PSModuleInfo] + $ModuleInfo + ) + + #endregion Data + + #region Code + + # When we have $VerbosePreference defined as "Continue" and import + # a module (either implicitly, by firt use, or explictily, calling + # "Import-Module"), there's a lot of Verbose output listing every cmdlet + # and every function. This output provides no value, thus we need + # to suppress it. Unfortunately, even if we pass "-Verobse:$False" + # to "Import-Module", it only helps to swallow the list of cmdlets. + # The list of functions is still thrown to output. (This is probably + # a bug). Thus, we need to temporary change $VerbosePreference. + + $VerbosePreferenceCurrent = $VerbosePreference + $Global:VerbosePreference = "SilentlyContinue" + + If (Test-Path -Path "Variable:\ModuleInternal") { + # $Item = Remove-Item -Path "Variable:\ModuleInternal" -Confirm:$false + + # Remove-Variable -Name "Module" -Scope "Global" + # Remove-Variable -Name "Module" -Scope "Local" + Remove-Variable -Name "Module" -Scope "Script" + } + + Switch ($PsCmdlet.ParameterSetName) { + "Name" { + If (( Get-Module -Name $Name -ListAvailable ) -Or + ( Test-Path -Path $Name )) { + $ModuleInternal = Import-Module -Name $Name -PassThru -WarningAction Ignore + } + } + + "ModuleInfo" { + $ModuleInternal = Import-Module -ModuleInfo $ModuleInfo -PassThru -WarningAction Ignore + } + } + + $Global:VerbosePreference = $VerbosePreferenceCurrent + + If (Test-Path -Path "Variable:\ModuleInternal") { + Return $ModuleInternal + } + + #endregion Code + +} diff --git a/deployment/dsc/azshcihost/Hyper-ConvertImage/10.2/PSGetModuleInfo.xml b/deployment/dsc/azshcihost/Hyper-ConvertImage/10.2/PSGetModuleInfo.xml new file mode 100644 index 0000000..b56eecf Binary files /dev/null and b/deployment/dsc/azshcihost/Hyper-ConvertImage/10.2/PSGetModuleInfo.xml differ diff --git a/deployment/dsc/azshcihost/MSCatalog/0.21.0/Classes/MSCatalogResponse.Class.ps1 b/deployment/dsc/azshcihost/MSCatalog/0.21.0/Classes/MSCatalogResponse.Class.ps1 new file mode 100644 index 0000000..38c8b7a --- /dev/null +++ b/deployment/dsc/azshcihost/MSCatalog/0.21.0/Classes/MSCatalogResponse.Class.ps1 @@ -0,0 +1,18 @@ +class MSCatalogResponse { + [HtmlAgilityPack.HtmlNodeCollection] $Rows + [string] $EventArgument + [string] $EventValidation + [string] $ViewState + [string] $ViewStateGenerator + [string] $NextPage + + MSCatalogResponse($HtmlDoc) { + $Table = $HtmlDoc.GetElementbyId("ctl00_catalogBody_updateMatches") + $this.Rows = $Table.SelectNodes("tr") + $this.EventArgument = $HtmlDoc.GetElementbyId("__EVENTARGUMENT")[0].Attributes["value"].Value + $this.EventValidation = $HtmlDoc.GetElementbyId("__EVENTVALIDATION")[0].Attributes["value"].Value + $this.ViewState = $HtmlDoc.GetElementbyId("__VIEWSTATE")[0].Attributes["value"].Value + $this.ViewStateGenerator = $HtmlDoc.GetElementbyId("__VIEWSTATEGENERATOR")[0].Attributes["value"].Value + $this.NextPage = $HtmlDoc.GetElementbyId("ctl00_catalogBody_nextPage") + } +} \ No newline at end of file diff --git a/deployment/dsc/azshcihost/MSCatalog/0.21.0/Classes/MSCatalogUpdate.Class.ps1 b/deployment/dsc/azshcihost/MSCatalog/0.21.0/Classes/MSCatalogUpdate.Class.ps1 new file mode 100644 index 0000000..75db9c5 --- /dev/null +++ b/deployment/dsc/azshcihost/MSCatalog/0.21.0/Classes/MSCatalogUpdate.Class.ps1 @@ -0,0 +1,31 @@ +class MSCatalogUpdate { + [string] $Title + [string] $Products + [string] $Classification + [datetime] $LastUpdated + [string] $Version + [string] $Size + [string] $SizeInBytes + [string] $Guid + [string[]] $FileNames + + MSCatalogUpdate() {} + + MSCatalogUpdate($Row, $IncludeFileNames) { + $Cells = $Row.SelectNodes("td") + $this.Title = $Cells[1].innerText.Trim() + $this.Products = $Cells[2].innerText.Trim() + $this.Classification = $Cells[3].innerText.Trim() + $this.LastUpdated = (Invoke-ParseDate -DateString $Cells[4].innerText.Trim()) + $this.Version = $Cells[5].innerText.Trim() + $this.Size = $Cells[6].SelectNodes("span")[0].InnerText + $this.SizeInBytes = [Int] $Cells[6].SelectNodes("span")[1].InnerText + $this.Guid = $Cells[7].SelectNodes("input")[0].Id + $this.FileNames = if ($IncludeFileNames) { + $Links = Get-UpdateLinks -Guid $Cells[7].SelectNodes("input")[0].Id + foreach ($Link in $Links.Matches) { + $Link.Value.Split('/')[-1] + } + } + } +} \ No newline at end of file diff --git a/deployment/dsc/azshcihost/MSCatalog/0.21.0/Format/MSCatalogUpdate.Format.ps1xml b/deployment/dsc/azshcihost/MSCatalog/0.21.0/Format/MSCatalogUpdate.Format.ps1xml new file mode 100644 index 0000000..ad0511e --- /dev/null +++ b/deployment/dsc/azshcihost/MSCatalog/0.21.0/Format/MSCatalogUpdate.Format.ps1xml @@ -0,0 +1,51 @@ + + + + + MSCatalogUpdate + + MSCatalogUpdate + + + + + + + + + + + + + + + + + + + + + + + + Title + + + Products + + + Classification + + + Get-Date -Date $_.LastUpdated -Format "yyyy/MM/dd" + + + Size + + + + + + + + \ No newline at end of file diff --git a/deployment/dsc/azshcihost/MSCatalog/0.21.0/MSCatalog.psd1 b/deployment/dsc/azshcihost/MSCatalog/0.21.0/MSCatalog.psd1 new file mode 100644 index 0000000..3608bdc --- /dev/null +++ b/deployment/dsc/azshcihost/MSCatalog/0.21.0/MSCatalog.psd1 @@ -0,0 +1,122 @@ +# +# Module manifest for module 'MSCatalog' +# + +@{ + + # Script module or binary module file associated with this manifest. + RootModule = 'MSCatalog.psm1' + + # Version number of this module. + ModuleVersion = '0.21.0' + + # Supported PSEditions + # CompatiblePSEditions = @() + + # ID used to uniquely identify this module + GUID = 'e30d3bed-1a89-41f0-b058-339a17dffd69' + + # Author of this module + Author = 'Ryan Kowalewski' + + # Company or vendor of this module + CompanyName = '' + + # Copyright statement for this module + Copyright = '(c) 2019 Ryan Kowalewski. All rights reserved.' + + # Description of the functionality provided by this module + Description = 'PowerShell module for searching and downloading offline updates from https://www.catalog.update.microsoft.com' + + # Minimum version of the Windows PowerShell engine required by this module + # PowerShellVersion = '' + + # Name of the Windows PowerShell host required by this module + # PowerShellHostName = '' + + # Minimum version of the Windows PowerShell host required by this module + # PowerShellHostVersion = '' + + # Minimum version of Microsoft .NET Framework required by this module. This prerequisite is valid for the PowerShell Desktop edition only. + # DotNetFrameworkVersion = '' + + # Minimum version of the common language runtime (CLR) required by this module. This prerequisite is valid for the PowerShell Desktop edition only. + # CLRVersion = '' + + # Processor architecture (None, X86, Amd64) required by this module + # ProcessorArchitecture = '' + + # Modules that must be imported into the global environment prior to importing this module + # RequiredModules = @() + + # Assemblies that must be loaded prior to importing this module + # RequiredAssemblies = @() + + # Script files (.ps1) that are run in the caller's environment prior to importing this module. + # ScriptsToProcess = @() + + # Type files (.ps1xml) to be loaded when importing this module + # TypesToProcess = @() + + # Format files (.ps1xml) to be loaded when importing this module + FormatsToProcess = @( + '.\Format\MSCatalogUpdate.Format.ps1xml' + ) + + # Modules to import as nested modules of the module specified in RootModule/ModuleToProcess + # NestedModules = @() + + # Functions to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no functions to export. + FunctionsToExport = @( + 'Get-MSCatalogUpdate', + 'Save-MSCatalogUpdate' + ) + + # Cmdlets to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no cmdlets to export. + #CmdletsToExport = '*' + + # Variables to export from this module + #VariablesToExport = '*' + + # Aliases to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no aliases to export. + #AliasesToExport = '*' + + # DSC resources to export from this module + # DscResourcesToExport = @() + + # List of all modules packaged with this module + # ModuleList = @() + + # List of all files packaged with this module + # FileList = @() + + # Private data to pass to the module specified in RootModule/ModuleToProcess. This may also contain a PSData hashtable with additional module metadata used by PowerShell. + PrivateData = @{ + + PSData = @{ + + # Tags applied to this module. These help with module discovery in online galleries. + # Tags = @() + + # A URL to the license for this module. + # LicenseUri = '' + + # A URL to the main website for this project. + ProjectUri = 'https://github.com/ryan-jan/MSCatalog' + + # A URL to an icon representing this module. + # IconUri = '' + + # ReleaseNotes of this module + # ReleaseNotes = '' + + } # End of PSData hashtable + + } # End of PrivateData hashtable + + # HelpInfo URI of this module + # HelpInfoURI = '' + + # Default prefix for commands exported from this module. Override the default prefix using Import-Module -Prefix. + # DefaultCommandPrefix = '' +} diff --git a/deployment/dsc/azshcihost/MSCatalog/0.21.0/MSCatalog.psm1 b/deployment/dsc/azshcihost/MSCatalog/0.21.0/MSCatalog.psm1 new file mode 100644 index 0000000..24f8478 --- /dev/null +++ b/deployment/dsc/azshcihost/MSCatalog/0.21.0/MSCatalog.psm1 @@ -0,0 +1,27 @@ + +try { + if (!([System.Management.Automation.PSTypeName]'HtmlAgilityPack.HtmlDocument').Type) { + if ($PSVersionTable.PSEdition -eq "Desktop") { + Add-Type -Path "$PSScriptRoot\Types\Net45\HtmlAgilityPack.dll" + } else { + Add-Type -Path "$PSScriptRoot\Types\netstandard2.0\HtmlAgilityPack.dll" + } + } +} catch { + $Err = $_ + throw $Err +} + +$Public = @(Get-ChildItem -Path $PSScriptRoot\Public\*.ps1) +$Private = @(Get-ChildItem -Path $PSScriptRoot\Private\*.ps1) +$Classes = @(Get-ChildItem -Path $PSScriptRoot\Classes\*.ps1) + +foreach ($Module in ($Public + $Private + $Classes)) { + try { + . $Module.FullName + } catch { + Write-Error -Message "Failed to import function $($Module.FullName): $_" + } +} + +Export-ModuleMember -Function $Public.BaseName diff --git a/deployment/dsc/azshcihost/MSCatalog/0.21.0/PSGetModuleInfo.xml b/deployment/dsc/azshcihost/MSCatalog/0.21.0/PSGetModuleInfo.xml new file mode 100644 index 0000000..cb2c537 Binary files /dev/null and b/deployment/dsc/azshcihost/MSCatalog/0.21.0/PSGetModuleInfo.xml differ diff --git a/deployment/dsc/azshcihost/MSCatalog/0.21.0/Private/Get-UpdateLinks.ps1 b/deployment/dsc/azshcihost/MSCatalog/0.21.0/Private/Get-UpdateLinks.ps1 new file mode 100644 index 0000000..d5b52b3 --- /dev/null +++ b/deployment/dsc/azshcihost/MSCatalog/0.21.0/Private/Get-UpdateLinks.ps1 @@ -0,0 +1,26 @@ +function Get-UpdateLinks { + [CmdLetBinding()] + param ( + [Parameter( + Mandatory = $true, + Position = 0 + )] + [String] $Guid + ) + + $Post = @{size = 0; updateID = $Guid; uidInfo = $Guid} | ConvertTo-Json -Compress + $Body = @{updateIDs = "[$Post]"} + + $Params = @{ + Uri = "https://www.catalog.update.microsoft.com/DownloadDialog.aspx" + Method = "Post" + Body = $Body + ContentType = "application/x-www-form-urlencoded" + UseBasicParsing = $true + } + $DownloadDialog = Invoke-WebRequest @Params + $Links = $DownloadDialog.Content.Replace("www.download.windowsupdate", "download.windowsupdate") + $Regex = "(http[s]?\://dl\.delivery\.mp\.microsoft\.com\/[^\'\""]*)|(http[s]?\://download\.windowsupdate\.com\/[^\'\""]*)" + $Links = $Links | Select-String -AllMatches -Pattern $Regex + $Links +} \ No newline at end of file diff --git a/deployment/dsc/azshcihost/MSCatalog/0.21.0/Private/Invoke-CatalogRequest.ps1 b/deployment/dsc/azshcihost/MSCatalog/0.21.0/Private/Invoke-CatalogRequest.ps1 new file mode 100644 index 0000000..66897f7 --- /dev/null +++ b/deployment/dsc/azshcihost/MSCatalog/0.21.0/Private/Invoke-CatalogRequest.ps1 @@ -0,0 +1,61 @@ +function Invoke-CatalogRequest { + [CmdletBinding()] + param ( + [parameter(Mandatory = $true)] + [string] $Uri, + + [Parameter(Mandatory = $false)] + [string] $Method = "Get", + + [Parameter(Mandatory = $false)] + [string] $EventArgument, + + [Parameter(Mandatory = $false)] + [string] $EventTarget, + + [Parameter(Mandatory = $false)] + [string] $EventValidation, + + [Parameter(Mandatory = $false)] + [string] $ViewState, + + [Parameter(Mandatory = $false)] + [string] $ViewStateGenerator + ) + + try { + if ($Method -eq "Post") { + $ReqBody = @{ + "__EVENTARGUMENT" = $EventArgument + "__EVENTTARGET" = $EventTarget + "__EVENTVALIDATION" = $EventValidation + "__VIEWSTATE" = $ViewState + "__VIEWSTATEGENERATOR" = $ViewStateGenerator + } + } + $Params = @{ + Uri = [Uri]::EscapeUriString($Uri) + Method = $Method + Body = $ReqBody + ContentType = "application/x-www-form-urlencoded" + UseBasicParsing = $true + ErrorAction = "Stop" + } + $Results = Invoke-WebRequest @Params + $HtmlDoc = [HtmlAgilityPack.HtmlDocument]::new() + $HtmlDoc.LoadHtml($Results.RawContent.ToString()) + $NoResults = $HtmlDoc.GetElementbyId("ctl00_catalogBody_noResultText") + if ($null -eq $NoResults) { + [MSCatalogResponse]::new($HtmlDoc) + } else { + throw "No results found." + } + } catch { + if ($_.Exception.Message -eq "No results found.") { + Write-Warning "$($NoResults.InnerText)$($Uri.Split("q=")[-1])" + break + } else { + throw $_ + } + } +} \ No newline at end of file diff --git a/deployment/dsc/azshcihost/MSCatalog/0.21.0/Private/Invoke-DownloadFile.ps1 b/deployment/dsc/azshcihost/MSCatalog/0.21.0/Private/Invoke-DownloadFile.ps1 new file mode 100644 index 0000000..333ba76 --- /dev/null +++ b/deployment/dsc/azshcihost/MSCatalog/0.21.0/Private/Invoke-DownloadFile.ps1 @@ -0,0 +1,24 @@ +function Invoke-DownloadFile { + [CmdLetBinding()] + param ( + [uri] $Uri, + [string] $Path, + [switch] $UseBits + ) + + try { + if ($UseBits) { + Start-BitsTransfer -Source $Uri -Destination $Path + } else { + $WebClient = [System.Net.WebClient]::new() + $WebClient.DownloadFile($Uri, $Path) + $WebClient.Dispose() + } + } catch { + $Err = $_ + if ($WebClient) { + $WebClient.Dispose() + } + throw $Err + } +} \ No newline at end of file diff --git a/deployment/dsc/azshcihost/MSCatalog/0.21.0/Private/Invoke-ParseDate.ps1 b/deployment/dsc/azshcihost/MSCatalog/0.21.0/Private/Invoke-ParseDate.ps1 new file mode 100644 index 0000000..1b9370d --- /dev/null +++ b/deployment/dsc/azshcihost/MSCatalog/0.21.0/Private/Invoke-ParseDate.ps1 @@ -0,0 +1,8 @@ +function Invoke-ParseDate { + param ( + [String] $DateString + ) + + $Array = $DateString.Split("/") + Get-Date -Year $Array[2] -Month $Array[0] -Day $Array[1] +} \ No newline at end of file diff --git a/deployment/dsc/azshcihost/MSCatalog/0.21.0/Private/Sort-CatalogResults.ps1 b/deployment/dsc/azshcihost/MSCatalog/0.21.0/Private/Sort-CatalogResults.ps1 new file mode 100644 index 0000000..8696e8f --- /dev/null +++ b/deployment/dsc/azshcihost/MSCatalog/0.21.0/Private/Sort-CatalogResults.ps1 @@ -0,0 +1,76 @@ +function Sort-CatalogResults { + [CmdletBinding()] + param ( + [Parameter(Mandatory = $true)] + [string] $Uri, + + [Parameter(Mandatory = $false)] + [ValidateSet("Title", "Products", "Classification", "LastUpdated", "Size")] + [string] $SortBy, + + [Parameter(Mandatory = $false)] + [switch] $Descending, + + [Parameter(DontShow)] + [string] $EventArgument, + + [Parameter(DontShow)] + [string] $EventValidation, + + [Parameter(DontShow)] + [string] $ViewState, + + [Parameter(DontShow)] + [string] $ViewStateGenerator + ) + + $EventTarget = switch ($SortBy) { + {$_ -eq "Title"} {'ctl00$catalogBody$updateMatches$ctl02$titleHeaderLink'} + {$_ -eq "Products"} {'ctl00$catalogBody$updateMatches$ctl02$productsHeaderLink'} + {$_ -eq "Classification"} {'ctl00$catalogBody$updateMatches$ctl02$classHeaderLink'} + {$_ -eq "LastUpdated"} {'ctl00$catalogBody$updateMatches$ctl02$dateHeaderLink'} + {$_ -eq "Size"} {'ctl00$catalogBody$updateMatches$ctl02$sizeHeaderLink'} + } + + $Params = @{ + Uri = $Uri + Method = "Post" + EventArgument = $EventArgument + EventTarget = $EventTarget + EventValidation = $EventValidation + ViewState = $ViewState + ViewStateGenerator = $ViewStateGenerator + } + $Res = Invoke-CatalogRequest @Params + + # By default the LastUpdated field is sorted in descending order from the catalog website the first time you + # issue the sort POST request. All the other fields are sorted in ascending order first as you would expect. + # To ensure that this sort function is predictable we re-request the sorted request if the SortBy parameter is LastUpdated + # and the Descending parameter is $false in order to return the results sorted in ascending order which is what would + # be expected. + if (($SortBy -eq "LastUpdated") -and -not $Descending) { + $Params = @{ + Uri = $Uri + Method = "Post" + EventArgument = $Res.EventArgument + EventTarget = $EventTarget + EventValidation = $Res.EventValidation + ViewState = $Res.ViewState + ViewStateGenerator = $Res.ViewStateGenerator + } + $Res = Invoke-CatalogRequest @Params + } elseif (($SortBy -ne "LastUpdated") -and $Descending) { + $Params = @{ + Uri = $Uri + Method = "Post" + EventArgument = $Res.EventArgument + EventTarget = $EventTarget + EventValidation = $Res.EventValidation + ViewState = $Res.ViewState + ViewStateGenerator = $Res.ViewStateGenerator + } + $Res = Invoke-CatalogRequest @Params + } + + $Res +} \ No newline at end of file diff --git a/deployment/dsc/azshcihost/MSCatalog/0.21.0/Public/Get-MSCatalogUpdate.ps1 b/deployment/dsc/azshcihost/MSCatalog/0.21.0/Public/Get-MSCatalogUpdate.ps1 new file mode 100644 index 0000000..2f1b45d --- /dev/null +++ b/deployment/dsc/azshcihost/MSCatalog/0.21.0/Public/Get-MSCatalogUpdate.ps1 @@ -0,0 +1,161 @@ +function Get-MSCatalogUpdate { + <# + .SYNOPSIS + Query catalog.update.micrsosoft.com for available updates. + + .DESCRIPTION + Given that there is currently no public API available for the catalog.update.micrsosoft.com site, this + command makes HTTP requests to the site and parses the returned HTML for the required data. + + .PARAMETER Search + Specify a string to search for. + + .PARAMETER SortBy + Specify a field to sort the results by. The default sort is by LastUpdated and in descending order. + + .PARAMETER Descending + Switch the sort order to descending. + + .PARAMETER Strict + Force a Search paramater with multiple words to be treated as a single string. + + .PARAMETER IncludeFileNames + Include the filenames for the files as they would be downloaded from catalog.update.micrsosoft.com. + This option will cause an extra web request for each update included in the results. It is best to only + use this option with a very narrow search term. + + .PARAMETER AllPages + By default the Get-MSCatalogUpdate command returns the first page of results from catalog.update.micrsosoft.com, which is + limited to 25 updates. If you specify this switch the command will instead return all pages of search results. + This can result in a significant increase in the number of HTTP requests to the catalog.update.micrsosoft.com endpoint. + + .EXAMPLE + Get-MSCatalogUpdate -Search "Cumulative for Windows Server, version 1903" + + .EXAMPLE + Get-MSCatalogUpdate -Search "Cumulative for Windows Server, version 1903" -SortBy "Title" -Descending + + .EXAMPLE + Get-MSCatalogUpdate -Search "Cumulative for Windows Server, version 1903" -Strict + + .EXAMPLE + Get-MSCatalogUpdate -Search "Cumulative for Windows Server, version 1903" -IncludeFileNames + + .EXAMPLE + Get-MSCatalogUpdate -Search "Cumulative for Windows Server, version 1903" -AllPages + #> + + [CmdLetBinding()] + param ( + [Parameter(Mandatory = $true)] + [string] $Search, + + [Parameter(Mandatory = $false)] + [ValidateSet("Title", "Products", "Classification", "LastUpdated", "Size")] + [string] $SortBy, + + [Parameter(Mandatory = $false)] + [switch] $Descending, + + [Parameter(Mandatory = $false)] + [switch] $Strict, + + [Parameter(Mandatory = $false)] + [switch] $IncludeFileNames, + + [Parameter(Mandatory = $false)] + [switch] $AllPages + ) + + try { + $ProgPref = $ProgressPreference + $ProgressPreference = "SilentlyContinue" + + $Uri = "https://www.catalog.update.microsoft.com/Search.aspx?q=$Search" + $Res = Invoke-CatalogRequest -Uri $Uri + + if ($PSBoundParameters.ContainsKey("SortBy")) { + $SortParams = @{ + Uri = $Uri + SortBy = $SortBy + Descending = $Descending + EventArgument = $Res.EventArgument + EventValidation = $Res.EventValidation + ViewState = $Res.ViewState + ViewStateGenerator = $Res.ViewStateGenerator + } + $Res = Sort-CatalogResults @SortParams + } else { + # Default sort is by LastUpdated and in descending order. + $SortParams = @{ + Uri = $Uri + SortBy = "LastUpdated" + Descending = $true + EventArgument = $Res.EventArgument + EventValidation = $Res.EventValidation + ViewState = $Res.ViewState + ViewStateGenerator = $Res.ViewStateGenerator + } + $Res = Sort-CatalogResults @SortParams + } + + $Rows = $Res.Rows + + if ($Strict -and -not $AllPages) { + $StrictRows = $Rows.Where({ + $_.SelectNodes("td")[1].innerText.Trim() -like "*$Search*" + }) + # If $NextPage is $null then there are more pages to collect. It is arse backwards but trust me. + while (($StrictRows.Count -lt 25) -and ($Res.NextPage -eq "")) { + $NextParams = @{ + Uri = $Uri + EventArgument = $Res.EventArgument + EventTarget = 'ctl00$catalogBody$nextPageLinkText' + EventValidation = $Res.EventValidation + ViewState = $Res.ViewState + ViewStateGenerator = $Res.ViewStateGenerator + Method = "Post" + } + $Res = Invoke-CatalogRequest @NextParams + $StrictRows += $Rows.Where({ + $_.SelectNodes("td")[1].innerText.Trim() -like "*$Search*" + }) + } + $Rows = $StrictRows[0..24] + } elseif ($AllPages) { + # If $NextPage is $null then there are more pages to collect. It is arse backwards but trust me. + while ($Res.NextPage -eq "") { + $NextParams = @{ + Uri = $Uri + EventArgument = $Res.EventArgument + EventTarget = 'ctl00$catalogBody$nextPageLinkText' + EventValidation = $Res.EventValidation + ViewState = $Res.ViewState + ViewStateGenerator = $Res.ViewStateGenerator + Method = "Post" + } + $Res = Invoke-CatalogRequest @NextParams + $Rows += $Res.Rows + } + if ($Strict) { + $Rows = $Rows.Where({ + $_.SelectNodes("td")[1].innerText.Trim() -like "*$Search*" + }) + } + } + + if ($Rows.Count -gt 0) { + foreach ($Row in $Rows) { + if ($Row.Id -ne "headerRow") { + [MSCatalogUpdate]::new($Row, $IncludeFileNames) + } + } + } else { + Write-Warning "No updates found matching the search term." + } + $ProgressPreference = $ProgPref + } catch { + $ProgressPreference = $ProgPref + throw $_ + } +} diff --git a/deployment/dsc/azshcihost/MSCatalog/0.21.0/Public/Save-MSCatalogUpdate.ps1 b/deployment/dsc/azshcihost/MSCatalog/0.21.0/Public/Save-MSCatalogUpdate.ps1 new file mode 100644 index 0000000..afacf93 --- /dev/null +++ b/deployment/dsc/azshcihost/MSCatalog/0.21.0/Public/Save-MSCatalogUpdate.ps1 @@ -0,0 +1,146 @@ +function Save-MSCatalogUpdate { + <# + .SYNOPSIS + Download an update file from catalog.update.micrsosoft.com. + + .PARAMETER Update + Specify the update to be downloaded. + The update object is retrieved using the Get-MSCatalogUpdate function. + + .PARAMETER Guid + Specify the Guid for the update to be downloaded. + The Guid is retrieved using the Get-MSCatalogUpdate function. + + .PARAMETER Destination + Specify the destination directory to download the update to. + + .PARAMETER Language + Some updates are available in multiple languages. By default this function will list all available + files for a specific update and prompt you to select the one to download. If you wish to remove + this prompt you can specify a language-country code combination e.g. "en-us". + + .PARAMETER UseBits + If using a Windows system you can use this parameter to download the update using BITS. + + .EXAMPLE + $Update = Get-MSCatalogUpdate -Search "KB4515384" + Save-MSCatalogUpdate -Update $Update -Destination C:\Windows\Temp\ + + .EXAMPLE + Save-MSCatalogUpdate -Guid "5570183b-a0b7-4478-b0af-47a6e65417ca" -Destination C:\Windows\Temp\ + + .EXAMPLE + $Update = Get-MSCatalogUpdate -Search "KB4515384" + Save-MSCatalogUpdate -Update $Update -Destination C:\Windows\Temp\ -Language "en-us" + + .EXAMPLE + $Update = Get-MSCatalogUpdate -Search "KB4515384" + Save-MSCatalogUpdate -Update $Update -Destination C:\Windows\Temp\ -UseBits + #> + + param ( + [Parameter( + Mandatory = $true, + Position = 0, + ParameterSetName = "ByObject" + )] + [Object] $Update, + + [Parameter( + Mandatory = $true, + Position = 0, + ValueFromPipelineByPropertyName = "Guid", + ParameterSetName = "ByGuid" + )] + [String] $Guid, + + [Parameter( + Mandatory = $true, + Position = 1, + ParameterSetName = "ByObject" + )] + [Parameter( + Mandatory = $true, + Position = 1, + ParameterSetName = "ByGuid" + )] + [String] $Destination, + + [Parameter( + Mandatory = $false, + Position = 2, + ParameterSetName = "ByObject" + )] + [Parameter( + Mandatory = $false, + Position = 2, + ParameterSetName = "ByGuid" + )] + [String] $Language, + + [Parameter( + Mandatory = $false, + Position = 3, + ParameterSetName = "ByObject" + )] + [Parameter( + Mandatory = $false, + Position = 3, + ParameterSetName = "ByGuid" + )] + [Switch] $UseBits + ) + + if ($Update) { + $Guid = $Update.Guid + } + + $Links = Get-UpdateLinks -Guid $Guid + if ($Links.Matches.Count -eq 1) { + $Link = $Links.Matches[0] + $OutFile = Join-Path -Path (Get-Item -Path $Destination) -ChildPath $Link.Value.Split('/')[-1] + if ($UseBits) { + Invoke-DownloadFile -Uri $Link.Value -Path $OutFile -UseBits + } else { + Invoke-DownloadFile -Uri $Link.Value -Path $OutFile + } + } elseif ($Language) { + $Link = $Links.Matches.Where({$_.Value -match $Language})[0] + $OutFile = Join-Path -Path (Get-Item -Path $Destination) -ChildPath $Link.Value.Split('/')[-1] + if ($UseBits) { + Invoke-DownloadFile -Uri $Link.Value -Path $OutFile -UseBits + } else { + Invoke-DownloadFile -Uri $Link.Value -Path $OutFile + } + } else { + Write-Host "Id FileName`r" + Write-Host "-- --------" + foreach ($Link in $Links.Matches) { + $Id = $Links.Matches.IndexOf($Link) + $FileName = $Link.Value.Split('/')[-1] + if ($Id -lt 10) { + Write-Host " $Id $FileName`r" + } else { + Write-Host "$Id $FileName`r" + } + } + $SelectedId = Read-Host "Multiple files exist for this update. Enter the Id of the file to download or 'A' to download all files." + $ToDownload = @() + if ($SelectedId -like "A") { + foreach ($Link in $Links.Matches) { + $ToDownload += $Link.Value + } + } else { + $ToDownload += $Links.Matches[$SelectedId].Value + } + + foreach ($Item in $ToDownload) { + $OutFile = Join-Path -Path (Get-Item -Path $Destination) -ChildPath $Item.Split('/')[-1] + if ($UseBits) { + Invoke-DownloadFile -Uri $Item -Path $OutFile -UseBits + } else { + Invoke-DownloadFile -Uri $Item -Path $OutFile + } + } + } +} diff --git a/deployment/dsc/azshcihost/MSCatalog/0.21.0/Types/Net45/HtmlAgilityPack.dll b/deployment/dsc/azshcihost/MSCatalog/0.21.0/Types/Net45/HtmlAgilityPack.dll new file mode 100644 index 0000000..ed3c716 Binary files /dev/null and b/deployment/dsc/azshcihost/MSCatalog/0.21.0/Types/Net45/HtmlAgilityPack.dll differ diff --git a/deployment/dsc/azshcihost/MSCatalog/0.21.0/Types/netstandard2.0/HtmlAgilityPack.dll b/deployment/dsc/azshcihost/MSCatalog/0.21.0/Types/netstandard2.0/HtmlAgilityPack.dll new file mode 100644 index 0000000..072653e Binary files /dev/null and b/deployment/dsc/azshcihost/MSCatalog/0.21.0/Types/netstandard2.0/HtmlAgilityPack.dll differ diff --git a/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_DefaultGatewayAddress/DSC_DefaultGatewayAddress.psm1 b/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_DefaultGatewayAddress/DSC_DefaultGatewayAddress.psm1 new file mode 100644 index 0000000..d103472 --- /dev/null +++ b/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_DefaultGatewayAddress/DSC_DefaultGatewayAddress.psm1 @@ -0,0 +1,376 @@ +$modulePath = Join-Path -Path (Split-Path -Path (Split-Path -Path $PSScriptRoot -Parent) -Parent) -ChildPath 'Modules' + +# Import the Networking Common Modules +Import-Module -Name (Join-Path -Path $modulePath ` + -ChildPath (Join-Path -Path 'NetworkingDsc.Common' ` + -ChildPath 'NetworkingDsc.Common.psm1')) + +Import-Module -Name (Join-Path -Path $modulePath -ChildPath 'DscResource.Common') + +# Import Localization Strings +$script:localizedData = Get-LocalizedData -DefaultUICulture 'en-US' + +<# + .SYNOPSIS + Returns the current state of the Default Gateway for an interface. + + .PARAMETER InterfaceAlias + Alias of the network interface for which the default gateway address is set. + + .PARAMETER AddressFamily + IP address family. + + .PARAMETER Address + The desired default gateway address - if not provided default gateway will be removed. +#> +function Get-TargetResource +{ + [CmdletBinding()] + [OutputType([System.Collections.Hashtable])] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $InterfaceAlias, + + [Parameter(Mandatory = $true)] + [ValidateSet('IPv4', 'IPv6')] + [System.String] + $AddressFamily, + + [Parameter()] + [System.String] + $Address + ) + + Write-Verbose -Message ( @("$($MyInvocation.MyCommand): " + $($script:localizedData.GettingDefaultGatewayAddressMessage) + ) -join '' ) + + $destinationPrefix = Get-NetDefaultGatewayDestinationPrefix ` + -AddressFamily $AddressFamily + + $defaultRoutes = Get-NetDefaultRoute ` + -InterfaceAlias $InterfaceAlias ` + -AddressFamily $AddressFamily + + $returnValue = @{ + AddressFamily = $AddressFamily + InterfaceAlias = $InterfaceAlias + } + + <# + If there is a Default Gateway defined for this interface/address family add it + to the return value. + #> + if ($defaultRoutes) + { + $returnValue += @{ + Address = $defaultRoutes.NextHop + } + } + else + { + $returnValue += @{ + Address = $null + } + } + + return $returnValue +} + +<# + .SYNOPSIS + Sets the Default Gateway for an interface. + + .PARAMETER InterfaceAlias + Alias of the network interface for which the default gateway address is set. + + .PARAMETER AddressFamily + IP address family. + + .PARAMETER Address + The desired default gateway address - if not provided default gateway will be removed. +#> +function Set-TargetResource +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $InterfaceAlias, + + [Parameter(Mandatory = $true)] + [ValidateSet('IPv4', 'IPv6')] + [System.String] + $AddressFamily, + + [Parameter()] + [System.String] + $Address + ) + + Write-Verbose -Message ( @("$($MyInvocation.MyCommand): " + $($script:localizedData.ApplyingDefaultGatewayAddressMessage) + ) -join '' ) + + $defaultRoutes = @(Get-NetDefaultRoute ` + -InterfaceAlias $InterfaceAlias ` + -AddressFamily $AddressFamily) + + # Remove any existing default routes + foreach ($defaultRoute in $defaultRoutes) + { + Remove-NetRoute ` + -DestinationPrefix $defaultRoute.DestinationPrefix ` + -NextHop $defaultRoute.NextHop ` + -InterfaceIndex $defaultRoute.InterfaceIndex ` + -AddressFamily $defaultRoute.AddressFamily ` + -Confirm:$false -ErrorAction Stop + } + + if ($Address) + { + $destinationPrefix = Get-NetDefaultGatewayDestinationPrefix ` + -AddressFamily $AddressFamily + + # Set the correct Default Route + $newNetRouteParameters = @{ + DestinationPrefix = $destinationPrefix + InterfaceAlias = $InterfaceAlias + AddressFamily = $AddressFamily + NextHop = $Address + } + + New-NetRoute @newNetRouteParameters -ErrorAction Stop + + Write-Verbose -Message ( @("$($MyInvocation.MyCommand): " + $($script:localizedData.DefaultGatewayAddressSetToDesiredStateMessage) + ) -join '' ) + } + else + { + Write-Verbose -Message ( @("$($MyInvocation.MyCommand): " + $($script:localizedData.DefaultGatewayRemovedMessage) + ) -join '' ) + } +} + +<# + .SYNOPSIS + Tests the state of the Default Gateway for an interface. + + .PARAMETER InterfaceAlias + Alias of the network interface for which the default gateway address is set. + + .PARAMETER AddressFamily + IP address family. + + .PARAMETER Address + The desired default gateway address - if not provided default gateway will be removed. +#> +function Test-TargetResource +{ + [CmdletBinding()] + [OutputType([System.Boolean])] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $InterfaceAlias, + + [Parameter(Mandatory = $true)] + [ValidateSet('IPv4', 'IPv6')] + [System.String] + $AddressFamily, + + [Parameter()] + [System.String] + $Address + ) + + # Flag to signal whether settings are correct + $desiredConfigurationMatch = $true + + Write-Verbose -Message ( @("$($MyInvocation.MyCommand): " + $($script:localizedData.CheckingDefaultGatewayAddressMessage) + ) -join '' ) + + Assert-ResourceProperty @PSBoundParameters + + $defaultRoutes = @(Get-NetDefaultRoute ` + -InterfaceAlias $InterfaceAlias ` + -AddressFamily $AddressFamily) + + # Test if the Default Gateway passed is equal to the current default gateway + if ($Address) + { + if ($defaultRoutes) + { + $nextHopRoute = $defaultRoutes.Where( { + $_.NextHop -eq $Address + } ) + + if ($nextHopRoute) + { + Write-Verbose -Message ( @("$($MyInvocation.MyCommand): " + $($script:localizedData.DefaultGatewayCorrectMessage) + ) -join '' ) + } + else + { + Write-Verbose -Message ( @("$($MyInvocation.MyCommand): " + $($script:localizedData.DefaultGatewayNotMatchMessage) -f $Address, $defaultRoutes.NextHop + ) -join '' ) + $desiredConfigurationMatch = $false + } + } + else + { + Write-Verbose -Message ( @("$($MyInvocation.MyCommand): " + $($script:localizedData.DefaultGatewayDoesNotExistMessage) -f $Address + ) -join '' ) + $desiredConfigurationMatch = $false + } + } + else + { + # Is a default gateway address set? + if ($defaultRoutes) + { + Write-Verbose -Message ( @("$($MyInvocation.MyCommand): " + $($script:localizedData.DefaultGatewayExistsButShouldNotMessage) + ) -join '' ) + $desiredConfigurationMatch = $false + } + else + { + Write-Verbose -Message ( @("$($MyInvocation.MyCommand): " + $($script:localizedData.DefaultGatewayExistsAndShouldMessage) + ) -join '' ) + } + } + + return $desiredConfigurationMatch +} + +<# + .SYNOPSIS + Check the Address details are valid and do not conflict with Address family. + Ensures interface exists. If any problems are detected an exception will be thrown. + + .PARAMETER InterfaceAlias + Alias of the network interface for which the default gateway address is set. + + .PARAMETER AddressFamily + IP address family. + + .PARAMETER Address + The desired default gateway address - if not provided default gateway will be removed. +#> +function Assert-ResourceProperty +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $InterfaceAlias, + + [Parameter()] + [ValidateSet('IPv4', 'IPv6')] + [System.String] + $AddressFamily = 'IPv4', + + [Parameter()] + [System.String] + $Address + ) + + if (-not (Get-NetAdapter | Where-Object -Property Name -EQ $InterfaceAlias )) + { + New-InvalidOperationException ` + -Message ($script:localizedData.InterfaceNotAvailableError -f $InterfaceAlias) + } + + if ($Address) + { + Assert-IPAddress -Address $Address -AddressFamily $AddressFamily + } +} # Assert-ResourceProperty + +<# + .SYNOPSIS + Get the default gateway destination prefix for the IP address family. + + .PARAMETER AddressFamily + IP address family. +#> +function Get-NetDefaultGatewayDestinationPrefix +{ + [CmdletBinding()] + [OutputType([System.String])] + param + ( + [Parameter()] + [ValidateSet('IPv4', 'IPv6')] + [System.String] + $AddressFamily = 'IPv4' + ) + + if ($AddressFamily -eq 'IPv4') + { + $destinationPrefix = '0.0.0.0/0' + } + else + { + $destinationPrefix = '::/0' + } + + return $destinationPrefix +} # Get-NetDefaultGatewayDestinationPrefix + +<# + .SYNOPSIS + Get the default network routes assigned to the interface. + + .PARAMETER InterfaceAlias + Alias of the network interface for which the default gateway address is set. + + .PARAMETER AddressFamily + IP address family. +#> +function Get-NetDefaultRoute +{ + [CmdletBinding()] + [OutputType([System.String[]])] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $InterfaceAlias, + + [Parameter()] + [ValidateSet('IPv4', 'IPv6')] + [System.String] + $AddressFamily = 'IPv4' + ) + + $destinationPrefix = Get-NetDefaultGatewayDestinationPrefix ` + -AddressFamily $AddressFamily + + return @(Get-NetRoute ` + -InterfaceAlias $InterfaceAlias ` + -AddressFamily $AddressFamily ` + -ErrorAction Stop).Where({ + $_.DestinationPrefix -eq $destinationPrefix + }) +} # Get-NetDefaultRoute + +Export-ModuleMember -function *-TargetResource diff --git a/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_DefaultGatewayAddress/DSC_DefaultGatewayAddress.schema.mof b/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_DefaultGatewayAddress/DSC_DefaultGatewayAddress.schema.mof new file mode 100644 index 0000000..a7a6306 --- /dev/null +++ b/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_DefaultGatewayAddress/DSC_DefaultGatewayAddress.schema.mof @@ -0,0 +1,7 @@ +[ClassVersion("1.0.0"), FriendlyName("DefaultGatewayAddress")] +class DSC_DefaultGatewayAddress : OMI_BaseResource +{ + [Key, Description("Alias of the network interface for which the default gateway address is set.")] string InterfaceAlias; + [Key, Description("IP address family."), ValueMap{"IPv4", "IPv6"},Values{"IPv4", "IPv6"}] string AddressFamily; + [Write, Description("The desired default gateway address - if not provided default gateway will be removed.")] string Address; +}; diff --git a/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_DefaultGatewayAddress/README.md b/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_DefaultGatewayAddress/README.md new file mode 100644 index 0000000..5d8f1fa --- /dev/null +++ b/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_DefaultGatewayAddress/README.md @@ -0,0 +1,4 @@ +# Description + +The resource is responsible for creating and managing the Default Gateway for +an interface on a node. diff --git a/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_DefaultGatewayAddress/en-US/DSC_DefaultGatewayAddress.strings.psd1 b/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_DefaultGatewayAddress/en-US/DSC_DefaultGatewayAddress.strings.psd1 new file mode 100644 index 0000000..66f757a --- /dev/null +++ b/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_DefaultGatewayAddress/en-US/DSC_DefaultGatewayAddress.strings.psd1 @@ -0,0 +1,15 @@ +# Localized resources for DSC_DefaultGatewayAddress + +ConvertFrom-StringData @' + GettingDefaultGatewayAddressMessage = Getting the Default Gateway Address. + ApplyingDefaultGatewayAddressMessage = Applying the Default Gateway Address. + DefaultGatewayAddressSetToDesiredStateMessage = Default Gateway address was set to the desired state. + DefaultGatewayRemovedMessage = Default Gateway address has been removed. + CheckingDefaultGatewayAddressMessage = Checking the Default Gateway Address. + DefaultGatewayNotMatchMessage = Default gateway does NOT match desired state. Expected "{0}", actual "{1}". + DefaultGatewayCorrectMessage = Default gateway is correct. + DefaultGatewayDoesNotExistMessage = Default gateway does not exist. Expected "{0}". + DefaultGatewayExistsButShouldNotMessage = Default gateway exists but it should not. + DefaultGatewayExistsAndShouldMessage = Default Gateway does not exist which is correct. + InterfaceNotAvailableError = Interface "{0}" is not available. Please select a valid interface and try again. +'@ diff --git a/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_DefaultGatewayAddress/en-US/about_DefaultGatewayAddress.help.txt b/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_DefaultGatewayAddress/en-US/about_DefaultGatewayAddress.help.txt new file mode 100644 index 0000000..b6c01c5 --- /dev/null +++ b/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_DefaultGatewayAddress/en-US/about_DefaultGatewayAddress.help.txt @@ -0,0 +1,60 @@ +.NAME + DefaultGatewayAddress + +.DESCRIPTION + The resource is responsible for creating and managing the Default Gateway for + an interface on a node. + +.PARAMETER InterfaceAlias + Key - String + Alias of the network interface for which the default gateway address is set. + +.PARAMETER AddressFamily + Key - String + Allowed values: IPv4, IPv6 + IP address family. + +.PARAMETER Address + Write - String + The desired default gateway address - if not provided default gateway will be removed. + +.EXAMPLE 1 + +Remove the IPv4 default gateway from the network interface +'Ethernet'. + +Configuration DefaultGatewayAddress_RemoveDefaultGateway_Config +{ + Import-DscResource -Module NetworkingDsc + + Node localhost + { + DefaultGatewayAddress RemoveDefaultGateway + { + InterfaceAlias = 'Ethernet' + AddressFamily = 'IPv4' + } + } +} + +.EXAMPLE 2 + +Set the IPv4 default gateway of the network interface 'Ethernet' +to '192.168.1.1'. + +Configuration DefaultGatewayAddress_SetDefaultGateway_Config +{ + Import-DscResource -Module NetworkingDsc + + Node localhost + { + DefaultGatewayAddress SetDefaultGateway + { + Address = '192.168.1.1' + InterfaceAlias = 'Ethernet' + AddressFamily = 'IPv4' + } + } +} + + diff --git a/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_DnsClientGlobalSetting/DSC_DnsClientGlobalSetting.data.psd1 b/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_DnsClientGlobalSetting/DSC_DnsClientGlobalSetting.data.psd1 new file mode 100644 index 0000000..b0447e1 --- /dev/null +++ b/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_DnsClientGlobalSetting/DSC_DnsClientGlobalSetting.data.psd1 @@ -0,0 +1,16 @@ +@{ + ParameterList = @( + @{ + Name = 'SuffixSearchList' + Type = 'Array' + }, + @{ + Name = 'UseDevolution' + Type = 'Boolean' + }, + @{ + Name = 'DevolutionLevel' + Type = 'Integer' + } + ) +} diff --git a/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_DnsClientGlobalSetting/DSC_DnsClientGlobalSetting.psm1 b/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_DnsClientGlobalSetting/DSC_DnsClientGlobalSetting.psm1 new file mode 100644 index 0000000..fc86745 --- /dev/null +++ b/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_DnsClientGlobalSetting/DSC_DnsClientGlobalSetting.psm1 @@ -0,0 +1,267 @@ +$modulePath = Join-Path -Path (Split-Path -Path (Split-Path -Path $PSScriptRoot -Parent) -Parent) -ChildPath 'Modules' + +# Import the Networking Common Modules +Import-Module -Name (Join-Path -Path $modulePath ` + -ChildPath (Join-Path -Path 'NetworkingDsc.Common' ` + -ChildPath 'NetworkingDsc.Common.psm1')) + +Import-Module -Name (Join-Path -Path $modulePath -ChildPath 'DscResource.Common') + +# Import Localization Strings +$script:localizedData = Get-LocalizedData -DefaultUICulture 'en-US' + +<# + This is an array of all the parameters used by this resource. +#> +$resourceData = Import-LocalizedData ` + -BaseDirectory $PSScriptRoot ` + -FileName 'DSC_DnsClientGlobalSetting.data.psd1' + +# This must be a script parameter so that it is accessible +$script:parameterList = $resourceData.ParameterList + +<# + .SYNOPSIS + Returns the current DNS Client Global Settings. + + .PARAMETER IsSingleInstance + Specifies the resource is a single instance, the value must be 'Yes'. +#> +function Get-TargetResource +{ + [CmdletBinding()] + [OutputType([System.Collections.Hashtable])] + param + ( + [Parameter(Mandatory = $true)] + [ValidateSet('Yes')] + [System.String] + $IsSingleInstance + ) + + Write-Verbose -Message ( @( + "$($MyInvocation.MyCommand): " + $($script:localizedData.GettingDnsClientGlobalSettingMessage) + ) -join '' ) + + # Get the current Dns Client Global Settings + $dnsClientGlobalSetting = Get-DnsClientGlobalSetting ` + -ErrorAction Stop + + # Generate the return object. + $returnValue = @{ + IsSingleInstance = 'Yes' + } + + foreach ($parameter in $script:parameterList) + { + $returnValue += @{ + $parameter.Name = $dnsClientGlobalSetting.$($parameter.name) + } + } # foreach + + return $returnValue +} # Get-TargetResource + +<# + .SYNOPSIS + Sets the DNS Client Global Settings. + + .PARAMETER IsSingleInstance + Specifies the resource is a single instance, the value must be 'Yes'. + + .PARAMETER SuffixSearchList + Specifies a list of global suffixes that can be used in the specified order by the DNS client + for resolving the IP address of the computer name. + + .PARAMETER UseDevolution. + Specifies that devolution is activated. + + .PARAMETER DevolutionLevel + Specifies the number of labels up to which devolution should occur. +#> +function Set-TargetResource +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [ValidateSet('Yes')] + [System.String] + $IsSingleInstance, + + [Parameter()] + [System.String[]] + $SuffixSearchList, + + [Parameter()] + [System.Boolean] + $UseDevolution, + + [Parameter()] + [System.Uint32] + $DevolutionLevel + ) + + Write-Verbose -Message ( @( + "$($MyInvocation.MyCommand): " + $($script:localizedData.SettingDnsClientGlobalSettingMessage) + ) -join '' ) + + # Get the current Dns Client Global Settings + $dnsClientGlobalSetting = Get-DnsClientGlobalSetting ` + -ErrorAction Stop + + # Generate a list of parameters that will need to be changed. + $changeParameters = @{} + + foreach ($parameter in $script:parameterList) + { + $parameterSourceValue = $dnsClientGlobalSetting.$($parameter.name) + $parameterNewValue = (Get-Variable -Name ($parameter.name)).Value + + if ($PSBoundParameters.ContainsKey($parameter.Name) ` + -and (Compare-Object -ReferenceObject $parameterSourceValue -DifferenceObject $parameterNewValue -SyncWindow 0)) + { + $changeParameters += @{ + $($parameter.name) = $parameterNewValue + } + + Write-Verbose -Message ( @( + "$($MyInvocation.MyCommand): " + $($script:localizedData.DnsClientGlobalSettingUpdateParameterMessage) ` + -f $parameter.Name,($parameterNewValue -join ',') + ) -join '' ) + } # if + } # foreach + + if ($changeParameters.Count -gt 0) + { + # Update any parameters that were identified as different + $null = Set-DnsClientGlobalSetting ` + @ChangeParameters ` + -ErrorAction Stop + + Write-Verbose -Message ( @( + "$($MyInvocation.MyCommand): " + $($script:localizedData.DnsClientGlobalSettingUpdatedMessage) + ) -join '' ) + } # if +} # Set-TargetResource + +<# + .SYNOPSIS + Tests the state of DNS Client Global Settings. + + .PARAMETER IsSingleInstance + Specifies the resource is a single instance, the value must be 'Yes'. + + .PARAMETER SuffixSearchList + Specifies a list of global suffixes that can be used in the specified order by the DNS client + for resolving the IP address of the computer name. + + .PARAMETER UseDevolution. + Specifies that devolution is activated. + + .PARAMETER DevolutionLevel + Specifies the number of labels up to which devolution should occur. +#> +function Test-TargetResource +{ + [CmdletBinding()] + [OutputType([System.Boolean])] + param + ( + [Parameter(Mandatory = $true)] + [ValidateSet('Yes')] + [System.String] + $IsSingleInstance, + + [Parameter()] + [System.String[]] + $SuffixSearchList, + + [Parameter()] + [System.Boolean] + $UseDevolution, + + [Parameter()] + [System.Uint32] + $DevolutionLevel + ) + + Write-Verbose -Message ( @( + "$($MyInvocation.MyCommand): " + $($script:localizedData.TestingDnsClientGlobalSettingMessage) + ) -join '' ) + + # Flag to signal whether settings are correct + $desiredConfigurationMatch = $true + + # Get the current Dns Client Global Settings + $dnsClientGlobalSetting = Get-DnsClientGlobalSetting ` + -ErrorAction Stop + + # Check each parameter + foreach ($parameter in $script:parameterList) + { + $parameterSourceValue = $dnsClientGlobalSetting.$($parameter.name) + $parameterNewValue = (Get-Variable -Name ($parameter.name)).Value + $parameterValueMatch = $true + + switch ($parameter.Type) + { + 'Integer' + { + # Perform a plain integer comparison. + if ($PSBoundParameters.ContainsKey($parameter.Name) -and $parameterSourceValue -ne $parameterNewValue) + { + $parameterValueMatch = $false + } + } + + 'Boolean' + { + # Perform a boolean comparison. + if ($PSBoundParameters.ContainsKey($parameter.Name) -and $parameterSourceValue -ne $parameterNewValue) + { + $parameterValueMatch = $false + } + } + + 'Array' + { + # Array comparison uses Compare-Object + if ([System.String]::IsNullOrEmpty($parameterSourceValue)) + { + $parameterSourceValue = @() + } + + if ([System.String]::IsNullOrEmpty($parameterNewValue)) + { + $parameterNewValue = @() + } + + if ($PSBoundParameters.ContainsKey($parameter.Name) ` + -and ((Compare-Object ` + -ReferenceObject $parameterSourceValue ` + -DifferenceObject $parameterNewValue -SyncWindow 0).Count -ne 0)) + { + $parameterValueMatch = $false + } + } + } + if ($parameterValueMatch -eq $false) + { + Write-Verbose -Message ( @( "$($MyInvocation.MyCommand): " + $($script:localizedData.DnsClientGlobalSettingParameterNeedsUpdateMessage) ` + -f $parameter.Name, ($parameterSourceValue -join ','), ($parameterNewValue -join ',') + ) -join '') + $desiredConfigurationMatch = $false + } + } # foreach + + return $desiredConfigurationMatch +} # Test-TargetResource + +Export-ModuleMember -Function *-TargetResource diff --git a/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_DnsClientGlobalSetting/DSC_DnsClientGlobalSetting.schema.mof b/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_DnsClientGlobalSetting/DSC_DnsClientGlobalSetting.schema.mof new file mode 100644 index 0000000..feaae16 --- /dev/null +++ b/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_DnsClientGlobalSetting/DSC_DnsClientGlobalSetting.schema.mof @@ -0,0 +1,8 @@ +[ClassVersion("1.0.0.0"), FriendlyName("DnsClientGlobalSetting")] +class DSC_DnsClientGlobalSetting : OMI_BaseResource +{ + [Key, Description("Specifies the resource is a single instance, the value must be 'Yes'."), ValueMap{"Yes"}, Values{"Yes"}] String IsSingleInstance; + [Write, Description("Specifies a list of global suffixes that can be used in the specified order by the DNS client for resolving the IP address of the computer name.")] String SuffixSearchList[]; + [Write, Description("Specifies that devolution is activated.")] Boolean UseDevolution; + [Write, Description("Specifies the number of labels up to which devolution should occur.")] Uint32 DevolutionLevel; +}; diff --git a/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_DnsClientGlobalSetting/README.MD b/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_DnsClientGlobalSetting/README.MD new file mode 100644 index 0000000..bc84bb3 --- /dev/null +++ b/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_DnsClientGlobalSetting/README.MD @@ -0,0 +1,3 @@ +# Description + +This resource is used to control global DNS client settings for a node. diff --git a/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_DnsClientGlobalSetting/en-US/DSC_DnsClientGlobalSetting.strings.psd1 b/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_DnsClientGlobalSetting/en-US/DSC_DnsClientGlobalSetting.strings.psd1 new file mode 100644 index 0000000..572e86a --- /dev/null +++ b/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_DnsClientGlobalSetting/en-US/DSC_DnsClientGlobalSetting.strings.psd1 @@ -0,0 +1,10 @@ +# Localized resources for DSC_DnsClientGlobalSetting + +ConvertFrom-StringData @' + GettingDnsClientGlobalSettingMessage = Getting DNS Client Global Settings. + SettingDnsClientGlobalSettingMessage = Setting DNS Client Global Settings. + DnsClientGlobalSettingUpdateParameterMessage = Setting DNS Client Global Settings parameter {0} to "{1}". + DnsClientGlobalSettingUpdatedMessage = Setting DNS Client Global Settings updated. + TestingDnsClientGlobalSettingMessage = Testing DNS Client Global Settings. + DnsClientGlobalSettingParameterNeedsUpdateMessage = DNS Client Global Setting "{0}" is "{1}" but should be "{2}". Change required. +'@ diff --git a/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_DnsClientGlobalSetting/en-US/about_DnsClientGlobalSetting.help.txt b/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_DnsClientGlobalSetting/en-US/about_DnsClientGlobalSetting.help.txt new file mode 100644 index 0000000..7a79cd6 --- /dev/null +++ b/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_DnsClientGlobalSetting/en-US/about_DnsClientGlobalSetting.help.txt @@ -0,0 +1,64 @@ +.NAME + DnsClientGlobalSetting + +.DESCRIPTION + This resource is used to control global DNS client settings for a node. + +.PARAMETER IsSingleInstance + Key - String + Allowed values: Yes + Specifies the resource is a single instance, the value must be 'Yes'. + +.PARAMETER SuffixSearchList + Write - StringArray + Specifies a list of global suffixes that can be used in the specified order by the DNS client for resolving the IP address of the computer name. + +.PARAMETER UseDevolution + Write - Boolean + Specifies that devolution is activated. + +.PARAMETER DevolutionLevel + Write - UInt32 + Specifies the number of labels up to which devolution should occur. + +.EXAMPLE 1 + +Configure only contoso.com for the DNS Suffix. + +Configuration DnsClientGlobalSetting_ConfigureSuffixSearchListSingle_Config +{ + Import-DscResource -Module NetworkingDsc + + Node localhost + { + DnsClientGlobalSetting ConfigureSuffixSearchListSingle + { + IsSingleInstance = 'Yes' + SuffixSearchList = 'contoso.com' + UseDevolution = $true + DevolutionLevel = 0 + } + } +} + +.EXAMPLE 2 + +Configure fabrikam.com and fourthcoffee.com for the DNS SuffixSearchList. + +Configuration DnsClientGlobalSetting_ConfigureSuffixSearchListMultiple_Config +{ + Import-DscResource -Module NetworkingDsc + + Node localhost + { + DnsClientGlobalSetting ConfigureSuffixSearchListMultiple + { + IsSingleInstance = 'Yes' + SuffixSearchList = ('fabrikam.com', 'fourthcoffee.com') + UseDevolution = $true + DevolutionLevel = 0 + } + } +} + + diff --git a/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_DnsConnectionSuffix/DSC_DnsConnectionSuffix.psm1 b/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_DnsConnectionSuffix/DSC_DnsConnectionSuffix.psm1 new file mode 100644 index 0000000..cb6aad4 --- /dev/null +++ b/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_DnsConnectionSuffix/DSC_DnsConnectionSuffix.psm1 @@ -0,0 +1,268 @@ +$modulePath = Join-Path -Path (Split-Path -Path (Split-Path -Path $PSScriptRoot -Parent) -Parent) -ChildPath 'Modules' + +# Import the Networking Common Modules +Import-Module -Name (Join-Path -Path $modulePath ` + -ChildPath (Join-Path -Path 'NetworkingDsc.Common' ` + -ChildPath 'NetworkingDsc.Common.psm1')) + +Import-Module -Name (Join-Path -Path $modulePath -ChildPath 'DscResource.Common') + +# Import Localization Strings +$script:localizedData = Get-LocalizedData -DefaultUICulture 'en-US' + +<# + .SYNOPSIS + Returns the current DNS Connection Suffix for an interface. + + .PARAMETER InterfaceAlias + Alias of the network interface for which the DNS server address is set. + + .PARAMETER ConnectionSpecificSuffix + DNS connection-specific suffix to assign to the network interface. + + .PARAMETER RegisterThisConnectionsAddress + Specifies that the IP address for this connection is to be registered. + + .PARAMETER UseSuffixWhenRegistering + Specifies that this host name and the connection specific suffix for this connection are to + be registered. + + .PARAMETER Ensure + Ensure that the network interface connection-specific suffix is present or not. +#> +function Get-TargetResource +{ + [CmdletBinding()] + [OutputType([System.Collections.Hashtable])] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $InterfaceAlias, + + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $ConnectionSpecificSuffix, + + [Parameter()] + [System.Boolean] + $RegisterThisConnectionsAddress = $true, + + [Parameter()] + [System.Boolean] + $UseSuffixWhenRegistering = $false, + + [Parameter()] + [ValidateSet('Present', 'Absent')] + [System.String] + $Ensure = 'Present' + ) + + $dnsClient = Get-DnsClient -InterfaceAlias $InterfaceAlias -ErrorAction SilentlyContinue + + $targetResource = @{ + InterfaceAlias = $dnsClient.InterfaceAlias + ConnectionSpecificSuffix = $dnsClient.ConnectionSpecificSuffix + RegisterThisConnectionsAddress = $dnsClient.RegisterThisConnectionsAddress + UseSuffixWhenRegistering = $dnsClient.UseSuffixWhenRegistering + } + + if ($Ensure -eq 'Present') + { + # Test to see if the connection-specific suffix matches + Write-Verbose -Message ($script:localizedData.CheckingConnectionSuffix -f $ConnectionSpecificSuffix) + + if ($dnsClient.ConnectionSpecificSuffix -eq $ConnectionSpecificSuffix) + { + $Ensure = 'Present' + } + else + { + $Ensure = 'Absent' + } + } + else + { + # ($Ensure -eq 'Absent'). Test to see if there is a connection-specific suffix + Write-Verbose -Message ($script:localizedData.CheckingConnectionSuffix -f '') + + if ([System.String]::IsNullOrEmpty($dnsClient.ConnectionSpecificSuffix)) + { + $Ensure = 'Absent' + } + else + { + $Ensure = 'Present' + } + } + + $targetResource['Ensure'] = $Ensure + + return $targetResource +} + +<# + .SYNOPSIS + Sets the DNS Connection Suffix for an interface. + + .PARAMETER InterfaceAlias + Alias of the network interface for which the DNS server address is set. + + .PARAMETER ConnectionSpecificSuffix + DNS connection-specific suffix to assign to the network interface. + + .PARAMETER RegisterThisConnectionsAddress + Specifies that the IP address for this connection is to be registered. + + .PARAMETER UseSuffixWhenRegistering + Specifies that this host name and the connection specific suffix for this connection are to + be registered. + + .PARAMETER Ensure + Ensure that the network interface connection-specific suffix is present or not. +#> +function Set-TargetResource +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $InterfaceAlias, + + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $ConnectionSpecificSuffix, + + [Parameter()] + [System.Boolean] + $RegisterThisConnectionsAddress = $true, + + [Parameter()] + [System.Boolean] + $UseSuffixWhenRegistering = $false, + + [Parameter()] + [ValidateSet('Present', 'Absent')] + [System.String] + $Ensure = 'Present' + ) + + $setDnsClientParams = @{ + InterfaceAlias = $InterfaceAlias + RegisterThisConnectionsAddress = $RegisterThisConnectionsAddress + UseSuffixWhenRegistering = $UseSuffixWhenRegistering + } + + if ($Ensure -eq 'Present') + { + $setDnsClientParams['ConnectionSpecificSuffix'] = $ConnectionSpecificSuffix + + Write-Verbose -Message ($script:localizedData.SettingConnectionSuffix ` + -f $ConnectionSpecificSuffix, $InterfaceAlias) + } + else + { + $setDnsClientParams['ConnectionSpecificSuffix'] = '' + + Write-Verbose -Message ($script:localizedData.RemovingConnectionSuffix ` + -f $ConnectionSpecificSuffix, $InterfaceAlias) + } + + Set-DnsClient @setDnsClientParams +} + +<# + .SYNOPSIS + Tests the current state of a DNS Connection Suffix for an interface. + + .PARAMETER InterfaceAlias + Alias of the network interface for which the DNS server address is set. + + .PARAMETER ConnectionSpecificSuffix + DNS connection-specific suffix to assign to the network interface. + + .PARAMETER RegisterThisConnectionsAddress + Specifies that the IP address for this connection is to be registered. + + .PARAMETER UseSuffixWhenRegistering + Specifies that this host name and the connection specific suffix for this connection are to + be registered. + + .PARAMETER Ensure + Ensure that the network interface connection-specific suffix is present or not. +#> +function Test-TargetResource +{ + [CmdletBinding()] + [OutputType([System.Boolean])] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $InterfaceAlias, + + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $ConnectionSpecificSuffix, + + [Parameter()] + [System.Boolean] + $RegisterThisConnectionsAddress = $true, + + [Parameter()] + [System.Boolean] + $UseSuffixWhenRegistering = $false, + + [Parameter()] + [ValidateSet('Present', 'Absent')] + [System.String] + $Ensure = 'Present' + ) + + $targetResource = Get-TargetResource @PSBoundParameters + $inDesiredState = $true + + if ($targetResource.Ensure -ne $Ensure) + { + Write-Verbose -Message ($script:localizedData.PropertyMismatch ` + -f 'Ensure', $Ensure, $targetResource.Ensure) + + $inDesiredState = $false + } + + if ($targetResource.RegisterThisConnectionsAddress -ne $RegisterThisConnectionsAddress) + { + Write-Verbose -Message ($script:localizedData.PropertyMismatch ` + -f 'RegisterThisConnectionsAddress', $RegisterThisConnectionsAddress, $targetResource.RegisterThisConnectionsAddress) + + $inDesiredState = $false + } + + if ($targetResource.UseSuffixWhenRegistering -ne $UseSuffixWhenRegistering) + { + Write-Verbose -Message ($script:localizedData.PropertyMismatch ` + -f 'UseSuffixWhenRegistering', $UseSuffixWhenRegistering, $targetResource.UseSuffixWhenRegistering) + + $inDesiredState = $false + } + + if ($inDesiredState) + { + Write-Verbose -Message $script:localizedData.ResourceInDesiredState + } + else + { + Write-Verbose -Message $script:localizedData.ResourceNotInDesiredState + } + + return $inDesiredState +} + +Export-ModuleMember -function *-TargetResource diff --git a/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_DnsConnectionSuffix/DSC_DnsConnectionSuffix.schema.mof b/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_DnsConnectionSuffix/DSC_DnsConnectionSuffix.schema.mof new file mode 100644 index 0000000..471be54 --- /dev/null +++ b/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_DnsConnectionSuffix/DSC_DnsConnectionSuffix.schema.mof @@ -0,0 +1,9 @@ +[ClassVersion("1.0.0"), FriendlyName("DnsConnectionSuffix")] +class DSC_DnsConnectionSuffix : OMI_BaseResource +{ + [Key, Description("Alias of the network interface for which the DNS server address is set.")] String InterfaceAlias; + [Required, Description("DNS connection-specific suffix to assign to the network interface.")] String ConnectionSpecificSuffix; + [Write, Description("Specifies that the IP address for this connection is to be registered.")] Boolean RegisterThisConnectionsAddress; + [Write, Description("Specifies that this host name and the connection specific suffix for this connection are to be registered.")] Boolean UseSuffixWhenRegistering; + [Write, Description("Ensure that the network interface connection-specific suffix is present or not."), ValueMap{"Present", "Absent"},Values{"Present", "Absent"}] String Ensure; +}; diff --git a/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_DnsConnectionSuffix/README.MD b/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_DnsConnectionSuffix/README.MD new file mode 100644 index 0000000..e279908 --- /dev/null +++ b/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_DnsConnectionSuffix/README.MD @@ -0,0 +1,3 @@ +# Description + +This resource is used to control interface-specific DNS client configurations on a node. diff --git a/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_DnsConnectionSuffix/en-US/DSC_DnsConnectionSuffix.strings.psd1 b/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_DnsConnectionSuffix/en-US/DSC_DnsConnectionSuffix.strings.psd1 new file mode 100644 index 0000000..838f53a --- /dev/null +++ b/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_DnsConnectionSuffix/en-US/DSC_DnsConnectionSuffix.strings.psd1 @@ -0,0 +1,10 @@ +# Localized resources for DSC_DnsConnectionSuffix + +ConvertFrom-StringData @' + PropertyMismatch = Property '{0}' does NOT match. Expected '{1}', actual '{2}'. + CheckingConnectionSuffix = Checking connection suffix matches '{0}'. + ResourceInDesiredState = Resource is in the desired state. + ResourceNotInDesiredState = Resource is NOT in the desired state. + SettingConnectionSuffix = Setting connection suffix '{0}' on interface '{1}'. + RemovingConnectionSuffix = Removing connection suffix '{0}' on interface '{1}'. +'@ diff --git a/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_DnsConnectionSuffix/en-US/about_DnsConnectionSuffix.help.txt b/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_DnsConnectionSuffix/en-US/about_DnsConnectionSuffix.help.txt new file mode 100644 index 0000000..834fb0a --- /dev/null +++ b/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_DnsConnectionSuffix/en-US/about_DnsConnectionSuffix.help.txt @@ -0,0 +1,47 @@ +.NAME + DnsConnectionSuffix + +.DESCRIPTION + This resource is used to control interface-specific DNS client configurations on a node. + +.PARAMETER InterfaceAlias + Key - String + Alias of the network interface for which the DNS server address is set. + +.PARAMETER ConnectionSpecificSuffix + Required - String + DNS connection-specific suffix to assign to the network interface. + +.PARAMETER RegisterThisConnectionsAddress + Write - Boolean + Specifies that the IP address for this connection is to be registered. + +.PARAMETER UseSuffixWhenRegistering + Write - Boolean + Specifies that this host name and the connection specific suffix for this connection are to be registered. + +.PARAMETER Ensure + Write - String + Allowed values: Present, Absent + Ensure that the network interface connection-specific suffix is present or not. + +.EXAMPLE 1 + +This configuration will set a DNS connection-specific suffix on a network interface that +is identified by its alias. + +Configuration DnsConnectionSuffix_AddSpecificSuffix_Config +{ + Import-DscResource -Module NetworkingDsc + + Node localhost + { + DnsConnectionSuffix AddSpecificSuffix + { + InterfaceAlias = 'Ethernet' + ConnectionSpecificSuffix = 'contoso.com' + } + } +} + + diff --git a/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_DnsServerAddress/DSC_DnsServerAddress.psm1 b/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_DnsServerAddress/DSC_DnsServerAddress.psm1 new file mode 100644 index 0000000..9980c3b --- /dev/null +++ b/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_DnsServerAddress/DSC_DnsServerAddress.psm1 @@ -0,0 +1,327 @@ +$modulePath = Join-Path -Path (Split-Path -Path (Split-Path -Path $PSScriptRoot -Parent) -Parent) -ChildPath 'Modules' + +# Import the Networking Common Modules +Import-Module -Name (Join-Path -Path $modulePath ` + -ChildPath (Join-Path -Path 'NetworkingDsc.Common' ` + -ChildPath 'NetworkingDsc.Common.psm1')) + +Import-Module -Name (Join-Path -Path $modulePath -ChildPath 'DscResource.Common') + +# Import Localization Strings +$script:localizedData = Get-LocalizedData -DefaultUICulture 'en-US' + +<# + .SYNOPSIS + Returns the current DNS Server Addresses for an interface. + + .PARAMETER InterfaceAlias + Alias of the network interface for which the DNS server address is set. + + .PARAMETER AddressFamily + IP address family. + + .PARAMETER Address + The desired DNS Server address(es). Exclude to enable DHCP. +#> +function Get-TargetResource +{ + [CmdletBinding()] + [OutputType([System.Collections.Hashtable])] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [String] + $InterfaceAlias, + + [Parameter(Mandatory = $true)] + [ValidateSet('IPv4', 'IPv6')] + [String] + $AddressFamily, + + [Parameter()] + [ValidateNotNullOrEmpty()] + [String[]] + $Address + ) + + Write-Verbose -Message ( @( "$($MyInvocation.MyCommand): " + $($script:localizedData.GettingDnsServerAddressesMessage) + ) -join '') + + # Remove the parameters we don't want to splat + $null = $PSBoundParameters.Remove('Address') + + # Get the current DNS Server Addresses based on the parameters given. + [String[]] $currentAddress = Get-DnsClientServerStaticAddress ` + @PSBoundParameters ` + -ErrorAction Stop + + $returnValue = @{ + Address = $currentAddress + AddressFamily = $AddressFamily + InterfaceAlias = $InterfaceAlias + } + + return $returnValue +} + +<# + .SYNOPSIS + Sets the DNS Server Address for an interface. + + .PARAMETER InterfaceAlias + Alias of the network interface for which the DNS server address is set. + + .PARAMETER AddressFamily + IP address family. + + .PARAMETER Address + The desired DNS Server address(es). Exclude to enable DHCP. + + .PARAMETER Validate + Requires that the DNS Server addresses be validated if they are updated. + It will cause the resource to throw a 'A general error occurred that is not covered by a more + specific error code.' error if set to True and specified DNS Servers are not accessible. +#> +function Set-TargetResource +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [String] + $InterfaceAlias, + + [Parameter(Mandatory = $true)] + [ValidateSet('IPv4', 'IPv6')] + [String] + $AddressFamily, + + [Parameter()] + [ValidateNotNullOrEmpty()] + [String[]] + $Address, + + [Parameter()] + [Boolean] + $Validate = $false + ) + + Write-Verbose -Message ( @("$($MyInvocation.MyCommand): " + $($script:localizedData.ApplyingDnsServerAddressesMessage) + ) -join '') + + # If address not passed, set to an empty array + if (-not $PSBoundParameters.ContainsKey('Address')) + { + [String[]] $Address = @() + } + + # Remove the parameters we don't want to splat + $null = $PSBoundParameters.Remove('Address') + $null = $PSBoundParameters.Remove('Validate') + + # Get the current DNS Server Addresses based on the parameters given. + [String[]] $currentAddress = @(Get-DnsClientServerStaticAddress ` + @PSBoundParameters ` + -ErrorAction Stop) + + # Check if the Server addresses are the same as the desired addresses. + [Boolean] $addressDifferent = (@(Compare-Object ` + -ReferenceObject $currentAddress ` + -DifferenceObject $Address ` + -SyncWindow 0).Length -gt 0) + + if ($addressDifferent) + { + $dnsServerAddressSplat = @{ + InterfaceAlias = $InterfaceAlias + } + + if ($Address.Count -eq 0) + { + # Reset the DNS server address to DHCP + $dnsServerAddressSplat += @{ + ResetServerAddresses = $true + } + + Set-DnsClientServerAddress @dnsServerAddressSplat ` + -ErrorAction Stop + + Write-Verbose -Message ( @( "$($MyInvocation.MyCommand): " + $($script:localizedData.DNSServersHaveBeenSetToDHCPMessage) + ) -join '' ) + } + else + { + # Set the DNS server address to static + $dnsServerAddressSplat += @{ + Address = $Address + Validate = $Validate + } + + Set-DnsClientServerAddress @dnsServerAddressSplat ` + -ErrorAction Stop + + Write-Verbose -Message ( @( "$($MyInvocation.MyCommand): " + $($script:localizedData.DNSServersHaveBeenSetCorrectlyMessage) + ) -join '' ) + } + } + else + { + # Test will return true in this case + Write-Verbose -Message ( @( "$($MyInvocation.MyCommand): " + $($script:localizedData.DNSServersAlreadySetMessage) + ) -join '' ) + } +} + +<# + .SYNOPSIS + Tests the current state of a DNS Server Address for an interface. + + .PARAMETER InterfaceAlias + Alias of the network interface for which the DNS server address is set. + + .PARAMETER AddressFamily + IP address family. + + .PARAMETER Address + The desired DNS Server address(es). Exclude to enable DHCP. + + .PARAMETER Validate + Requires that the DNS Server addresses be validated if they are updated. + It will cause the resource to throw a 'A general error occurred that is not covered by a more + specific error code.' error if set to True and specified DNS Servers are not accessible. +#> +function Test-TargetResource +{ + [CmdletBinding()] + [OutputType([System.Boolean])] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [String] + $InterfaceAlias, + + [Parameter(Mandatory = $true)] + [ValidateSet('IPv4', 'IPv6')] + [String] + $AddressFamily, + + [Parameter()] + [ValidateNotNullOrEmpty()] + [String[]] + $Address, + + [Parameter()] + [Boolean] + $Validate = $false + ) + # Flag to signal whether settings are correct + $desiredConfigurationMatch = $true + + Write-Verbose -Message ( @( "$($MyInvocation.MyCommand): " + $($script:localizedData.CheckingDnsServerAddressesMessage) + ) -join '' ) + + # Validate the Address passed or set to empty array if not passed + if ($PSBoundParameters.ContainsKey('Address')) + { + foreach ($ServerAddress in $Address) + { + Assert-ResourceProperty ` + -Address $ServerAddress ` + -AddressFamily $AddressFamily ` + -InterfaceAlias $InterfaceAlias + } # foreach + } + else + { + [String[]] $Address = @() + } # if + + # Remove the parameters we don't want to splat + $null = $PSBoundParameters.Remove('Address') + $null = $PSBoundParameters.Remove('Validate') + + # Get the current DNS Server Addresses based on the parameters given. + [String[]] $currentAddress = @(Get-DnsClientServerStaticAddress ` + @PSBoundParameters ` + -ErrorAction Stop) + + # Check if the Server addresses are the same as the desired addresses. + [Boolean] $addressDifferent = (@(Compare-Object ` + -ReferenceObject $currentAddress ` + -DifferenceObject $Address ` + -SyncWindow 0).Length -gt 0) + + if ($addressDifferent) + { + $desiredConfigurationMatch = $false + + Write-Verbose -Message ( @( "$($MyInvocation.MyCommand): " + $($script:localizedData.DNSServersNotCorrectMessage) ` + -f ($Address -join ','),($currentAddress -join ',') + ) -join '' ) + } + else + { + # Test will return true in this case + Write-Verbose -Message ( @( "$($MyInvocation.MyCommand): " + $($script:localizedData.DNSServersSetCorrectlyMessage) + ) -join '' ) + } + return $desiredConfigurationMatch +} + +<# + .SYNOPSIS + Checks the Address details are valid and do not conflict with Address family. + Ensures interface exists. If any problems are detected an exception will be thrown. + + .PARAMETER InterfaceAlias + Alias of the network interface for which the DNS server address is set. + + .PARAMETER AddressFamily + IP address family. + + .PARAMETER Address + The desired DNS Server address. Set to empty to enable DHCP. +#> +function Assert-ResourceProperty +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [String] + $InterfaceAlias, + + [Parameter(Mandatory = $true)] + [ValidateSet('IPv4', 'IPv6')] + [String] + $AddressFamily, + + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [String] + $Address + ) + + if ( -not (Get-NetAdapter | Where-Object -Property Name -EQ $InterfaceAlias )) + { + New-InvalidArgumentException ` + -Message ($script:localizedData.InterfaceNotAvailableError -f $InterfaceAlias) ` + -ArgumentName 'InterfaceAlias' + } + + Assert-IPAddress -Address $Address -AddressFamily $AddressFamily +} # Assert-ResourceProperty + +Export-ModuleMember -function *-TargetResource diff --git a/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_DnsServerAddress/DSC_DnsServerAddress.schema.mof b/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_DnsServerAddress/DSC_DnsServerAddress.schema.mof new file mode 100644 index 0000000..7ebd6b1 --- /dev/null +++ b/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_DnsServerAddress/DSC_DnsServerAddress.schema.mof @@ -0,0 +1,8 @@ +[ClassVersion("1.0.0"), FriendlyName("DnsServerAddress")] +class DSC_DnsServerAddress : OMI_BaseResource +{ + [Key, Description("Alias of the network interface for which the DNS server address is set.")] string InterfaceAlias; + [Key, Description("IP address family."), ValueMap{"IPv4", "IPv6"},Values{"IPv4", "IPv6"}] string AddressFamily; + [Write, Description("The desired DNS Server address(es). Exclude to enable DHCP.")] string Address[]; + [Write, Description("Requires that the DNS Server addresses be validated if they are updated. It will cause the resource to throw a 'A general error occurred that is not covered by a more specific error code.' error if set to True and specified DNS Servers are not accessible.")] boolean Validate; +}; diff --git a/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_DnsServerAddress/README.MD b/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_DnsServerAddress/README.MD new file mode 100644 index 0000000..cbccb2f --- /dev/null +++ b/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_DnsServerAddress/README.MD @@ -0,0 +1,3 @@ +# Description + +This resource is used to control a node's DNS Server address(s). diff --git a/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_DnsServerAddress/en-US/DSC_DnsServerAddress.strings.psd1 b/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_DnsServerAddress/en-US/DSC_DnsServerAddress.strings.psd1 new file mode 100644 index 0000000..b331456 --- /dev/null +++ b/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_DnsServerAddress/en-US/DSC_DnsServerAddress.strings.psd1 @@ -0,0 +1,13 @@ +# Localized resources for DSC_DnsServerAddress + +ConvertFrom-StringData @' + GettingDnsServerAddressesMessage = Getting the DNS server addresses. + ApplyingDnsServerAddressesMessage = Applying the DNS server addresses. + DNSServersSetCorrectlyMessage = DNS server addresses are set correctly. + DNSServersAlreadySetMessage = DNS server addresses are already set correctly. + CheckingDnsServerAddressesMessage = Checking the DNS server addresses. + DNSServersNotCorrectMessage = DNS server addresses are not correct. Expected "{0}", actual "{1}". + DNSServersHaveBeenSetCorrectlyMessage = DNS server addresses were set to the desired state. + DNSServersHaveBeenSetToDHCPMessage = DNS server addresses were set to the desired state of DHCP. + InterfaceNotAvailableError = Interface "{0}" is not available. Please select a valid interface and try again. +'@ diff --git a/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_DnsServerAddress/en-US/about_DnsServerAddress.help.txt b/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_DnsServerAddress/en-US/about_DnsServerAddress.help.txt new file mode 100644 index 0000000..e6a9681 --- /dev/null +++ b/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_DnsServerAddress/en-US/about_DnsServerAddress.help.txt @@ -0,0 +1,89 @@ +.NAME + DnsServerAddress + +.DESCRIPTION + This resource is used to control a node's DNS Server address(s). + +.PARAMETER InterfaceAlias + Key - String + Alias of the network interface for which the DNS server address is set. + +.PARAMETER AddressFamily + Key - String + Allowed values: IPv4, IPv6 + IP address family. + +.PARAMETER Address + Write - StringArray + The desired DNS Server address(es). Exclude to enable DHCP. + +.PARAMETER Validate + Write - Boolean + Requires that the DNS Server addresses be validated if they are updated. It will cause the resource to throw a 'A general error occurred that is not covered by a more specific error code.' error if set to True and specified DNS Servers are not accessible. + +.EXAMPLE 1 + +Configure DNS Server for the Ethernet adapter. + +Configuration DnsServerAddress_Config +{ + Import-DscResource -Module NetworkingDsc + + Node localhost + { + DnsServerAddress DnsServerAddress + { + Address = '127.0.0.1' + InterfaceAlias = 'Ethernet' + AddressFamily = 'IPv4' + Validate = $true + } + } +} + +.EXAMPLE 2 + +Configure primary and secondary DNS Server addresses on the Ethernet adapter. + +Configuration DnsServerAddress_PrimaryAndSecondary_Config +{ + Import-DscResource -Module NetworkingDsc + + Node localhost + { + DnsServerAddress PrimaryAndSecondary + { + Address = '10.0.0.2','10.0.0.40' + InterfaceAlias = 'Ethernet' + AddressFamily = 'IPv4' + Validate = $true + } + } +} + +.EXAMPLE 3 + +Enabling DHCP for the IPv4 Address and DNS on the adapter with alias 'Ethernet'. + +Configuration DnsServerAddress_EnableDHCP_Config +{ + Import-DscResource -Module NetworkingDsc + + Node localhost + { + NetIPInterface EnableDhcp + { + InterfaceAlias = 'Ethernet' + AddressFamily = 'IPv4' + Dhcp = 'Enabled' + } + + DnsServerAddress EnableDhcpDNS + { + InterfaceAlias = 'Ethernet' + AddressFamily = 'IPv4' + } + } +} + + diff --git a/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_Firewall/DSC_Firewall.data.psd1 b/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_Firewall/DSC_Firewall.data.psd1 new file mode 100644 index 0000000..73bc7da --- /dev/null +++ b/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_Firewall/DSC_Firewall.data.psd1 @@ -0,0 +1,36 @@ +@{ + ParameterList = @( + @{ Name = 'Name'; Variable = 'FirewallRule'; Type = 'String' } + @{ Name = 'DisplayName'; Variable = 'FirewallRule'; Type = 'String' } + @{ Name = 'Group'; Variable = 'FirewallRule'; Type = 'String' } + @{ Name = 'DisplayGroup'; Variable = 'FirewallRule'; Type = '' } + @{ Name = 'Enabled'; Variable = 'FirewallRule'; Type = 'String' } + @{ Name = 'Action'; Variable = 'FirewallRule'; Type = 'String' } + @{ Name = 'Profile'; Variable = 'FirewallRule'; Type = 'Array'; Delimiter = ', ' } + @{ Name = 'Direction'; Variable = 'FirewallRule'; Type = 'String' } + @{ Name = 'Description'; Variable = 'FirewallRule'; Type = 'String' } + @{ Name = 'RemotePort'; Variable = 'properties'; Property = 'PortFilters'; Type = 'Array' } + @{ Name = 'LocalPort'; Variable = 'properties'; Property = 'PortFilters'; Type = 'Array' } + @{ Name = 'Protocol'; Variable = 'properties'; Property = 'PortFilters'; Type = 'String' } + @{ Name = 'Program'; Variable = 'properties'; Property = 'ApplicationFilters'; Type = 'String' } + @{ Name = 'Service'; Variable = 'properties'; Property = 'ServiceFilters'; Type = 'String' } + @{ Name = 'Authentication'; Variable = 'properties'; Property = 'SecurityFilters'; Type = 'String' } + @{ Name = 'Encryption'; Variable = 'properties'; Property = 'SecurityFilters'; Type = 'String' } + @{ Name = 'InterfaceAlias'; Variable = 'properties'; Property = 'InterfaceFilters'; Type = 'Array' } + @{ Name = 'InterfaceType'; Variable = 'properties'; Property = 'InterfaceTypeFilters'; Type = 'String' } + @{ Name = 'LocalAddress'; Variable = 'properties'; Property = 'AddressFilters'; Type = 'ArrayIP' } + @{ Name = 'LocalUser'; Variable = 'properties'; Property = 'SecurityFilters'; Type = 'String' } + @{ Name = 'Package'; Variable = 'properties'; Property = 'ApplicationFilters'; Type = 'String' } + @{ Name = 'Platform'; Variable = 'FirewallRule'; Type = 'Array' } + @{ Name = 'RemoteAddress'; Variable = 'properties'; Property = 'AddressFilters'; Type = 'ArrayIP' } + @{ Name = 'RemoteMachine'; Variable = 'properties'; Property = 'SecurityFilters'; Type = 'String' } + @{ Name = 'RemoteUser'; Variable = 'properties'; Property = 'SecurityFilters'; Type = 'String' } + @{ Name = 'DynamicTransport'; Variable = 'properties'; Property = 'PortFilters'; Type = 'String' } + @{ Name = 'EdgeTraversalPolicy'; Variable = 'FirewallRule'; Type = 'String' } + @{ Name = 'IcmpType'; Variable = 'properties'; Property = 'PortFilters'; Type = 'Array' } + @{ Name = 'LocalOnlyMapping'; Variable = 'FirewallRule'; Type = 'Boolean' } + @{ Name = 'LooseSourceMapping'; Variable = 'FirewallRule'; Type = 'Boolean' } + @{ Name = 'OverrideBlockRules'; Variable = 'properties'; Property = 'SecurityFilters'; Type = 'Boolean' } + @{ Name = 'Owner'; Variable = 'FirewallRule'; Type = 'String' } + ) +} diff --git a/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_Firewall/DSC_Firewall.psm1 b/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_Firewall/DSC_Firewall.psm1 new file mode 100644 index 0000000..f5dd463 --- /dev/null +++ b/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_Firewall/DSC_Firewall.psm1 @@ -0,0 +1,1377 @@ +$modulePath = Join-Path -Path (Split-Path -Path (Split-Path -Path $PSScriptRoot -Parent) -Parent) -ChildPath 'Modules' + +# Import the Networking Common Modules +Import-Module -Name (Join-Path -Path $modulePath ` + -ChildPath (Join-Path -Path 'NetworkingDsc.Common' ` + -ChildPath 'NetworkingDsc.Common.psm1')) + +Import-Module -Name (Join-Path -Path $modulePath -ChildPath 'DscResource.Common') + +# Import Localization Strings +$script:localizedData = Get-LocalizedData -DefaultUICulture 'en-US' + +<# + This is an array of all the parameters used by this resource + It can be used by several of the functions to reduce the amount of code required + Each element contains 3 properties: + Name: The parameter name + Source: The source where the existing parameter can be pulled from + Type: This is the content type of the paramater (it is either array or string or blank) + A blank type means it will not be compared + data ParameterList + Delimiter: Only required for Profile parameter, because Get-NetFirewall rule doesn't + return the profile as an array, but a comma delimited string. Setting this value causes + the functions to first split the parameter into an array. +#> +$script:resourceData = Import-LocalizedData ` + -BaseDirectory $PSScriptRoot ` + -FileName 'DSC_Firewall.data.psd1' +$script:parameterList = $script:resourceData.ParameterList + +<# + .SYNOPSIS + Returns the current state of the Firewall Rule. + + .PARAMETER Name + Name of the firewall rule. +#> +function Get-TargetResource +{ + [CmdletBinding()] + [OutputType([System.Collections.Hashtable])] + param + ( + # Name of the Firewall Rule + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [String] + $Name + ) + + $ErrorActionPreference = 'Stop' + Write-Verbose -Message ( @( "$($MyInvocation.MyCommand): " + $($script:localizedData.GettingFirewallRuleMessage) -f $Name + ) -join '') + + Write-Verbose -Message ( @( "$($MyInvocation.MyCommand): " + $($script:localizedData.FindFirewallRuleMessage) -f $Name + ) -join '') + + $firewallRule = Get-FirewallRule -Name $Name + + if (-not $firewallRule) + { + Write-Verbose -Message ( @( "$($MyInvocation.MyCommand): " + $($script:localizedData.FirewallRuleDoesNotExistMessage) -f $Name + ) -join '') + return @{ + Ensure = 'Absent' + Name = $Name + } + } + + $properties = Get-FirewallRuleProperty -FirewallRule $firewallRule + + $result = @{ + Ensure = 'Present' + } + + <# + Populate the properties for get target resource by looping through + the parameter array list and adding the values to + #> + foreach ($parameter in $script:parameterList) + { + if ($parameter.Type -in @('Array', 'ArrayIP')) + { + $parameterValue = @(Get-FirewallPropertyValue ` + -FirewallRule $firewallRule ` + -Properties $properties ` + -Parameter $parameter) + if ($parameter.Delimiter) + { + $parameterValue = $parameterValue -split $parameter.Delimiter + } + + $result += @{ + $parameter.Name = $parameterValue + } + + Write-Verbose -Message ( @( "$($MyInvocation.MyCommand): " + $($script:localizedData.FirewallParameterValueMessage) -f ` + $Name, $parameter.Name, ($parameterValue -join ',') + ) -join '') + } + else + { + $parameterValue = Get-FirewallPropertyValue ` + -FirewallRule $firewallRule ` + -Properties $properties ` + -Parameter $parameter + + $result += @{ + $parameter.Name = $parameterValue + } + + Write-Verbose -Message ( @( "$($MyInvocation.MyCommand): " + $($script:localizedData.FirewallParameterValueMessage) -f ` + $Name, $parameter.Name, $parameterValue + ) -join '') + } + } + return $result +} + +<# + .SYNOPSIS + Create, update or delete the Firewall Rule. + + .PARAMETER Name + Name of the firewall rule. + + .PARAMETER DisplayName + Localized, user-facing name of the firewall rule being created. + + .PARAMETER Group + Name of the firewall group where we want to put the firewall rule. + + .PARAMETER Ensure + Ensure that the firewall rule exists. + + .PARAMETER Enabled + Enable or Disable the supplied configuration. + + .PARAMETER Action + Allow or Block the supplied configuration. + + .PARAMETER Profile + Specifies one or more profiles to which the rule is assigned. + + .PARAMETER Direction + Direction of the connection. + + .PARAMETER RemotePort + Specific port used for filter. Specified by port number, range, or keyword. + + .PARAMETER LocalPort + Local port used for the filter. + + .PARAMETER Protocol + Specific protocol for filter. Specified by name, number, or range. + + .PARAMETER Description + Documentation for the rule. + + .PARAMETER Program + Path and filename of the program for which the rule is applied. + + .PARAMETER Service + Specifies the short name of a Windows service to which the firewall rule applies. + + .PARAMETER Authentication + Specifies that authentication is required on firewall rules. + + .PARAMETER Encryption + Specifies that encryption in authentication is required on firewall rules. + + .PARAMETER InterfaceAlias + Specifies the alias of the interface that applies to the traffic. + + .PARAMETER InterfaceType + Specifies that only network connections made through the indicated interface types are subject + to the requirements of this rule. + + .PARAMETER LocalAddress + Specifies that network packets with matching IP addresses match this rule. This parameter value + is the first end point of an IPsec rule and specifies the computers that are subject to the + requirements of this rule. This parameter value is an IPv4 or IPv6 address, hostname, subnet, + range, or the following keyword: Any. + + .PARAMETER LocalUser + Specifies the principals to which network traffic this firewall rule applies. The principals, + represented by security identifiers (SIDs) in the security descriptor definition language (SDDL) + string, are services, users, application containers, or any SID to which network traffic is + associated. + + .PARAMETER Package + Specifies the Windows Store application to which the firewall rule applies. This parameter is + specified as a security identifier (SID). + + .PARAMETER Platform + Specifies which version of Windows the associated rule applies. + + .PARAMETER RemoteAddress + Specifies that network packets with matching IP addresses match this rule. This parameter value + is the second end point of an IPsec rule and specifies the computers that are subject to the + requirements of this rule. This parameter value is an IPv4 or IPv6 address, hostname, subnet, + range, or the following keyword: Any + + .PARAMETER RemoteMachine + Specifies that matching IPsec rules of the indicated computer accounts are created. This + parameter specifies that only network packets that are authenticated as incoming from or + outgoing to a computer identified in the list of computer accounts (SID) match this rule. + This parameter value is specified as an SDDL string. + + .PARAMETER RemoteUser + Specifies that matching IPsec rules of the indicated user accounts are created. This parameter + specifies that only network packets that are authenticated as incoming from or outgoing to a + user identified in the list of user accounts match this rule. This parameter value is specified + as an SDDL string. + + .PARAMETER DynamicTransport + Specifies a dynamic transport. + + .PARAMETER EdgeTraversalPolicy + Specifies that matching firewall rules of the indicated edge traversal policy are created. + + .PARAMETER IcmpType + Specifies the ICMP type codes. + + .PARAMETER LocalOnlyMapping + Indicates that matching firewall rules of the indicated value are created. + + .PARAMETER LooseSourceMapping + Indicates that matching firewall rules of the indicated value are created. + + .PARAMETER OverrideBlockRules + Indicates that matching network traffic that would otherwise be blocked are allowed. + + .PARAMETER Owner + Specifies that matching firewall rules of the indicated owner are created. +#> +function Set-TargetResource +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [String] + $Name, + + [Parameter()] + [ValidateNotNullOrEmpty()] + [String] + $DisplayName, + + [Parameter()] + [ValidateNotNullOrEmpty()] + [String] + $Group, + + [Parameter()] + [ValidateSet('Present', 'Absent')] + [String] + $Ensure = 'Present', + + [Parameter()] + [ValidateSet('True', 'False')] + [String] + $Enabled, + + [Parameter()] + [ValidateSet('NotConfigured', 'Allow', 'Block')] + [String] + $Action, + + [Parameter()] + [String[]] + $Profile, + + [Parameter()] + [ValidateSet('Inbound', 'Outbound')] + [String] + $Direction, + + [Parameter()] + [ValidateNotNullOrEmpty()] + [String[]] + $RemotePort, + + [Parameter()] + [ValidateNotNullOrEmpty()] + [String[]] + $LocalPort, + + [Parameter()] + [ValidateNotNullOrEmpty()] + [String] + $Protocol, + + [Parameter()] + [String] + $Description, + + [Parameter()] + [ValidateNotNullOrEmpty()] + [String] + $Program, + + [Parameter()] + [ValidateNotNullOrEmpty()] + [String] + $Service, + + [Parameter()] + [ValidateSet('NotRequired', 'Required', 'NoEncap')] + [String] + $Authentication, + + [Parameter()] + [ValidateSet('NotRequired', 'Required', 'Dynamic')] + [String] + $Encryption, + + [Parameter()] + [ValidateNotNullOrEmpty()] + [String[]] + $InterfaceAlias, + + [Parameter()] + [ValidateSet('Any', 'Wired', 'Wireless', 'RemoteAccess')] + [String] + $InterfaceType, + + [Parameter()] + [ValidateNotNullOrEmpty()] + [String[]] + $LocalAddress, + + [Parameter()] + [ValidateNotNullOrEmpty()] + [String] + $LocalUser, + + [Parameter()] + [ValidateNotNullOrEmpty()] + [String] + $Package, + + [Parameter()] + [ValidateNotNullOrEmpty()] + [String[]] + $Platform, + + [Parameter()] + [ValidateNotNullOrEmpty()] + [String[]] + $RemoteAddress, + + [Parameter()] + [ValidateNotNullOrEmpty()] + [String] + $RemoteMachine, + + [Parameter()] + [ValidateNotNullOrEmpty()] + [String] + $RemoteUser, + + [Parameter()] + [ValidateSet('Any', 'ProximityApps', 'ProximitySharing', 'WifiDirectPrinting', 'WifiDirectDisplay', 'WifiDirectDevices')] + [String] + $DynamicTransport, + + [Parameter()] + [ValidateSet('Block', 'Allow', 'DeferToUser', 'DeferToApp')] + [String] + $EdgeTraversalPolicy, + + [Parameter()] + [ValidateNotNullOrEmpty()] + [String[]] + $IcmpType, + + [Parameter()] + [Boolean] + $LocalOnlyMapping, + + [Parameter()] + [Boolean] + $LooseSourceMapping, + + [Parameter()] + [Boolean] + $OverrideBlockRules, + + [Parameter()] + [ValidateNotNullOrEmpty()] + [String] + $Owner + ) + + Write-Verbose -Message ( @( "$($MyInvocation.MyCommand): " + $($script:localizedData.ApplyingFirewallRuleMessage) -f $Name + ) -join '') + + # Remove any parameters not used in Splats + $null = $PSBoundParameters.Remove('Ensure') + + Write-Verbose -Message ( @( "$($MyInvocation.MyCommand): " + $($script:localizedData.FindFirewallRuleMessage) -f $Name + ) -join '') + $firewallRule = Get-FirewallRule -Name $Name + + $exists = ($null -ne $firewallRule) + + if ($Ensure -eq 'Present') + { + Write-Verbose -Message ( @( "$($MyInvocation.MyCommand): " + $($script:localizedData.FirewallRuleShouldExistMessage) -f $Name, $Ensure + ) -join '') + + if ($exists) + { + Write-Verbose -Message ( @( "$($MyInvocation.MyCommand): " + $($script:localizedData.FirewallRuleShouldExistAndDoesMessage) -f $Name + ) -join '') + + Write-Verbose -Message ( @( "$($MyInvocation.MyCommand): " + $($script:localizedData.CheckFirewallRuleParametersMessage) -f $Name + ) -join '') + + if (-not (Test-RuleProperties -FirewallRule $firewallRule @PSBoundParameters)) + { + Write-Verbose -Message ( @( "$($MyInvocation.MyCommand): " + $($script:localizedData.UpdatingExistingFirewallMessage) -f $Name + ) -join '') + + # If the Group is being changed the the rule needs to be recreated + if ($PSBoundParameters.ContainsKey('Group') ` + -and ($Group -ne $FirewallRule.Group)) + { + Remove-NetFirewallRule -Name (ConvertTo-FirewallRuleNameEscapedString -Name $Name) + + <# + Merge the existing rule values into the PSBoundParameters + so that it can be splatted. + #> + $properties = Get-FirewallRuleProperty -FirewallRule $firewallRule + + <# + Loop through each possible property and if it is not passed as a parameter + then set the PSBoundParameter property to the exiting rule value. + #> + foreach ($parameter in $ParametersList) + { + if (-not $PSBoundParameters.ContainsKey($parameter.Name)) + { + $parameterValue = Get-FirewallPropertyValue ` + -FirewallRule $firewallRule ` + -Properties $properties ` + -Parameter $parameter + + if ($ParameterValue) + { + $null = $PSBoundParameters.Add($parameter.Name, $ParameterValue) + } + } + } + + New-NetFirewallRule @PSBoundParameters + } + else + { + # Group is a lookup key parameter that cannot be used in conjunction with Name + $null = $PSBoundParameters.Remove('Group') + + <# + If the DisplayName is provided then need to remove it + And change it to NewDisplayName if it is different. + #> + if ($PSBoundParameters.ContainsKey('DisplayName')) + { + $null = $PSBoundParameters.Remove('DisplayName') + if ($DisplayName -ne $FirewallRule.DisplayName) + { + $null = $PSBoundParameters.Add('NewDisplayName', $DisplayName) + } + } + + # Escape firewall rule name to ensure that wildcard update is not used + $PSBoundParameters['Name'] = ConvertTo-FirewallRuleNameEscapedString -Name $Name + + # Set the existing Firewall rule based on specified parameters + Set-NetFirewallRule @PSBoundParameters + } + } + } + else + { + Write-Verbose -Message ( @( "$($MyInvocation.MyCommand): " + $($script:localizedData.FirewallRuleShouldExistAndDoesNotMessage) -f $Name + ) -join '') + + # Set any default parameter values + if (-not $DisplayName) + { + if (-not $PSBoundParameters.ContainsKey('DisplayName')) + { + $null = $PSBoundParameters.Add('DisplayName', $Name) + } + else + { + $PSBoundParameters.DisplayName = $Name + } + } + + # Add the new Firewall rule based on specified parameters + New-NetFirewallRule @PSBoundParameters + } + } + else + { + Write-Verbose -Message ( @( "$($MyInvocation.MyCommand): " + $($script:localizedData.FirewallRuleShouldNotExistMessage) -f $Name, $Ensure + ) -join '') + + if ($exists) + { + Write-Verbose -Message ( @( "$($MyInvocation.MyCommand): " + $($script:localizedData.FirewallRuleShouldNotExistButDoesMessage) -f $Name + ) -join '') + + # Remove the existing Firewall rule + Remove-NetFirewallRule -Name (ConvertTo-FirewallRuleNameEscapedString -Name $Name) + } + else + { + Write-Verbose -Message ( @( "$($MyInvocation.MyCommand): " + $($script:localizedData.FirewallRuleShouldNotExistAndDoesNotMessage) -f $Name + ) -join '') + # Do Nothing + } + } +} + +<# + .SYNOPSIS + Test if Firewall Rule is in the required state. + + .PARAMETER Name + Name of the firewall rule. + + .PARAMETER DisplayName + Localized, user-facing name of the firewall rule being created. + + .PARAMETER Group + Name of the firewall group where we want to put the firewall rule. + + .PARAMETER Ensure + Ensure that the firewall rule exists. + + .PARAMETER Enabled + Enable or Disable the supplied configuration. + + .PARAMETER Action + Allow or Block the supplied configuration. + + .PARAMETER Profile + Specifies one or more profiles to which the rule is assigned. + + .PARAMETER Direction + Direction of the connection. + + .PARAMETER RemotePort + Specific port used for filter. Specified by port number, range, or keyword. + + .PARAMETER LocalPort + Local port used for the filter. + + .PARAMETER Protocol + Specific protocol for filter. Specified by name, number, or range. + + .PARAMETER Description + Documentation for the rule. + + .PARAMETER Program + Path and filename of the program for which the rule is applied. + + .PARAMETER Service + Specifies the short name of a Windows service to which the firewall rule applies. + + .PARAMETER Authentication + Specifies that authentication is required on firewall rules. + + .PARAMETER Encryption + Specifies that encryption in authentication is required on firewall rules. + + .PARAMETER InterfaceAlias + Specifies the alias of the interface that applies to the traffic. + + .PARAMETER InterfaceType + Specifies that only network connections made through the indicated interface types are subject + to the requirements of this rule. + + .PARAMETER LocalAddress + Specifies that network packets with matching IP addresses match this rule. This parameter value + is the first end point of an IPsec rule and specifies the computers that are subject to the + requirements of this rule. This parameter value is an IPv4 or IPv6 address, hostname, subnet, + range, or the following keyword: Any. + + .PARAMETER LocalUser + Specifies the principals to which network traffic this firewall rule applies. The principals, + represented by security identifiers (SIDs) in the security descriptor definition language (SDDL) + string, are services, users, application containers, or any SID to which network traffic is + associated. + + .PARAMETER Package + Specifies the Windows Store application to which the firewall rule applies. This parameter is + specified as a security identifier (SID). + + .PARAMETER Platform + Specifies which version of Windows the associated rule applies. + + .PARAMETER RemoteAddress + Specifies that network packets with matching IP addresses match this rule. This parameter value + is the second end point of an IPsec rule and specifies the computers that are subject to the + requirements of this rule. This parameter value is an IPv4 or IPv6 address, hostname, subnet, + range, or the following keyword: Any + + .PARAMETER RemoteMachine + Specifies that matching IPsec rules of the indicated computer accounts are created. This + parameter specifies that only network packets that are authenticated as incoming from or + outgoing to a computer identified in the list of computer accounts (SID) match this rule. + This parameter value is specified as an SDDL string. + + .PARAMETER RemoteUser + Specifies that matching IPsec rules of the indicated user accounts are created. This parameter + specifies that only network packets that are authenticated as incoming from or outgoing to a + user identified in the list of user accounts match this rule. This parameter value is specified + as an SDDL string. + + .PARAMETER DynamicTransport + Specifies a dynamic transport. + + .PARAMETER EdgeTraversalPolicy + Specifies that matching firewall rules of the indicated edge traversal policy are created. + + .PARAMETER IcmpType + Specifies the ICMP type codes. + + .PARAMETER LocalOnlyMapping + Indicates that matching firewall rules of the indicated value are created. + + .PARAMETER LooseSourceMapping + Indicates that matching firewall rules of the indicated value are created. + + .PARAMETER OverrideBlockRules + Indicates that matching network traffic that would otherwise be blocked are allowed. + + .PARAMETER Owner + Specifies that matching firewall rules of the indicated owner are created. +#> +function Test-TargetResource +{ + [CmdletBinding()] + [OutputType([System.Boolean])] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [String] + $Name, + + [Parameter()] + [ValidateNotNullOrEmpty()] + [String] + $DisplayName, + + [Parameter()] + [ValidateNotNullOrEmpty()] + [String] + $Group, + + [Parameter()] + [ValidateSet('Present', 'Absent')] + [String] + $Ensure = 'Present', + + [Parameter()] + [ValidateSet('True', 'False')] + [String] + $Enabled, + + [Parameter()] + [ValidateSet('NotConfigured', 'Allow', 'Block')] + [String] + $Action, + + [Parameter()] + [String[]] + $Profile, + + [Parameter()] + [ValidateSet('Inbound', 'Outbound')] + [String] + $Direction, + + [Parameter()] + [ValidateNotNullOrEmpty()] + [String[]] + $RemotePort, + + [Parameter()] + [ValidateNotNullOrEmpty()] + [String[]] + $LocalPort, + + [Parameter()] + [ValidateNotNullOrEmpty()] + [String] + $Protocol, + + [Parameter()] + [String] + $Description, + + [Parameter()] + [ValidateNotNullOrEmpty()] + [String] + $Program, + + [Parameter()] + [ValidateNotNullOrEmpty()] + [String] + $Service, + + [Parameter()] + [ValidateSet('NotRequired', 'Required', 'NoEncap')] + [String] + $Authentication, + + [Parameter()] + [ValidateSet('NotRequired', 'Required', 'Dynamic')] + [String] + $Encryption, + + [Parameter()] + [ValidateNotNullOrEmpty()] + [String[]] + $InterfaceAlias, + + [Parameter()] + [ValidateSet('Any', 'Wired', 'Wireless', 'RemoteAccess')] + [String] + $InterfaceType, + + [Parameter()] + [ValidateNotNullOrEmpty()] + [String[]] + $LocalAddress, + + [Parameter()] + [ValidateNotNullOrEmpty()] + [String] + $LocalUser, + + [Parameter()] + [ValidateNotNullOrEmpty()] + [String] + $Package, + + [Parameter()] + [ValidateNotNullOrEmpty()] + [String[]] + $Platform, + + [Parameter()] + [ValidateNotNullOrEmpty()] + [String[]] + $RemoteAddress, + + [Parameter()] + [ValidateNotNullOrEmpty()] + [String] + $RemoteMachine, + + [Parameter()] + [ValidateNotNullOrEmpty()] + [String] + $RemoteUser, + + [Parameter()] + [ValidateSet('Any', 'ProximityApps', 'ProximitySharing', 'WifiDirectPrinting', 'WifiDirectDisplay', 'WifiDirectDevices')] + [String] + $DynamicTransport, + + [Parameter()] + [ValidateSet('Block', 'Allow', 'DeferToUser', 'DeferToApp')] + [String] + $EdgeTraversalPolicy, + + [Parameter()] + [ValidateNotNullOrEmpty()] + [String[]] + $IcmpType, + + [Parameter()] + [Boolean] + $LocalOnlyMapping, + + [Parameter()] + [Boolean] + $LooseSourceMapping, + + [Parameter()] + [Boolean] + $OverrideBlockRules, + + [Parameter()] + [ValidateNotNullOrEmpty()] + [String] + $Owner + ) + + Write-Verbose -Message ( @( "$($MyInvocation.MyCommand): " + $($script:localizedData.CheckingFirewallRuleMessage) -f $Name + ) -join '') + + # Remove any parameters not used in Splats + $null = $PSBoundParameters.Remove('Ensure') + + Write-Verbose -Message ( @( "$($MyInvocation.MyCommand): " + $($script:localizedData.FindFirewallRuleMessage) -f $Name + ) -join '') + + $firewallRule = Get-FirewallRule -Name $Name + + $exists = ($null -ne $firewallRule) + + if (-not $exists) + { + Write-Verbose -Message ( @( "$($MyInvocation.MyCommand): " + $($script:localizedData.FirewallRuleDoesNotExistMessage) -f $Name + ) -join '') + + # Returns whether complies with $Ensure + $returnValue = ($false -eq ($Ensure -eq 'Present')) + + Write-Verbose -Message ( @( "$($MyInvocation.MyCommand): " + $($script:localizedData.CheckingFirewallReturningMessage) -f $Name, $returnValue + ) -join '') + + return $returnValue + } + + Write-Verbose -Message ( @( "$($MyInvocation.MyCommand): " + $($script:localizedData.CheckFirewallRuleParametersMessage) -f $Name + ) -join '') + + $desiredConfigurationMatch = Test-RuleProperties -FirewallRule $firewallRule @PSBoundParameters + + # Returns whether or not $exists complies with $Ensure + $returnValue = ($desiredConfigurationMatch -and $exists -eq ($Ensure -eq 'Present')) + + Write-Verbose -Message ( @( "$($MyInvocation.MyCommand): " + $($script:localizedData.CheckingFirewallReturningMessage) -f $Name, $returnValue + ) -join '') + + return $returnValue +} + +<# + .SYNOPSIS + Tests if the properties in the supplied Firewall Rule match the expected parameters passed. + + .PARAMETER FirewallRule + The firewall rule object to compare the properties of. + + .PARAMETER Name + Name of the firewall rule. + + .PARAMETER DisplayName + Localized, user-facing name of the firewall rule being created. + + .PARAMETER Group + Name of the firewall group where we want to put the firewall rule. + + .PARAMETER Ensure + Ensure that the firewall rule exists. + + .PARAMETER Enabled + Enable or Disable the supplied configuration. + + .PARAMETER Action + Allow or Block the supplied configuration. + + .PARAMETER Profile + Specifies one or more profiles to which the rule is assigned. + + .PARAMETER Direction + Direction of the connection. + + .PARAMETER RemotePort + Specific port used for filter. Specified by port number, range, or keyword. + + .PARAMETER LocalPort + Local port used for the filter. + + .PARAMETER Protocol + Specific protocol for filter. Specified by name, number, or range. + + .PARAMETER Description + Documentation for the rule. + + .PARAMETER Program + Path and filename of the program for which the rule is applied. + + .PARAMETER Service + Specifies the short name of a Windows service to which the firewall rule applies. + + .PARAMETER Authentication + Specifies that authentication is required on firewall rules. + + .PARAMETER Encryption + Specifies that encryption in authentication is required on firewall rules. + + .PARAMETER InterfaceAlias + Specifies the alias of the interface that applies to the traffic. + + .PARAMETER InterfaceType + Specifies that only network connections made through the indicated interface types are subject + to the requirements of this rule. + + .PARAMETER LocalAddress + Specifies that network packets with matching IP addresses match this rule. This parameter value + is the first end point of an IPsec rule and specifies the computers that are subject to the + requirements of this rule. This parameter value is an IPv4 or IPv6 address, hostname, subnet, + range, or the following keyword: Any. + + .PARAMETER LocalUser + Specifies the principals to which network traffic this firewall rule applies. The principals, + represented by security identifiers (SIDs) in the security descriptor definition language (SDDL) + string, are services, users, application containers, or any SID to which network traffic is + associated. + + .PARAMETER Package + Specifies the Windows Store application to which the firewall rule applies. This parameter is + specified as a security identifier (SID). + + .PARAMETER Platform + Specifies which version of Windows the associated rule applies. + + .PARAMETER RemoteAddress + Specifies that network packets with matching IP addresses match this rule. This parameter value + is the second end point of an IPsec rule and specifies the computers that are subject to the + requirements of this rule. This parameter value is an IPv4 or IPv6 address, hostname, subnet, + range, or the following keyword: Any + + .PARAMETER RemoteMachine + Specifies that matching IPsec rules of the indicated computer accounts are created. This + parameter specifies that only network packets that are authenticated as incoming from or + outgoing to a computer identified in the list of computer accounts (SID) match this rule. + This parameter value is specified as an SDDL string. + + .PARAMETER RemoteUser + Specifies that matching IPsec rules of the indicated user accounts are created. This parameter + specifies that only network packets that are authenticated as incoming from or outgoing to a + user identified in the list of user accounts match this rule. This parameter value is specified + as an SDDL string. + + .PARAMETER DynamicTransport + Specifies a dynamic transport. + + .PARAMETER EdgeTraversalPolicy + Specifies that matching firewall rules of the indicated edge traversal policy are created. + + .PARAMETER IcmpType + Specifies the ICMP type codes. + + .PARAMETER LocalOnlyMapping + Indicates that matching firewall rules of the indicated value are created. + + .PARAMETER LooseSourceMapping + Indicates that matching firewall rules of the indicated value are created. + + .PARAMETER OverrideBlockRules + Indicates that matching network traffic that would otherwise be blocked are allowed. + + .PARAMETER Owner + Specifies that matching firewall rules of the indicated owner are created. +#> +function Test-RuleProperties +{ + [CmdletBinding()] + [OutputType([System.Boolean])] + param + ( + [Parameter(Mandatory = $true)] + $FirewallRule, + + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [String] + $Name, + + [Parameter()] + [String] + $DisplayName, + + [Parameter()] + [String] + $Group, + + [Parameter()] + [String] + $DisplayGroup, + + [Parameter()] + [ValidateSet('Present', 'Absent')] + [String] + $Ensure = 'Present', + + [Parameter()] + [ValidateSet('True', 'False')] + [String] + $Enabled, + + [Parameter()] + [ValidateSet('NotConfigured', 'Allow', 'Block')] + [String] + $Action, + + [Parameter()] + [String[]] + $Profile, + + [Parameter()] + [ValidateSet('Inbound', 'Outbound')] + [String] + $Direction, + + [Parameter()] + [String[]] + $RemotePort, + + [Parameter()] + [String[]] + $LocalPort, + + [Parameter()] + [String] + $Protocol, + + [Parameter()] + [String] + $Description, + + [Parameter()] + [String] + $Program, + + [Parameter()] + [String] + $Service, + + [Parameter()] + [ValidateSet('NotRequired', 'Required', 'NoEncap')] + [String] + $Authentication, + + [Parameter()] + [ValidateSet('NotRequired', 'Required', 'Dynamic')] + [String] + $Encryption, + + [Parameter()] + [String[]] + $InterfaceAlias, + + [Parameter()] + [ValidateSet('Any', 'Wired', 'Wireless', 'RemoteAccess')] + [String] + $InterfaceType, + + [Parameter()] + [String[]] + $LocalAddress, + + [Parameter()] + [String] + $LocalUser, + + [Parameter()] + [String] + $Package, + + [Parameter()] + [String[]] + $Platform, + + [Parameter()] + [String[]] + $RemoteAddress, + + [Parameter()] + [String] + $RemoteMachine, + + [Parameter()] + [String] + $RemoteUser, + + [Parameter()] + [ValidateSet('Any', 'ProximityApps', 'ProximitySharing', 'WifiDirectPrinting', 'WifiDirectDisplay', 'WifiDirectDevices')] + [String] + $DynamicTransport, + + [Parameter()] + [ValidateSet('Block', 'Allow', 'DeferToUser', 'DeferToApp')] + [String] + $EdgeTraversalPolicy, + + [Parameter()] + [String[]] + $IcmpType, + + [Parameter()] + [Boolean] + $LocalOnlyMapping, + + [Parameter()] + [Boolean] + $LooseSourceMapping, + + [Parameter()] + [Boolean] + $OverrideBlockRules, + + [Parameter()] + [String] + $Owner + ) + + $properties = Get-FirewallRuleProperty -FirewallRule $FirewallRule + $desiredConfigurationMatch = $true + + <# + Loop through the $script:parameterList array and compare the source + with the value of each parameter. If different then set $desiredConfigurationMatch + to false. + #> + foreach ($parameter in $script:parameterList) + { + $parameterValue = Get-FirewallPropertyValue ` + -FirewallRule $firewallRule ` + -Properties $properties ` + -Parameter $parameter + + $parameterNew = (Get-Variable -Name ($parameter.Name)).Value + + switch -Wildcard ($parameter.Type) + { + 'String' + { + # Perform a plain string comparison. + if ($parameterNew -and ($parameterValue -ne $parameterNew)) + { + Write-Verbose -Message ( @( "$($MyInvocation.MyCommand): " + $($script:localizedData.PropertyNoMatchMessage) ` + -f $parameter.Name, $parameterValue, $parameterNew + ) -join '') + + $desiredConfigurationMatch = $false + } + } + + 'Boolean' + { + # Perform a boolean comparison. + if ($parameterNew -and ($parameterValue -ne $parameterNew)) + { + Write-Verbose -Message ( @( "$($MyInvocation.MyCommand): " + $($script:localizedData.PropertyNoMatchMessage) ` + -f $parameter.Name, $parameterValue, $parameterNew + ) -join '') + $desiredConfigurationMatch = $false + } + } + + 'Array*' + { + # Array comparison uses Compare-Object + if ($null -eq $parameterValue) + { + $parameterValue = @() + } + + if ($parameter.Delimiter) + { + $parameterValue = $parameterValue -split $parameter.Delimiter + } + + if ($parameter.Type -eq 'ArrayIP') + { + <# + IPArray comparison uses Compare-Object, except needs to convert any IP addresses + that use CIDR notation to use Subnet Mask notification because this is the + format that the Get-NetFirewallAddressFilter will return the IP addresses in + even if they were set using CIDR notation. + #> + if ($null -ne $parameterNew) + { + $parameterNew = Convert-CIDRToSubhetMask -Address $parameterNew + } + } + + if ($parameterNew ` + -and ((Compare-Object ` + -ReferenceObject $parameterValue ` + -DifferenceObject $parameterNew).Count -ne 0)) + { + Write-Verbose -Message ( @( "$($MyInvocation.MyCommand): " + $($script:localizedData.PropertyNoMatchMessage) ` + -f $parameter.Name, ($parameterValue -join ','), ($parameterNew -join ',') + ) -join '') + $desiredConfigurationMatch = $false + } + } + } + } + + Write-Verbose -Message ( @( "$($MyInvocation.MyCommand): " + $($script:localizedData.TestFirewallRuleReturningMessage) -f $Name, $desiredConfigurationMatch + ) -join '') + return $desiredConfigurationMatch +} + +<# + .SYNOPSIS + Returns a Firewall object matching the specified name. + + .PARAMETER Name + The name of the Firewall Rule to Retrieve. +#> +function Get-FirewallRule +{ + [CmdletBinding()] + [OutputType([Microsoft.Management.Infrastructure.CimInstance])] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [String] + $Name + ) + + $firewallRule = @(Get-NetFirewallRule -Name (ConvertTo-FirewallRuleNameEscapedString -Name $Name) -ErrorAction SilentlyContinue) + + if (-not $firewallRule) + { + Write-Verbose -Message ( @( "$($MyInvocation.MyCommand): " + $($script:localizedData.FirewallRuleNotFoundMessage) -f $Name + ) -join '') + return $null + } + + <# + If more than one rule is returned for a name, then throw an exception + because this should not be possible. + #> + if ($firewallRule.Count -gt 1) + { + New-InvalidOperationException ` + -Message ($script:localizedData.RuleNotUniqueError -f $firewallRule.Count, $Name) + } + + # The array will only contain a single rule so only return the first one (not the array) + return $firewallRule[0] +} + +<# + .SYNOPSIS + Returns a Hashtable containing the component Firewall objects for the specified Firewall Rule. + + .PARAMETER FirewallRule + The firewall rule object to pull the additional firewall objects for. +#> +function Get-FirewallRuleProperty +{ + [CmdletBinding()] + [OutputType([HashTable])] + param + ( + [Parameter(Mandatory = $true)] + $FirewallRule + ) + + Write-Verbose -Message ( @( "$($MyInvocation.MyCommand): " + $($script:localizedData.GetAllPropertiesMessage) + ) -join '') + + return @{ + AddressFilters = @(Get-NetFirewallAddressFilter -AssociatedNetFirewallRule $FirewallRule) + ApplicationFilters = @(Get-NetFirewallApplicationFilter -AssociatedNetFirewallRule $FirewallRule) + InterfaceFilters = @(Get-NetFirewallInterfaceFilter -AssociatedNetFirewallRule $FirewallRule) + InterfaceTypeFilters = @(Get-NetFirewallInterfaceTypeFilter -AssociatedNetFirewallRule $FirewallRule) + PortFilters = @(Get-NetFirewallPortFilter -AssociatedNetFirewallRule $FirewallRule) + Profile = @(Get-NetFirewallProfile -AssociatedNetFirewallRule $FirewallRule) + SecurityFilters = @(Get-NetFirewallSecurityFilter -AssociatedNetFirewallRule $FirewallRule) + ServiceFilters = @(Get-NetFirewallServiceFilter -AssociatedNetFirewallRule $FirewallRule) + } +} + +<# + .SYNOPSIS + Looks up a Firewall Property value using the specified parameterList entry. + + .PARAMETER FirewallRule + The firewall rule object to pull the property from. + + .PARAMETER Properties + The additional firewall objects to pull the property from. + + .PARAMETER Parameter + The entry from the ParameterList table used to retireve the parameter for. +#> +function Get-FirewallPropertyValue +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + $FirewallRule, + + [Parameter(Mandatory = $true)] + $Properties, + + [Parameter(Mandatory = $true)] + $Parameter + ) + + if ($Parameter.Property) + { + return (Get-Variable ` + -Name ($Parameter.Variable)).value.$($Parameter.Property).$($Parameter.Name) + } + else + { + return (Get-Variable ` + -Name ($Parameter.Variable)).value.$($Parameter.Name) + } +} + +<# + .SYNOPSIS + Convert Firewall Rule name to Escape Wildcard Characters. + + It will append '[', ']' and '*' with a backtick. + + .PARAMETER Name + The firewall rule name to escape. +#> +function ConvertTo-FirewallRuleNameEscapedString +{ + [CmdletBinding()] + [OutputType([System.String])] + param + ( + [Parameter(Mandatory = $true)] + $Name + ) + + return $Name.Replace('[','`[').Replace(']','`]').Replace('*','`*') +} + +Export-ModuleMember -Function *-TargetResource diff --git a/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_Firewall/DSC_Firewall.schema.mof b/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_Firewall/DSC_Firewall.schema.mof new file mode 100644 index 0000000..ab75049 --- /dev/null +++ b/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_Firewall/DSC_Firewall.schema.mof @@ -0,0 +1,37 @@ +[ClassVersion("1.0.0"), FriendlyName("Firewall")] +class DSC_Firewall : OMI_BaseResource +{ + [Key, Description("Name of the Firewall Rule.")] String Name; + [Write, Description("Localized, user-facing name of the Firewall Rule being created.")] String DisplayName; + [Write, Description("Name of the Firewall Group where we want to put the Firewall Rule.")] string Group; + [Write, Description("Ensure the presence/absence of the resource."), ValueMap{"Present", "Absent"},Values{"Present", "Absent"}] String Ensure; + [Write, Description("Enable or disable the supplied configuration."), ValueMap{"True", "False"},Values{"True", "False"}] string Enabled; + [Write, Description("Allow or Block the supplied configuration."), ValueMap{"NotConfigured", "Allow", "Block"}, Values{"NotConfigured", "Allow", "Block"}] String Action; + [Write, Description("Specifies one or more profiles to which the rule is assigned.")] String Profile[]; + [Write, Description("Direction of the connection."), ValueMap{"Inbound","Outbound"}, Values{"Inbound","Outbound"}] String Direction; + [Write, Description("Specific Port used for filter. Specified by port number, range, or keyword")] String RemotePort[]; + [Write, Description("Local Port used for the filter.")] String LocalPort[]; + [Write, Description("Specific Protocol for filter. Specified by name, number, or range.")] String Protocol; + [Write, Description("Documentation for the Rule.")] String Description; + [Write, Description("Path and file name of the program for which the rule is applied.")] String Program; + [Write, Description("Specifies the short name of a Windows service to which the firewall rule applies.")] String Service; + [Write, Description("Specifies that authentication is required on firewall rules."), ValueMap{"NotRequired", "Required", "NoEncap"}, Values{"NotRequired", "Required", "NoEncap"}] String Authentication; + [Write, Description("Specifies that encryption in authentication is required on firewall rules."), ValueMap{"NotRequired", "Required", "Dynamic"}, Values{"NotRequired", "Required", "Dynamic"}] String Encryption; + [Write, Description("Specifies the alias of the interface that applies to the traffic.")] String InterfaceAlias[]; + [Write, Description("Specifies that only network connections made through the indicated interface types are subject to the requirements of this rule."), ValueMap{"Any", "Wired", "Wireless", "RemoteAccess"}, Values{"Any", "Wired", "Wireless", "RemoteAccess"}] String InterfaceType; + [Write, Description("Specifies that network packets with matching IP addresses match this rule.")] String LocalAddress[]; + [Write, Description("Specifies the principals to which network traffic this firewall rule applies.")] String LocalUser; + [Write, Description("Specifies the Windows Store application to which the firewall rule applies.")] String Package; + [Write, Description("Specifies which version of Windows the associated rule applies.")] String Platform[]; + [Write, Description("Specifies that network packets with matching IP addresses match this rule.")] String RemoteAddress[]; + [Write, Description("Specifies that matching IPsec rules of the indicated computer accounts are created.")] String RemoteMachine; + [Write, Description("Specifies that matching IPsec rules of the indicated user accounts are created.")] String RemoteUser; + [Write, Description("Specifies a dynamic transport."), ValueMap{"Any","ProximityApps","ProximitySharing","WifiDirectPrinting","WifiDirectDisplay","WifiDirectDevices"},Values{"Any","ProximityApps","ProximitySharing","WifiDirectPrinting","WifiDirectDisplay","WifiDirectDevices"}] String DynamicTransport; + [Write, Description("Specifies that matching firewall rules of the indicated edge traversal policy are created."), ValueMap{"Block","Allow","DeferToUser","DeferToApp"},Values{"Block","Allow","DeferToUser","DeferToApp"}] String EdgeTraversalPolicy; + [Write, Description("Specifies the ICMP type codes.")] String IcmpType[]; + [Write, Description("Indicates that matching firewall rules of the indicated value are created.")] Boolean LocalOnlyMapping; + [Write, Description("Indicates that matching firewall rules of the indicated value are created.")] Boolean LooseSourceMapping; + [Write, Description("Indicates that matching network traffic that would otherwise be blocked are allowed.")] Boolean OverrideBlockRules; + [Write, Description("Specifies that matching firewall rules of the indicated owner are created.")] String Owner; + [Read, Description("The current value of the Display Group of the Firewall Rule.")] string DisplayGroup; +}; diff --git a/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_Firewall/README.MD b/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_Firewall/README.MD new file mode 100644 index 0000000..86b89a8 --- /dev/null +++ b/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_Firewall/README.MD @@ -0,0 +1,3 @@ +# Description + +This resource is used to control firewall rules for a node. diff --git a/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_Firewall/en-US/DSC_Firewall.strings.psd1 b/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_Firewall/en-US/DSC_Firewall.strings.psd1 new file mode 100644 index 0000000..9bc0265 --- /dev/null +++ b/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_Firewall/en-US/DSC_Firewall.strings.psd1 @@ -0,0 +1,24 @@ +# Localized resources for DSC_Firewall + +ConvertFrom-StringData @' + GettingFirewallRuleMessage = Getting firewall rule with Name '{0}'. + FirewallRuleDoesNotExistMessage = Firewall rule with Name '{0}' does not exist. + FirewallParameterValueMessage = Firewall rule with Name '{0}' parameter {1} is '{2}'. + ApplyingFirewallRuleMessage = Applying settings for firewall rule with Name '{0}'. + FindFirewallRuleMessage = Find firewall rule with Name '{0}'. + FirewallRuleShouldExistMessage = We want the firewall rule with Name '{0}' to exist since Ensure is set to {1}. + FirewallRuleShouldExistAndDoesMessage = We want the firewall rule with Name '{0}' to exist and it does. Check for valid properties. + CheckFirewallRuleParametersMessage = Check each defined parameter against the existing firewall rule with Name '{0}'. + UpdatingExistingFirewallMessage = Updating existing firewall rule with Name '{0}'. + FirewallRuleShouldExistAndDoesNotMessage = We want the firewall rule with Name '{0}' to exist, but it does not. + FirewallRuleShouldNotExistMessage = We do not want the firewall rule with Name '{0}' to exist since Ensure is set to {1}. + FirewallRuleShouldNotExistButDoesMessage = We do not want the firewall rule with Name '{0}' to exist, but it does. Removing it. + FirewallRuleShouldNotExistAndDoesNotMessage = We do not want the firewall rule with Name '{0}' to exist, and it does not. + CheckingFirewallRuleMessage = Checking settings for firewall rule with Name '{0}'. + CheckingFirewallReturningMessage = Check Firewall rule with Name '{0}' returning {1}. + PropertyNoMatchMessage = {0} property value '{1}' does not match desired state '{2}'. + TestFirewallRuleReturningMessage = Test Firewall rule with Name '{0}' returning {1}. + FirewallRuleNotFoundMessage = No Firewall Rule found with Name '{0}'. + GetAllPropertiesMessage = Get all the properties and add filter info to rule map. + RuleNotUniqueError = {0} Firewall Rules with the Name '{1}' were found. Only one expected. +'@ diff --git a/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_Firewall/en-US/about_Firewall.help.txt b/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_Firewall/en-US/about_Firewall.help.txt new file mode 100644 index 0000000..8e07d04 --- /dev/null +++ b/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_Firewall/en-US/about_Firewall.help.txt @@ -0,0 +1,275 @@ +.NAME + Firewall + +.DESCRIPTION + This resource is used to control firewall rules for a node. + +.PARAMETER Name + Key - String + Name of the Firewall Rule. + +.PARAMETER DisplayName + Write - String + Localized, user-facing name of the Firewall Rule being created. + +.PARAMETER Group + Write - String + Name of the Firewall Group where we want to put the Firewall Rule. + +.PARAMETER Ensure + Write - String + Allowed values: Present, Absent + Ensure the presence/absence of the resource. + +.PARAMETER Enabled + Write - String + Allowed values: True, False + Enable or disable the supplied configuration. + +.PARAMETER Action + Write - String + Allowed values: NotConfigured, Allow, Block + Allow or Block the supplied configuration. + +.PARAMETER Profile + Write - StringArray + Specifies one or more profiles to which the rule is assigned. + +.PARAMETER Direction + Write - String + Allowed values: Inbound, Outbound + Direction of the connection. + +.PARAMETER RemotePort + Write - StringArray + Specific Port used for filter. Specified by port number, range, or keyword + +.PARAMETER LocalPort + Write - StringArray + Local Port used for the filter. + +.PARAMETER Protocol + Write - String + Specific Protocol for filter. Specified by name, number, or range. + +.PARAMETER Description + Write - String + Documentation for the Rule. + +.PARAMETER Program + Write - String + Path and file name of the program for which the rule is applied. + +.PARAMETER Service + Write - String + Specifies the short name of a Windows service to which the firewall rule applies. + +.PARAMETER Authentication + Write - String + Allowed values: NotRequired, Required, NoEncap + Specifies that authentication is required on firewall rules. + +.PARAMETER Encryption + Write - String + Allowed values: NotRequired, Required, Dynamic + Specifies that encryption in authentication is required on firewall rules. + +.PARAMETER InterfaceAlias + Write - StringArray + Specifies the alias of the interface that applies to the traffic. + +.PARAMETER InterfaceType + Write - String + Allowed values: Any, Wired, Wireless, RemoteAccess + Specifies that only network connections made through the indicated interface types are subject to the requirements of this rule. + +.PARAMETER LocalAddress + Write - StringArray + Specifies that network packets with matching IP addresses match this rule. + +.PARAMETER LocalUser + Write - String + Specifies the principals to which network traffic this firewall rule applies. + +.PARAMETER Package + Write - String + Specifies the Windows Store application to which the firewall rule applies. + +.PARAMETER Platform + Write - StringArray + Specifies which version of Windows the associated rule applies. + +.PARAMETER RemoteAddress + Write - StringArray + Specifies that network packets with matching IP addresses match this rule. + +.PARAMETER RemoteMachine + Write - String + Specifies that matching IPsec rules of the indicated computer accounts are created. + +.PARAMETER RemoteUser + Write - String + Specifies that matching IPsec rules of the indicated user accounts are created. + +.PARAMETER DynamicTransport + Write - String + Allowed values: Any, ProximityApps, ProximitySharing, WifiDirectPrinting, WifiDirectDisplay, WifiDirectDevices + Specifies a dynamic transport. + +.PARAMETER EdgeTraversalPolicy + Write - String + Allowed values: Block, Allow, DeferToUser, DeferToApp + Specifies that matching firewall rules of the indicated edge traversal policy are created. + +.PARAMETER IcmpType + Write - StringArray + Specifies the ICMP type codes. + +.PARAMETER LocalOnlyMapping + Write - Boolean + Indicates that matching firewall rules of the indicated value are created. + +.PARAMETER LooseSourceMapping + Write - Boolean + Indicates that matching firewall rules of the indicated value are created. + +.PARAMETER OverrideBlockRules + Write - Boolean + Indicates that matching network traffic that would otherwise be blocked are allowed. + +.PARAMETER Owner + Write - String + Specifies that matching firewall rules of the indicated owner are created. + +.PARAMETER DisplayGroup + Read - String + The current value of the Display Group of the Firewall Rule. + +.EXAMPLE 1 + +Allow notepad to access ports on the Domain and Private Profiles. + +Configuration Firewall_AddFirewallRule_Config +{ + Import-DSCResource -ModuleName NetworkingDsc + + Node localhost + { + Firewall AddFirewallRule + { + Name = 'NotePadFirewallRule' + DisplayName = 'Firewall Rule for Notepad.exe' + Group = 'NotePad Firewall Rule Group' + Ensure = 'Present' + Enabled = 'True' + Profile = ('Domain', 'Private') + Direction = 'OutBound' + RemotePort = ('8080', '8081') + LocalPort = ('9080', '9081') + Protocol = 'TCP' + Description = 'Firewall Rule for Notepad.exe' + Program = 'c:\windows\system32\notepad.exe' + Service = 'WinRM' + } + } + } + +.EXAMPLE 2 + +Configure a network firewall rule using all parameters. +Note: This configuration sample uses all Firewall rule parameters. +It is only used to show example usage and should not be created. + +Configuration Firewall_AddFirewallRule_AllParameters_Config +{ + Import-DSCResource -ModuleName NetworkingDsc + + Node localhost + { + Firewall AddFirewallRuleAllParameters + { + Name = 'NotePadFirewallRule' + DisplayName = 'Firewall Rule for Notepad.exe' + Group = 'NotePad Firewall Rule Group' + Ensure = 'Present' + Enabled = 'True' + Profile = ('Domain', 'Private') + Direction = 'OutBound' + RemotePort = ('8080', '8081') + LocalPort = ('9080', '9081') + Protocol = 'TCP' + Description = 'Firewall Rule for Notepad.exe' + Program = 'c:\windows\system32\notepad.exe' + Service = 'WinRM' + Authentication = 'Required' + Encryption = 'Required' + InterfaceAlias = 'Ethernet' + InterfaceType = 'Wired' + LocalAddress = ('192.168.2.0-192.168.2.128','192.168.1.0/255.255.255.0','10.0.0.0/8') + LocalUser = 'O:LSD:(D;;CC;;;S-1-15-3-4)(A;;CC;;;S-1-5-21-3337988176-3917481366-464002247-1001)' + Package = 'S-1-15-2-3676279713-3632409675-756843784-3388909659-2454753834-4233625902-1413163418' + Platform = '6.1' + RemoteAddress = ('192.168.2.0-192.168.2.128','192.168.1.0/255.255.255.0','10.0.0.0/8') + RemoteMachine = 'O:LSD:(D;;CC;;;S-1-5-21-1915925333-479612515-2636650677-1621)(A;;CC;;;S-1-5-21-1915925333-479612515-2636650677-1620)' + RemoteUser = 'O:LSD:(D;;CC;;;S-1-15-3-4)(A;;CC;;;S-1-5-21-3337988176-3917481366-464002247-1001)' + DynamicTransport = 'ProximitySharing' + EdgeTraversalPolicy = 'Block' + IcmpType = ('51','52') + LocalOnlyMapping = $true + LooseSourceMapping = $true + OverrideBlockRules = $true + Owner = 'S-1-5-21-3337988176-3917481366-464002247-500' + } + } +} + +.EXAMPLE 3 + +Adding a firewall to an existing Firewall group 'My Firewall Rule'. + +Configuration Firewall_AddFirewallRuleToExistingGroup_Config +{ + Import-DSCResource -ModuleName NetworkingDsc + + Node localhost + { + Firewall AddFirewallRuleToExistingGroup + { + Name = 'MyFirewallRule' + DisplayName = 'My Firewall Rule' + Group = 'My Firewall Rule Group' + } + + Firewall Firewall1 + { + Name = 'MyFirewallRule1' + DisplayName = 'My Firewall Rule' + Group = 'My Firewall Rule Group' + Ensure = 'Present' + Enabled = 'True' + Profile = ('Domain', 'Private') + } + } +} + +.EXAMPLE 4 + +DSC configuration that enables the built-in Firewall Rule +'World Wide Web Services (HTTP Traffic-In)'. + +Configuration Firewall_EnableBuiltInFirewallRule_Config +{ + Import-DSCResource -ModuleName NetworkingDsc + + Node localhost + { + Firewall EnableBuiltInFirewallRule + { + Name = 'IIS-WebServerRole-HTTP-In-TCP' + Ensure = 'Present' + Enabled = 'True' + } + } +} + + diff --git a/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_FirewallProfile/DSC_FirewallProfile.data.psd1 b/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_FirewallProfile/DSC_FirewallProfile.data.psd1 new file mode 100644 index 0000000..6c01212 --- /dev/null +++ b/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_FirewallProfile/DSC_FirewallProfile.data.psd1 @@ -0,0 +1,72 @@ +@{ + ParameterList = @( + @{ + Name = 'AllowInboundRules' + Type = 'String' + }, + @{ + Name = 'AllowLocalFirewallRules' + Type = 'String' + }, + @{ + Name = 'AllowLocalIPsecRules' + Type = 'String' + }, + @{ + Name = 'AllowUnicastResponseToMulticast' + Type = 'String' + }, + @{ + Name = 'AllowUserApps' + Type = 'String' + }, + @{ + Name = 'AllowUserPorts' + Type = 'String' + }, + @{ + Name = 'DefaultInboundAction' + Type = 'String' + }, + @{ + Name = 'DefaultOutboundAction' + Type = 'String' + }, + @{ + Name = 'DisabledInterfaceAliases' + Type = 'Array' + }, + @{ + Name = 'Enabled' + Type = 'String' + }, + @{ + Name = 'EnableStealthModeForIPsec' + Type = 'String' + }, + @{ + Name = 'LogAllowed' + Type = 'String' + }, + @{ + Name = 'LogBlocked' + Type = 'String' + }, + @{ + Name = 'LogFileName' + Type = 'String' + }, + @{ + Name = 'LogIgnored' + Type = 'String' + }, + @{ + Name = 'LogMaxSizeKilobytes' + Type = 'Uint64' + } + @{ + Name = 'NotifyOnListen' + Type = 'String' + } + ) +} diff --git a/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_FirewallProfile/DSC_FirewallProfile.psm1 b/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_FirewallProfile/DSC_FirewallProfile.psm1 new file mode 100644 index 0000000..9e0a3c7 --- /dev/null +++ b/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_FirewallProfile/DSC_FirewallProfile.psm1 @@ -0,0 +1,470 @@ +$modulePath = Join-Path -Path (Split-Path -Path (Split-Path -Path $PSScriptRoot -Parent) -Parent) -ChildPath 'Modules' + +# Import the Networking Common Modules +Import-Module -Name (Join-Path -Path $modulePath ` + -ChildPath (Join-Path -Path 'NetworkingDsc.Common' ` + -ChildPath 'NetworkingDsc.Common.psm1')) + +Import-Module -Name (Join-Path -Path $modulePath -ChildPath 'DscResource.Common') + +# Import Localization Strings +$script:localizedData = Get-LocalizedData -DefaultUICulture 'en-US' + +<# + This is an array of all the parameters used by this resource. +#> +$resourceData = Import-LocalizedData ` + -BaseDirectory $PSScriptRoot ` + -FileName 'DSC_FirewallProfile.data.psd1' + +# This must be a script parameter so that it is accessible +$script:parameterList = $resourceData.ParameterList + +<# + .SYNOPSIS + Returns the current Firewall Profile. + + .PARAMETER Name + The name of the firewall profile to configure. +#> +function Get-TargetResource +{ + [CmdletBinding()] + [OutputType([System.Collections.Hashtable])] + param + ( + [Parameter(Mandatory = $true)] + [ValidateSet('Domain', 'Public', 'Private')] + [System.String] + $Name + ) + + Write-Verbose -Message ( @( + "$($MyInvocation.MyCommand): " + $($script:localizedData.GettingFirewallProfileMessage) ` + -f $Name + ) -join '' ) + + # Get the current Dns Client Global Settings + $netFirewallProfile = Get-NetFirewallProfile -Name $Name ` + -ErrorAction Stop + + # Generate the return object. + $returnValue = @{ + Name = $Name + } + + foreach ($parameter in $script:parameterList) + { + $returnValue += @{ + $parameter.Name = $netFirewallProfile.$($parameter.name) + } + } # foreach + + return $returnValue +} # Get-TargetResource + + +<# + .SYNOPSIS + Sets the Firewall Profile. + + .PARAMETER Name + The name of the firewall profile to configure. + + .PARAMETER AllowInboundRules + Specifies that the firewall blocks inbound traffic. + + .PARAMETER AllowLocalFirewallRules + Specifies that the local firewall rules should be merged into the effective policy + along with Group Policy settings. + + .PARAMETER AllowLocalIPsecRules + Specifies that the local IPsec rules should be merged into the effective policy + along with Group Policy settings. + + .PARAMETER AllowUnicastResponseToMulticast + Allows unicast responses to multi-cast traffic. + + .PARAMETER AllowUserApps + Specifies that traffic from local user applications is allowed through the firewall. + + .PARAMETER AllowUserPorts + Specifies that traffic is allowed through local user ports. + + .PARAMETER DefaultInboundAction + Specifies how to filter inbound traffic. + + .PARAMETER DefaultOutboundAction + Specifies how to filter outbound traffic. + + .PARAMETER DisabledInterfaceAliases + Specifies a list of interfaces on which firewall settings are excluded. + + .PARAMETER Enabled + Specifies that devolution is activated. + + .PARAMETER EnableStealthModeForIPsec + Enables stealth mode for IPsec traffic. + + .PARAMETER LogAllowed + Specifies how to log the allowed packets in the location specified by the + LogFileName parameter. + + .PARAMETER LogBlocked + Specifies how to log the dropped packets in the location specified by the + LogFileName parameter. + + .PARAMETER LogFileName + Specifies the path and filename of the file to which Windows Server writes log entries. + + .PARAMETER LogIgnored + Specifies how to log the ignored packets in the location specified by the LogFileName + parameter. + + .PARAMETER LogMaxSizeKilobytes + Specifies the maximum file size of the log, in kilobytes. The acceptable values for + this parameter are: 1 through 32767. + + .PARAMETER NotifyOnListen + Allows the notification of listening for inbound connections by a service. +#> +function Set-TargetResource +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [ValidateSet('Domain', 'Public', 'Private')] + [System.String] + $Name, + + [Parameter()] + [ValidateSet('True', 'False', 'NotConfigured')] + [System.String] + $AllowInboundRules, + + [Parameter()] + [ValidateSet('True', 'False', 'NotConfigured')] + [System.String] + $AllowLocalFirewallRules, + + [Parameter()] + [ValidateSet('True', 'False', 'NotConfigured')] + [System.String] + $AllowLocalIPsecRules, + + [Parameter()] + [ValidateSet('True', 'False', 'NotConfigured')] + [System.String] + $AllowUnicastResponseToMulticast, + + [Parameter()] + [ValidateSet('True', 'False', 'NotConfigured')] + [System.String] + $AllowUserApps, + + [Parameter()] + [ValidateSet('True', 'False', 'NotConfigured')] + [System.String] + $AllowUserPorts, + + [Parameter()] + [ValidateSet('Block', 'Allow', 'NotConfigured')] + [System.String] + $DefaultInboundAction, + + [Parameter()] + [ValidateSet('Block', 'Allow', 'NotConfigured')] + [System.String] + $DefaultOutboundAction, + + [Parameter()] + [System.String[]] + $DisabledInterfaceAliases, + + [Parameter()] + [ValidateSet('True', 'False', 'NotConfigured')] + [System.String] + $Enabled, + + [Parameter()] + [ValidateSet('True', 'False', 'NotConfigured')] + [System.String] + $EnableStealthModeForIPsec, + + [Parameter()] + [ValidateSet('True', 'False', 'NotConfigured')] + [System.String] + $LogAllowed, + + [Parameter()] + [ValidateSet('True', 'False', 'NotConfigured')] + [System.String] + $LogBlocked, + + [Parameter()] + [ValidateNotNullOrEmpty()] + [System.String] + $LogFileName, + + [Parameter()] + [ValidateSet('True', 'False', 'NotConfigured')] + [System.String] + $LogIgnored, + + [Parameter()] + [ValidateRange(1,32767)] + [System.Uint64] + $LogMaxSizeKilobytes, + + [Parameter()] + [ValidateSet('True', 'False', 'NotConfigured')] + [System.String] + $NotifyOnListen + ) + + Write-Verbose -Message ( @( + "$($MyInvocation.MyCommand): " + $($script:localizedData.SettingFirewallProfileMessage) ` + -f $Name + ) -join '' ) + + # Get the current Firewall Profile Settings + $netFirewallProfile = Get-NetFirewallProfile -Name $Name ` + -ErrorAction Stop + + # Generate a list of parameters that will need to be changed. + $changeParameters = @{} + + foreach ($parameter in $script:parameterList) + { + $parameterSourceValue = $netFirewallProfile.$($parameter.name) + $parameterNewValue = (Get-Variable -Name ($parameter.name)).Value + + if ($PSBoundParameters.ContainsKey($parameter.Name) ` + -and (Compare-Object -ReferenceObject $parameterSourceValue -DifferenceObject $parameterNewValue -SyncWindow 0)) + { + $changeParameters += @{ + $($parameter.name) = $parameterNewValue + } + + Write-Verbose -Message ( @( + "$($MyInvocation.MyCommand): " + $($script:localizedData.FirewallProfileUpdateParameterMessage) ` + -f $Name,$parameter.Name,$parameterNewValue + ) -join '' ) + } # if + } # foreach + + if ($changeParameters.Count -gt 0) + { + # Update any parameters that were identified as different + $null = Set-NetFirewallProfile -Name $Name ` + @ChangeParameters ` + -ErrorAction Stop + + Write-Verbose -Message ( @( + "$($MyInvocation.MyCommand): " + $($script:localizedData.FirewallProfileUpdatedMessage) ` + -f $Name + ) -join '' ) + } # if +} # Set-TargetResource + +<# + .SYNOPSIS + Tests the state of Firewall Profile. + + .PARAMETER Name + The name of the firewall profile to configure. + + .PARAMETER AllowInboundRules + Specifies that the firewall blocks inbound traffic. + + .PARAMETER AllowLocalFirewallRules + Specifies that the local firewall rules should be merged into the effective policy + along with Group Policy settings. + + .PARAMETER AllowLocalIPsecRules + Specifies that the local IPsec rules should be merged into the effective policy + along with Group Policy settings. + + .PARAMETER AllowUnicastResponseToMulticast + Allows unicast responses to multi-cast traffic. + + .PARAMETER AllowUserApps + Specifies that traffic from local user applications is allowed through the firewall. + + .PARAMETER AllowUserPorts + Specifies that traffic is allowed through local user ports. + + .PARAMETER DefaultInboundAction + Specifies how to filter inbound traffic. + + .PARAMETER DefaultOutboundAction + Specifies how to filter outbound traffic. + + .PARAMETER DisabledInterfaceAliases + Specifies a list of interfaces on which firewall settings are excluded. + + .PARAMETER Enabled + Specifies that devolution is activated. + + .PARAMETER EnableStealthModeForIPsec + Enables stealth mode for IPsec traffic. + + .PARAMETER LogAllowed + Specifies how to log the allowed packets in the location specified by the + LogFileName parameter. + + .PARAMETER LogBlocked + Specifies how to log the dropped packets in the location specified by the + LogFileName parameter. + + .PARAMETER LogFileName + Specifies the path and filename of the file to which Windows Server writes log entries. + + .PARAMETER LogIgnored + Specifies how to log the ignored packets in the location specified by the LogFileName + parameter. + + .PARAMETER LogMaxSizeKilobytes + Specifies the maximum file size of the log, in kilobytes. The acceptable values for + this parameter are: 1 through 32767. + + .PARAMETER NotifyOnListen + Allows the notification of listening for inbound connections by a service. +#> +function Test-TargetResource +{ + [CmdletBinding()] + [OutputType([System.Boolean])] + param + ( + [Parameter(Mandatory = $true)] + [ValidateSet('Domain', 'Public', 'Private')] + [System.String] + $Name, + + [Parameter()] + [ValidateSet('True', 'False', 'NotConfigured')] + [System.String] + $AllowInboundRules, + + [Parameter()] + [ValidateSet('True', 'False', 'NotConfigured')] + [System.String] + $AllowLocalFirewallRules, + + [Parameter()] + [ValidateSet('True', 'False', 'NotConfigured')] + [System.String] + $AllowLocalIPsecRules, + + [Parameter()] + [ValidateSet('True', 'False', 'NotConfigured')] + [System.String] + $AllowUnicastResponseToMulticast, + + [Parameter()] + [ValidateSet('True', 'False', 'NotConfigured')] + [System.String] + $AllowUserApps, + + [Parameter()] + [ValidateSet('True', 'False', 'NotConfigured')] + [System.String] + $AllowUserPorts, + + [Parameter()] + [ValidateSet('Block', 'Allow', 'NotConfigured')] + [System.String] + $DefaultInboundAction, + + [Parameter()] + [ValidateSet('Block', 'Allow', 'NotConfigured')] + [System.String] + $DefaultOutboundAction, + + [Parameter()] + [System.String[]] + $DisabledInterfaceAliases, + + [Parameter()] + [ValidateSet('True', 'False', 'NotConfigured')] + [System.String] + $Enabled, + + [Parameter()] + [ValidateSet('True', 'False', 'NotConfigured')] + [System.String] + $EnableStealthModeForIPsec, + + [Parameter()] + [ValidateSet('True', 'False', 'NotConfigured')] + [System.String] + $LogAllowed, + + [Parameter()] + [ValidateSet('True', 'False', 'NotConfigured')] + [System.String] + $LogBlocked, + + [Parameter()] + [ValidateNotNullOrEmpty()] + [System.String] + $LogFileName, + + [Parameter()] + [ValidateSet('True', 'False', 'NotConfigured')] + [System.String] + $LogIgnored, + + [Parameter()] + [ValidateRange(1,32767)] + [System.Uint64] + $LogMaxSizeKilobytes, + + [Parameter()] + [ValidateSet('True', 'False', 'NotConfigured')] + [System.String] + $NotifyOnListen + ) + + Write-Verbose -Message ( @( + "$($MyInvocation.MyCommand): " + $($script:localizedData.TestingFirewallProfileMessage) ` + -f $Name + ) -join '' ) + + # Flag to signal whether settings are correct + $desiredConfigurationMatch = $true + + # Get the current Dns Client Global Settings + $netFirewallProfile = Get-NetFirewallProfile -Name $Name ` + -ErrorAction Stop + + # Check each parameter + foreach ($parameter in $script:parameterList) + { + $parameterSourceValue = $netFirewallProfile.$($parameter.name) + $parameterNewValue = (Get-Variable -Name ($parameter.name)).Value + + if ($PSBoundParameters.ContainsKey($parameter.Name) ` + -and (Compare-Object -ReferenceObject $parameterSourceValue -DifferenceObject $parameterNewValue -SyncWindow 0)) + { + Write-Verbose -Message ( @( + "$($MyInvocation.MyCommand): " + $($script:localizedData.FirewallProfileParameterNeedsUpdateMessage) ` + -f $Name,$parameter.Name,$parameterSourceValue,$parameterNewValue + ) -join '' ) + + $desiredConfigurationMatch = $false + } # if + } # foreach + + return $desiredConfigurationMatch +} # Test-TargetResource + +Export-ModuleMember -Function *-TargetResource diff --git a/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_FirewallProfile/DSC_FirewallProfile.schema.mof b/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_FirewallProfile/DSC_FirewallProfile.schema.mof new file mode 100644 index 0000000..a8133a5 --- /dev/null +++ b/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_FirewallProfile/DSC_FirewallProfile.schema.mof @@ -0,0 +1,22 @@ +[ClassVersion("1.0.0.0"), FriendlyName("FirewallProfile")] +class DSC_FirewallProfile : OMI_BaseResource +{ + [Key, Description("The name of the firewall profile to configure."), ValueMap{"Domain", "Public", "Private"}, Values{"Domain", "Public", "Private"}] String Name; + [Write, Description("Specifies that the firewall blocks inbound traffic."), ValueMap{"True", "False", "NotConfigured"}, Values{"True", "False", "NotConfigured"}] String AllowInboundRules; + [Write, Description("Specifies that the local firewall rules should be merged into the effective policy along with Group Policy settings."), ValueMap{"True", "False", "NotConfigured"}, Values{"True", "False", "NotConfigured"}] String AllowLocalFirewallRules; + [Write, Description("Specifies that the local IPsec rules should be merged into the effective policy along with Group Policy settings."), ValueMap{"True", "False", "NotConfigured"}, Values{"True", "False", "NotConfigured"}] String AllowLocalIPsecRules; + [Write, Description("Allows unicast responses to multi-cast traffic."), ValueMap{"True", "False", "NotConfigured"}, Values{"True", "False", "NotConfigured"}] String AllowUnicastResponseToMulticast; + [Write, Description("Specifies that traffic from local user applications is allowed through the firewall."), ValueMap{"True", "False", "NotConfigured"}, Values{"True", "False", "NotConfigured"}] String AllowUserApps; + [Write, Description("Specifies that traffic is allowed through local user ports."), ValueMap{"True", "False", "NotConfigured"}, Values{"True", "False", "NotConfigured"}] String AllowUserPorts; + [Write, Description("Specifies how to filter inbound traffic."), ValueMap{"Block", "Allow", "NotConfigured"}, Values{"Block", "Allow", "NotConfigured"}] String DefaultInboundAction; + [Write, Description("Specifies how to filter outbound traffic."), ValueMap{"Block", "Allow", "NotConfigured"}, Values{"Block", "Allow", "NotConfigured"}] String DefaultOutboundAction; + [Write, Description("Specifies a list of interfaces on which firewall settings are excluded.")] String DisabledInterfaceAliases[]; + [Write, Description("Specifies that devolution is activated."), ValueMap{"True", "False", "NotConfigured"}, Values{"True", "False", "NotConfigured"}] String Enabled; + [Write, Description("Enables stealth mode for IPsec traffic."), ValueMap{"True", "False", "NotConfigured"}, Values{"True", "False", "NotConfigured"}] String EnableStealthModeForIPsec; + [Write, Description("Specifies how to log the allowed packets in the location specified by the LogFileName parameter."), ValueMap{"True", "False", "NotConfigured"}, Values{"True", "False", "NotConfigured"}] String LogAllowed; + [Write, Description("Specifies how to log the dropped packets in the location specified by the LogFileName parameter."), ValueMap{"True", "False", "NotConfigured"}, Values{"True", "False", "NotConfigured"}] String LogBlocked; + [Write, Description("Specifies the path and filename of the file to which Windows Server writes log entries.")] String LogFileName; + [Write, Description("Specifies how to log the ignored packets in the location specified by the LogFileName parameter."), ValueMap{"True", "False", "NotConfigured"}, Values{"True", "False", "NotConfigured"}] String LogIgnored; + [Write, Description("Specifies the maximum file size of the log, in kilobytes. The acceptable values for this parameter are: 1 through 32767.")] UInt64 LogMaxSizeKilobytes; + [Write, Description("Allows the notification of listening for inbound connections by a service."), ValueMap{"True", "False", "NotConfigured"}, Values{"True", "False", "NotConfigured"}] String NotifyOnListen; +}; diff --git a/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_FirewallProfile/README.MD b/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_FirewallProfile/README.MD new file mode 100644 index 0000000..019952d --- /dev/null +++ b/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_FirewallProfile/README.MD @@ -0,0 +1,4 @@ +# Description + +This resource is used to enable or disable and configure Windows Firewall with +Advanced Security profiles. diff --git a/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_FirewallProfile/en-US/DSC_FirewallProfile.strings.psd1 b/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_FirewallProfile/en-US/DSC_FirewallProfile.strings.psd1 new file mode 100644 index 0000000..b856938 --- /dev/null +++ b/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_FirewallProfile/en-US/DSC_FirewallProfile.strings.psd1 @@ -0,0 +1,10 @@ +# Localized resources for DSC_FirewallProfile + +ConvertFrom-StringData @' + GettingFirewallProfileMessage = Getting Firewall {0} Profile. + SettingFirewallProfileMessage = Setting Firewall {0} Profile. + FirewallProfileUpdateParameterMessage = Setting Firewall {0} Profile parameter {1} to "{1}". + FirewallProfileUpdatedMessage = Setting Firewall {0} Profile updated. + TestingFirewallProfileMessage = Testing Firewall {0} Profile. + FirewallProfileParameterNeedsUpdateMessage = Firewall {0} Profile "{1}" is "{2}" but should be "{3}". Change required. +'@ diff --git a/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_FirewallProfile/en-US/about_FirewallProfile.help.txt b/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_FirewallProfile/en-US/about_FirewallProfile.help.txt new file mode 100644 index 0000000..e74b09c --- /dev/null +++ b/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_FirewallProfile/en-US/about_FirewallProfile.help.txt @@ -0,0 +1,124 @@ +.NAME + FirewallProfile + +.DESCRIPTION + This resource is used to enable or disable and configure Windows Firewall with + Advanced Security profiles. + +.PARAMETER Name + Key - String + Allowed values: Domain, Public, Private + The name of the firewall profile to configure. + +.PARAMETER AllowInboundRules + Write - String + Allowed values: True, False, NotConfigured + Specifies that the firewall blocks inbound traffic. + +.PARAMETER AllowLocalFirewallRules + Write - String + Allowed values: True, False, NotConfigured + Specifies that the local firewall rules should be merged into the effective policy along with Group Policy settings. + +.PARAMETER AllowLocalIPsecRules + Write - String + Allowed values: True, False, NotConfigured + Specifies that the local IPsec rules should be merged into the effective policy along with Group Policy settings. + +.PARAMETER AllowUnicastResponseToMulticast + Write - String + Allowed values: True, False, NotConfigured + Allows unicast responses to multi-cast traffic. + +.PARAMETER AllowUserApps + Write - String + Allowed values: True, False, NotConfigured + Specifies that traffic from local user applications is allowed through the firewall. + +.PARAMETER AllowUserPorts + Write - String + Allowed values: True, False, NotConfigured + Specifies that traffic is allowed through local user ports. + +.PARAMETER DefaultInboundAction + Write - String + Allowed values: Block, Allow, NotConfigured + Specifies how to filter inbound traffic. + +.PARAMETER DefaultOutboundAction + Write - String + Allowed values: Block, Allow, NotConfigured + Specifies how to filter outbound traffic. + +.PARAMETER DisabledInterfaceAliases + Write - StringArray + Specifies a list of interfaces on which firewall settings are excluded. + +.PARAMETER Enabled + Write - String + Allowed values: True, False, NotConfigured + Specifies that devolution is activated. + +.PARAMETER EnableStealthModeForIPsec + Write - String + Allowed values: True, False, NotConfigured + Enables stealth mode for IPsec traffic. + +.PARAMETER LogAllowed + Write - String + Allowed values: True, False, NotConfigured + Specifies how to log the allowed packets in the location specified by the LogFileName parameter. + +.PARAMETER LogBlocked + Write - String + Allowed values: True, False, NotConfigured + Specifies how to log the dropped packets in the location specified by the LogFileName parameter. + +.PARAMETER LogFileName + Write - String + Specifies the path and filename of the file to which Windows Server writes log entries. + +.PARAMETER LogIgnored + Write - String + Allowed values: True, False, NotConfigured + Specifies how to log the ignored packets in the location specified by the LogFileName parameter. + +.PARAMETER LogMaxSizeKilobytes + Write - UInt64 + Specifies the maximum file size of the log, in kilobytes. The acceptable values for this parameter are: 1 through 32767. + +.PARAMETER NotifyOnListen + Write - String + Allowed values: True, False, NotConfigured + Allows the notification of listening for inbound connections by a service. + +.EXAMPLE 1 + +Configure the Private Firewall Profile. + +Configuration FirewallProfile_ConfigurePrivateFirewallProfile_Config +{ + Import-DscResource -Module NetworkingDsc + + Node localhost + { + FirewallProfile ConfigurePrivateFirewallProfile + { + Name = 'Private' + Enabled = 'True' + DefaultInboundAction = 'Block' + DefaultOutboundAction = 'Allow' + AllowInboundRules = 'True' + AllowLocalFirewallRules = 'False' + AllowLocalIPsecRules = 'False' + NotifyOnListen = 'True' + LogFileName = '%systemroot%\system32\LogFiles\Firewall\pfirewall.log' + LogMaxSizeKilobytes = 16384 + LogAllowed = 'False' + LogBlocked = 'True' + LogIgnored = 'NotConfigured' + } + } +} + + diff --git a/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_HostsFile/DSC_HostsFile.psm1 b/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_HostsFile/DSC_HostsFile.psm1 new file mode 100644 index 0000000..279af66 --- /dev/null +++ b/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_HostsFile/DSC_HostsFile.psm1 @@ -0,0 +1,278 @@ +$modulePath = Join-Path -Path (Split-Path -Path (Split-Path -Path $PSScriptRoot -Parent) -Parent) -ChildPath 'Modules' + +# Import the Networking Common Modules +Import-Module -Name (Join-Path -Path $modulePath ` + -ChildPath (Join-Path -Path 'NetworkingDsc.Common' ` + -ChildPath 'NetworkingDsc.Common.psm1')) + +Import-Module -Name (Join-Path -Path $modulePath -ChildPath 'DscResource.Common') + +# Import Localization Strings +$script:localizedData = Get-LocalizedData -DefaultUICulture 'en-US' + +<# + .SYNOPSIS + Returns the current state of a hosts file entry. + + .PARAMETER HostName + Specifies the name of the computer that will be mapped to an IP address. + + .PARAMETER IPAddress + Specifies the IP Address that should be mapped to the host name. + + .PARAMETER Ensure + Specifies if the hosts file entry should be created or deleted. +#> +function Get-TargetResource +{ + [CmdletBinding()] + [OutputType([System.Collections.Hashtable])] + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $HostName, + + [Parameter()] + [System.String] + $IPAddress, + + [Parameter()] + [ValidateSet('Present', 'Absent')] + [System.String] + $Ensure = 'Present' + ) + + Write-Verbose -Message ($script:localizedData.StartingGet -f $HostName) + + $result = Get-HostEntry -HostName $HostName + + if ($null -ne $result) + { + return @{ + HostName = $result.HostName + IPAddress = $result.IPAddress + Ensure = 'Present' + } + } + else + { + return @{ + HostName = $HostName + IPAddress = $null + Ensure = 'Absent' + } + } +} + +<# + .SYNOPSIS + Adds, updates or removes a hosts file entry. + + .PARAMETER HostName + Specifies the name of the computer that will be mapped to an IP address. + + .PARAMETER IPAddress + Specifies the IP Address that should be mapped to the host name. + + .PARAMETER Ensure + Specifies if the hosts file entry should be created or deleted. +#> +function Set-TargetResource +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $HostName, + + [Parameter()] + [System.String] + $IPAddress, + + [Parameter()] + [ValidateSet('Present', 'Absent')] + [System.String] + $Ensure = 'Present' + ) + + $hostPath = "$env:windir\System32\drivers\etc\hosts" + $currentValues = Get-TargetResource @PSBoundParameters + + Write-Verbose -Message ($script:localizedData.StartingSet -f $HostName) + + if ($Ensure -eq 'Present' -and $PSBoundParameters.ContainsKey('IPAddress') -eq $false) + { + New-InvalidArgumentException ` + -Message $($($script:localizedData.UnableToEnsureWithoutIP) -f $Address, $AddressFamily) ` + -ArgumentName 'IPAddress' + } + + if ($currentValues.Ensure -eq 'Absent' -and $Ensure -eq 'Present') + { + Write-Verbose -Message ($script:localizedData.CreateNewEntry -f $HostName) + Add-Content -Path $hostPath -Value "`r`n$IPAddress`t$HostName" + } + else + { + $hosts = Get-Content -Path $hostPath + $replace = $hosts | Where-Object -FilterScript { + [System.String]::IsNullOrEmpty($_) -eq $false -and $_.StartsWith('#') -eq $false -and $_ -like "*$HostName*" + } + + $multiLineEntry = $false + $data = $replace -split '\s+' + + if ($data.Length -gt 2) + { + $multiLineEntry = $true + } + + if ($Ensure -eq 'Present') + { + Write-Verbose -Message ($script:localizedData.UpdateExistingEntry -f $HostName) + + if ($multiLineEntry -eq $true) + { + $newReplaceLine = $replace -replace $HostName, '' + $hosts = $hosts -replace $replace, $newReplaceLine + $hosts += "$IPAddress`t$HostName" + } + else + { + $hosts = $hosts -replace $replace, "$IPAddress`t$HostName" + } + } + else + { + Write-Verbose -Message ($script:localizedData.RemoveEntry -f $HostName) + + if ($multiLineEntry -eq $true) + { + $newReplaceLine = $replace -replace $HostName, '' + $hosts = $hosts -replace $replace, $newReplaceLine + } + else + { + $hosts = $hosts -replace $replace, '' + } + } + + Set-Content -Path $hostPath -Value $hosts + } +} + +<# + .SYNOPSIS + Tests the current state of a hosts file entry. + + .PARAMETER HostName + Specifies the name of the computer that will be mapped to an IP address. + + .PARAMETER IPAddress + Specifies the IP Address that should be mapped to the host name. + + .PARAMETER Ensure + Specifies if the hosts file entry should be created or deleted. +#> +function Test-TargetResource +{ + [CmdletBinding()] + [OutputType([System.Boolean])] + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $HostName, + + [Parameter()] + [System.String] + $IPAddress, + + [Parameter()] + [ValidateSet('Present', 'Absent')] + [System.String] + $Ensure = 'Present' + ) + + $currentValues = Get-TargetResource @PSBoundParameters + + Write-Verbose -Message ($script:localizedData.StartingTest -f $HostName) + + if ($Ensure -ne $currentValues.Ensure) + { + return $false + } + + if ($Ensure -eq 'Present' -and $IPAddress -ne $currentValues.IPAddress) + { + return $false + } + + return $true +} + +function Get-HostEntry +{ + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $HostName + ) + + $hostPath = "$env:windir\System32\drivers\etc\hosts" + + $allHosts = Get-Content -Path $hostPath | Where-Object -FilterScript { + [System.String]::IsNullOrEmpty($_) -eq $false -and $_.StartsWith('#') -eq $false + } + + foreach ($hosts in $allHosts) + { + $data = $hosts -split '\s+' + + if ($data.Length -gt 2) + { + # Account for host entries that have multiple entries on a single line + $result = @() + $array = @() + + for ($i = 1; $i -lt $data.Length; $i++) + { + <# + Filter commments on the line. + Example: 0.0.0.0 s.gateway.messenger.live.com # breaks Skype GH-183 + becomes: + 0.0.0.0 s.gateway.messenger.live.com + #> + if ($data[$i] -eq '#') + { + break + } + + $array += $data[$i] + } + + $result = @{ + Host = $array + IPAddress = $data[0] + } + } + else + { + $result = @{ + Host = $data[1] + IPAddress = $data[0] + } + } + + if ($result.Host -eq $HostName) + { + return @{ + HostName = $result.Host + IPAddress = $result.IPAddress + } + } + } +} diff --git a/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_HostsFile/DSC_HostsFile.schema.mof b/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_HostsFile/DSC_HostsFile.schema.mof new file mode 100644 index 0000000..a2dcf86 --- /dev/null +++ b/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_HostsFile/DSC_HostsFile.schema.mof @@ -0,0 +1,7 @@ +[ClassVersion("1.0.0.0"), FriendlyName("HostsFile")] +class DSC_HostsFile : OMI_BaseResource +{ + [Key, Description("Specifies the name of the computer that will be mapped to an IP address.")] string HostName; + [Write, Description("Specifies the IP Address that should be mapped to the host name.")] string IPAddress; + [Write, Description("Specifies if the hosts file entry should be created or deleted."), ValueMap{"Present","Absent"}, Values{"Present","Absent"}] string Ensure; +}; diff --git a/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_HostsFile/README.MD b/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_HostsFile/README.MD new file mode 100644 index 0000000..da0b687 --- /dev/null +++ b/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_HostsFile/README.MD @@ -0,0 +1,3 @@ +# Description + +This resource is used to control entries on a the host file for a node. diff --git a/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_HostsFile/en-US/DSC_HostsFile.strings.psd1 b/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_HostsFile/en-US/DSC_HostsFile.strings.psd1 new file mode 100644 index 0000000..d1eb880 --- /dev/null +++ b/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_HostsFile/en-US/DSC_HostsFile.strings.psd1 @@ -0,0 +1,11 @@ +# Localized resources for DSC_HostFile + +ConvertFrom-StringData @' + UnableToEnsureWithoutIP = Unable to ensure a host entry is present without a corresponding IP address. Please add the IPAddress property and run this resource again. + CreateNewEntry = Creating new host entry for {0}. + UpdateExistingEntry = Updating existing host entry for {0}. + RemoveEntry = Removing host entry for {0}. + StartingGet = Looking up host entry for {0}. + StartingSet = Setting host entry for {0}. + StartingTest = Testing host entry for {0}. +'@ diff --git a/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_HostsFile/en-US/about_HostsFile.help.txt b/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_HostsFile/en-US/about_HostsFile.help.txt new file mode 100644 index 0000000..5d3459a --- /dev/null +++ b/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_HostsFile/en-US/about_HostsFile.help.txt @@ -0,0 +1,58 @@ +.NAME + HostsFile + +.DESCRIPTION + This resource is used to control entries on a the host file for a node. + +.PARAMETER HostName + Key - String + Specifies the name of the computer that will be mapped to an IP address. + +.PARAMETER IPAddress + Write - String + Specifies the IP Address that should be mapped to the host name. + +.PARAMETER Ensure + Write - String + Allowed values: Present, Absent + Specifies if the hosts file entry should be created or deleted. + +.EXAMPLE 1 + +Add a new host to the host file. + +Configuration HostsFile_AddEntry_Config +{ + Import-DSCResource -ModuleName NetworkingDsc + + Node localhost + { + HostsFile HostsFileAddEntry + { + HostName = 'Host01' + IPAddress = '192.168.0.1' + Ensure = 'Present' + } + } +} + +.EXAMPLE 2 + +Remove a host from the hosts file. + +Configuration HostsFile_RemoveEntry_Config +{ + Import-DSCResource -ModuleName NetworkingDsc + + Node localhost + { + HostsFile HostsFileRemoveEntry + { + HostName = 'Host01' + IPAddress = '192.168.0.1' + Ensure = 'Absent' + } + } +} + + diff --git a/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_IPAddress/DSC_IPAddress.psm1 b/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_IPAddress/DSC_IPAddress.psm1 new file mode 100644 index 0000000..a715f64 --- /dev/null +++ b/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_IPAddress/DSC_IPAddress.psm1 @@ -0,0 +1,505 @@ +$modulePath = Join-Path -Path (Split-Path -Path (Split-Path -Path $PSScriptRoot -Parent) -Parent) -ChildPath 'Modules' + +# Import the Networking Common Modules +Import-Module -Name (Join-Path -Path $modulePath ` + -ChildPath (Join-Path -Path 'NetworkingDsc.Common' ` + -ChildPath 'NetworkingDsc.Common.psm1')) + +Import-Module -Name (Join-Path -Path $modulePath -ChildPath 'DscResource.Common') + +# Import Localization Strings +$script:localizedData = Get-LocalizedData -DefaultUICulture 'en-US' + +<# + .SYNOPSIS + Returns the current state of an IP address assigned to an interface. + + .PARAMETER IPAddress + The desired IP address. + + .PARAMETER InterfaceAlias + Alias of the network interface for which the IP address should be set. + + .PARAMETER AddressFamily + IP address family. + + .PARAMETER KeepExistingAddress + Indicates whether or not existing IP addresses on an interface will be retained. +#> +function Get-TargetResource +{ + [CmdletBinding()] + [OutputType([System.Collections.Hashtable])] + param + ( + [Parameter()] + [ValidateNotNullOrEmpty()] + [System.String[]] + $IPAddress, + + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $InterfaceAlias, + + [Parameter(Mandatory = $true)] + [ValidateSet('IPv4', 'IPv6')] + [System.String] + $AddressFamily = 'IPv4', + + [Parameter()] + [System.Boolean] + $KeepExistingAddress = $false + ) + + Write-Verbose -Message ( @( "$($MyInvocation.MyCommand): " + $($script:localizedData.GettingIPAddressMessage) + ) -join '') + + $getNetIPAddressParameters = @{ + InterfaceAlias = $InterfaceAlias + AddressFamily = $AddressFamily + } + + $currentIPAddress = Get-NetIPAddress @getNetIPAddressParameters + + $currentIPAddressWithPrefix = $currentIPAddress | + Foreach-Object { + "$($_.IPAddress)/$($_.prefixLength)" + } + + $returnValue = @{ + IPAddress = @($currentIPAddressWithPrefix) + AddressFamily = $AddressFamily + InterfaceAlias = $InterfaceAlias + KeepExistingAddress = $KeepExistingAddress + } + + return $returnValue +} + +<# + .SYNOPSIS + Sets an IP address on an interface. + + .PARAMETER IPAddress + The desired IP address. + + .PARAMETER InterfaceAlias + Alias of the network interface for which the IP address should be set. + + .PARAMETER AddressFamily + IP address family. + + .PARAMETER KeepExistingAddress + Indicates whether or not existing IP addresses on an interface will be retained. +#> +function Set-TargetResource +{ + [CmdletBinding()] + param + ( + [Parameter()] + [ValidateNotNullOrEmpty()] + [System.String[]] + $IPAddress, + + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $InterfaceAlias, + + [Parameter(Mandatory = $true)] + [ValidateSet('IPv4', 'IPv6')] + [System.String] + $AddressFamily = 'IPv4', + + [Parameter()] + [System.Boolean] + $KeepExistingAddress = $false + ) + + Write-Verbose -Message ( @( "$($MyInvocation.MyCommand): " + $($script:localizedData.ApplyingIPAddressMessage) + ) -join '') + + # Use $AddressFamily to select the IPv4 or IPv6 destination prefix + $destinationPrefix = '0.0.0.0/0' + + if ($AddressFamily -eq 'IPv6') + { + $destinationPrefix = '::/0' + $prefixLength = 64 + } + + # Get all the default routes - this has to be done in case the IP Address is being Removed + $getNetRouteParameters = @{ + InterfaceAlias = $InterfaceAlias + AddressFamily = $AddressFamily + ErrorAction = 'Stop' + } + + $defaultRoutes = @(Get-NetRoute @getNetRouteParameters).Where( + { + $_.DestinationPrefix -eq $destinationPrefix + } + ) + + <# + Remove any default routes on the specified interface -- it is important to do + this *before* removing the IP address, particularly in the case where the IP + address was auto-configured by DHCP + #> + if ($defaultRoutes) + { + foreach ($defaultRoute in $defaultRoutes) + { + $removeNetRouteParameters = @{ + DestinationPrefix = $defaultRoute.DestinationPrefix + NextHop = $defaultRoute.NextHop + InterfaceIndex = $defaultRoute.InterfaceIndex + AddressFamily = $defaultRoute.AddressFamily + Confirm = $false + ErrorAction = 'Stop' + } + Remove-NetRoute @removeNetRouteParameters + } + } + + # Get the current IP Address based on the parameters given. + $getNetIPAddressParameters = @{ + InterfaceAlias = $InterfaceAlias + AddressFamily = $AddressFamily + ErrorAction = 'Stop' + } + + $currentIPs = @(Get-NetIPAddress @getNetIPAddressParameters) + + # Remove any IP addresses on the specified interface + if ($currentIPs) + { + foreach ($currentIP in $currentIPs) + { + $removeIP = $false + + if ($currentIP.IPAddress -notin ($IPAddress -replace '\/\S*', '') -and -not $KeepExistingAddress) + { + $removeIP = $true + } + elseif ($currentIP.IPAddress -in ($IPAddress -replace '\/\S*', '')) + { + $existingIP = $IPAddress | Where-Object { + $_ -match $currentIP.IPAddress + } + + if ($existingIP -ne "$($currentIP.IPAddress)/$($currentIP.prefixLength)") + { + $removeIP = $true + } + } + + if ($removeIP) + { + $removeNetIPAddressParameters = @{ + IPAddress = $currentIP.IPAddress + InterfaceIndex = $currentIP.InterfaceIndex + AddressFamily = $currentIP.AddressFamily + prefixLength = $currentIP.prefixLength + Confirm = $false + ErrorAction = 'Stop' + } + + Remove-NetIPAddress @removeNetIPAddressParameters + } + } + } + + $ipAddressObject = Get-IPAddressPrefix -IPAddress $IPAddress -AddressFamily $AddressFamily + + foreach ($singleIP in $ipAddressObject) + { + # Build parameter hash table + $newNetIPAddressParameters = @{ + IPAddress = $singleIP.IPAddress + prefixLength = $singleIP.prefixLength + InterfaceAlias = $InterfaceAlias + } + + try + { + # Apply the specified IP configuration + New-NetIPAddress @newNetIPAddressParameters -ErrorAction Stop + } + catch [Microsoft.Management.Infrastructure.CimException] + { + $verifyNetIPAddressAdapterParam = @{ + IPAddress = $singleIP.IPAddress + prefixLength = $singleIP.prefixLength + } + <# + Setting New-NetIPaddress will throw [Microsoft.Management.Infrastructure.CimException] if + the IP address is already set. Need to check to make sure the IP is set on correct interface + #> + $verifyNetIPAddressAdapter = Get-NetIPAddress @verifyNetIPAddressAdapterParam -ErrorAction SilentlyContinue + + if ($verifyNetIPAddressAdapter.InterfaceAlias -eq $InterfaceAlias) + { + # The IP Address is already set on the correct interface + Write-Verbose -Message ( @("$($MyInvocation.MyCommand): " + $($script:localizedData.IPAddressMatchMessage) + ) -join '' ) + } + else + { + Write-Error -Message ( @( + "$($MyInvocation.MyCommand): " + $($script:localizedData.IPAddressDoesNotMatchInterfaceAliasMessage) -f $InterfaceAlias,$verifyNetIPAddressAdapter.InterfaceAlias + ) -join '' ) + } + continue + } + + Write-Verbose -Message ( @("$($MyInvocation.MyCommand): " + $($script:localizedData.IPAddressSetStateMessage) + ) -join '' ) + } +} # Set-TargetResource + +<# + .SYNOPSIS + Tests the IP address on the interface. + + .PARAMETER IPAddress + The desired IP address. + + .PARAMETER InterfaceAlias + Alias of the network interface for which the IP address should be set. + + .PARAMETER AddressFamily + IP address family. + + .PARAMETER KeepExistingAddress + Indicates whether or not existing IP addresses on an interface will be retained. +#> +function Test-TargetResource +{ + [CmdletBinding()] + [OutputType([System.Boolean])] + param + ( + [Parameter()] + [ValidateNotNullOrEmpty()] + [System.String[]] + $IPAddress, + + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $InterfaceAlias, + + [Parameter(Mandatory = $true)] + [ValidateSet('IPv4', 'IPv6')] + [System.String] + $AddressFamily = 'IPv4', + + [Parameter()] + [System.Boolean] + $KeepExistingAddress = $false + ) + + # Flag to signal whether settings are correct + [System.Boolean] $desiredConfigurationMatch = $true + + Write-Verbose -Message ( @("$($MyInvocation.MyCommand): " + $($script:localizedData.CheckingIPAddressMessage) + ) -join '') + + Assert-ResourceProperty @PSBoundParameters + + <# + Get the current IP Address based on the parameters given. + First make sure that adapter is available + #> + [System.Boolean] $adapterBindingReady = $false + [System.DateTime] $startTime = Get-Date + + while (-not $adapterBindingReady -and (((Get-Date) - $startTime).TotalSeconds) -lt 30) + { + $getNetIPAddressParameters = @{ + InterfaceAlias = $InterfaceAlias + AddressFamily = $AddressFamily + ErrorAction = 'SilentlyContinue' + } + + $currentIPs = @(Get-NetIPAddress @getNetIPAddressParameters) + + if ($currentIPs) + { + $adapterBindingReady = $true + } + else + { + Start-Sleep -Milliseconds 200 + } + } # while + + $ipAddressObject = Get-IPAddressPrefix -IPAddress $IPAddress -AddressFamily $AddressFamily + + # Test if the IP Address passed is present + foreach ($singleIP in $ipAddressObject) + { + $prefixLength = $singleIP.prefixLength + + if ($singleIP.IPAddress -notin $currentIPs.IPAddress) + { + Write-Verbose -Message ( @( + "$($MyInvocation.MyCommand): " + $($script:localizedData.IPAddressDoesNotMatchMessage) -f $singleIP, $currentIPs.IPAddress + ) -join '' ) + + $desiredConfigurationMatch = $false + } + else + { + Write-Verbose -Message ( @("$($MyInvocation.MyCommand): " + $($script:localizedData.IPAddressMatchMessage) + ) -join '') + + # Filter the IP addresses for the IP address to check + $filterIP = $currentIPs.Where( + { + $_.IPAddress -eq $singleIP.IPAddress + } + ) + + # Only test the Prefix Length if the IP address is present + if (-not $filterIP.prefixLength.Equals([System.Byte] $prefixLength)) + { + Write-Verbose -Message ( @( + "$($MyInvocation.MyCommand): " + $($script:localizedData.prefixLengthDoesNotMatchMessage) -f $prefixLength, $currentIPs.prefixLength + ) -join '' ) + + $desiredConfigurationMatch = $false + } + else + { + Write-Verbose -Message ( @( "$($MyInvocation.MyCommand): " + $($script:localizedData.prefixLengthMatchMessage) + ) -join '' ) + } + } + } + return $desiredConfigurationMatch +} # Test-TargetResource + +<# + .SYNOPSIS + Check the IP Address details are valid and do not conflict with Address family. + Also checks the prefix length and ensures the interface exists. + If any problems are detected an exception will be thrown. + + .PARAMETER IPAddress + The desired IP address. + + .PARAMETER InterfaceAlias + Alias of the network interface for which the IP address should be set. + + .PARAMETER AddressFamily + IP address family. + + .PARAMETER KeepExistingAddress + Indicates whether or not existing IP addresses on an interface will be retained. +#> +function Assert-ResourceProperty +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String[]] + $IPAddress, + + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $InterfaceAlias, + + [Parameter()] + [ValidateSet('IPv4', 'IPv6')] + [System.String] + $AddressFamily = 'IPv4', + + [Parameter()] + [System.Boolean] + $KeepExistingAddress = $false + ) + + $prefixLengthArray = ($IPAddress -split '/')[1] + + if ($prefixLengthArray.Count -ne $IPAddress.Count) + { + # Return the prefix length of each IP address specified + $prefixLengthArray = $IPAddress | Foreach-Object { + if ($_ -match '\/\d{1,3}') + { + ($_ -split '/')[1] + } + else + { + if ($_.split('.')[0] -in (0..127)) + { + $prefixLength = 8 + } + elseif ($_.split('.')[0] -in (128..191)) + { + $prefixLength = 16 + } + elseif ($_.split('.')[0] -in (192..223)) + { + $prefixLength = 24 + } + if ($AddressFamily -eq 'IPv6') + { + $prefixLength = 64 + } + $prefixLength + } + } + } + + if (-not (Get-NetAdapter | Where-Object -Property Name -EQ $InterfaceAlias )) + { + New-InvalidArgumentException ` + -Message $($($script:localizedData.InterfaceNotAvailableError) -f $InterfaceAlias) ` + -ArgumentName 'InterfaceAlias' + } + + foreach ($singleIPAddress in $IPAddress) + { + $singleIP = ($singleIPAddress -split '/')[0] + + Assert-IPAddress -Address $singleIP -AddressFamily $AddressFamily + } + + foreach ($prefixLength in $prefixLengthArray) + { + $prefixLength = [uint32]::Parse($prefixLength) + + if (( + ($AddressFamily -eq 'IPv4') ` + -and (($prefixLength -lt [uint32]0) -or ($prefixLength -gt [uint32]32)) + ) -or ( + ($AddressFamily -eq 'IPv6') ` + -and (($prefixLength -lt [uint32]0) -or ($prefixLength -gt [uint32]128)) + )) + { + New-InvalidArgumentException ` + -Message $($($script:localizedData.PrefixLengthError) -f $prefixLength, $AddressFamily) ` + -ArgumentName 'IPAddress' + } + } +} # Assert-ResourceProperty + +Export-ModuleMember -function *-TargetResource diff --git a/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_IPAddress/DSC_IPAddress.schema.mof b/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_IPAddress/DSC_IPAddress.schema.mof new file mode 100644 index 0000000..1e5beda --- /dev/null +++ b/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_IPAddress/DSC_IPAddress.schema.mof @@ -0,0 +1,8 @@ +[ClassVersion("1.0.0.0"), FriendlyName("IPAddress")] +class DSC_IPAddress : OMI_BaseResource +{ + [Write, Description("The desired IP address, optionally including prefix length using CIDR notation.")] string IPAddress[]; + [Key, Description("Alias of the network interface for which the IP address should be set.")] string InterfaceAlias; + [Key, Description("IP address family.") ,ValueMap{"IPv4", "IPv6"},Values{"IPv4", "IPv6"}] string AddressFamily; + [Write, Description("Indicates whether or not existing IP addresses on an interface will be retained.")] boolean KeepExistingAddress; +}; diff --git a/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_IPAddress/README.MD b/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_IPAddress/README.MD new file mode 100644 index 0000000..f3226df --- /dev/null +++ b/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_IPAddress/README.MD @@ -0,0 +1,4 @@ +# Description + +This resource is used to control a node's IP address. This can be used in +conjunction with disabling DHCP to set static IP addresses. diff --git a/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_IPAddress/en-US/DSC_IPAddress.strings.psd1 b/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_IPAddress/en-US/DSC_IPAddress.strings.psd1 new file mode 100644 index 0000000..a0ea455 --- /dev/null +++ b/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_IPAddress/en-US/DSC_IPAddress.strings.psd1 @@ -0,0 +1,15 @@ +# Localized resources for DSC_IPAddress + +ConvertFrom-StringData @' + GettingIPAddressMessage = Getting the IP Address. + ApplyingIPAddressMessage = Applying the IP Address. + IPAddressSetStateMessage = IP Interface was set to the desired state. + CheckingIPAddressMessage = Checking the IP Address. + IPAddressDoesNotMatchMessage = IP Address does NOT match desired state. Expected {0}, actual {1}. + IPAddressMatchMessage = IP Address is in desired state. + IPAddressDoesNotMatchInterfaceAliasMessage = IP Address set on different InterfaceAlias. Expected {0}, actual {1}. + PrefixLengthDoesNotMatchMessage = Prefix Length does NOT match desired state. Expected {0}, actual {1}. + PrefixLengthMatchMessage = Prefix Length is in desired state. + InterfaceNotAvailableError = Interface "{0}" is not available. Please select a valid interface and try again. + PrefixLengthError = A Prefix Length of {0} is not valid for {1} addresses. Please correct the Prefix Length and try again. +'@ diff --git a/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_IPAddress/en-US/about_IPAddress.help.txt b/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_IPAddress/en-US/about_IPAddress.help.txt new file mode 100644 index 0000000..f8c227a --- /dev/null +++ b/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_IPAddress/en-US/about_IPAddress.help.txt @@ -0,0 +1,177 @@ +.NAME + IPAddress + +.DESCRIPTION + This resource is used to control a node's IP address. This can be used in + conjunction with disabling DHCP to set static IP addresses. + +.PARAMETER IPAddress + Write - StringArray + The desired IP address, optionally including prefix length using CIDR notation. + +.PARAMETER InterfaceAlias + Key - String + Alias of the network interface for which the IP address should be set. + +.PARAMETER AddressFamily + Key - String + Allowed values: IPv4, IPv6 + IP address family. + +.PARAMETER KeepExistingAddress + Write - Boolean + Indicates whether or not existing IP addresses on an interface will be retained. + +.EXAMPLE 1 + +Disabling DHCP and adding a static IP Address for IPv6 and IPv4 +using default prefix lengths for the matching address classes. + +Configuration IPAddress_AddingStaticIP_Config +{ + Import-DscResource -Module NetworkingDsc + + Node localhost + { + NetIPInterface DisableDhcp + { + InterfaceAlias = 'Ethernet' + AddressFamily = 'IPv4' + Dhcp = 'Disabled' + } + + # If no prefix is supplied IPv6 will default to /64. + IPAddress NewIPv6Address + { + IPAddress = '2001:4898:200:7:6c71:a102:ebd8:f482' + InterfaceAlias = 'Ethernet' + AddressFamily = 'IPV6' + } + + <# + If no prefix is supplied then IPv4 will default to class based: + - Class A - /8 + - Class B - /16 + - Class C - /24 + #> + IPAddress NewIPv4Address + { + IPAddress = '192.168.10.5' + InterfaceAlias = 'Ethernet' + AddressFamily = 'IPV4' + } + } +} + +.EXAMPLE 2 + +Disabling DHCP and adding multiple static IP Addresses for IPv4 and IPv6. + +Configuration IPAddress_AddingMultipleStaticIP_Config +{ + Import-DscResource -Module NetworkingDsc + + Node localhost + { + NetIPInterface DisableDhcp + { + InterfaceAlias = 'Ethernet' + AddressFamily = 'IPv6' + Dhcp = 'Disabled' + } + + IPAddress NewIPv6Address + { + IPAddress = '2001:4898:200:7:6c71:a102:ebd8:f482/64','2001:4598:210:7:6d71:a102:ebe8:f483/64' + InterfaceAlias = 'Ethernet' + AddressFamily = 'IPV6' + } + + IPAddress NewIPv4Address + { + IPAddress = '192.168.10.5/24','192.168.10.6/24' + InterfaceAlias = 'Ethernet' + AddressFamily = 'IPV4' + } + } +} + +.EXAMPLE 3 + +Disabling DHCP and adding a static IP Address for IPv6 and IPv4 +using specified prefixes in CIDR notation. + +Configuration IPAddress_AddingStaticIPWithPrefix_Config +{ + Import-DscResource -Module NetworkingDsc + + Node localhost + { + NetIPInterface DisableDhcp + { + InterfaceAlias = 'Ethernet' + AddressFamily = 'IPv6' + Dhcp = 'Disabled' + } + + IPAddress NewIPv6Address + { + IPAddress = '2001:4898:200:7:6c71:a102:ebd8:f482/64' + InterfaceAlias = 'Ethernet' + AddressFamily = 'IPV6' + } + + IPAddress NewIPv4Address + { + IPAddress = '192.168.10.5/24' + InterfaceAlias = 'Ethernet' + AddressFamily = 'IPV4' + } + } +} + +.EXAMPLE 4 + +Disabling DHCP and adding a static IP Address for IPv6 and IPv4 +using default prefix lengths for the matching address classes. +Any existing IP addresses will be retained on the network adapter. + +Configuration IPAddress_AddingStaticIPKeepSettings_Config +{ + Import-DscResource -Module NetworkingDsc + + Node localhost + { + NetIPInterface DisableDhcp + { + InterfaceAlias = 'Ethernet' + AddressFamily = 'IPv4' + Dhcp = 'Disabled' + } + + # If no prefix is supplied IPv6 will default to /64. + IPAddress NewIPv6Address + { + IPAddress = '2001:4898:200:7:6c71:a102:ebd8:f482' + InterfaceAlias = 'Ethernet' + AddressFamily = 'IPV6' + KeepExistingAddress = $true + } + + <# + If no prefix is supplied then IPv4 will default to class based: + - Class A - /8 + - Class B - /16 + - Class C - /24 + #> + IPAddress NewIPv4Address + { + IPAddress = '192.168.10.5' + InterfaceAlias = 'Ethernet' + AddressFamily = 'IPV4' + KeepExistingAddress = $true + } + } +} + + diff --git a/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_IPAddressOption/DSC_IPAddressOption.psm1 b/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_IPAddressOption/DSC_IPAddressOption.psm1 new file mode 100644 index 0000000..8c90074 --- /dev/null +++ b/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_IPAddressOption/DSC_IPAddressOption.psm1 @@ -0,0 +1,131 @@ +$modulePath = Join-Path -Path (Split-Path -Path (Split-Path -Path $PSScriptRoot -Parent) -Parent) -ChildPath 'Modules' + +# Import the Networking Common Modules +Import-Module -Name (Join-Path -Path $modulePath ` + -ChildPath (Join-Path -Path 'NetworkingDsc.Common' ` + -ChildPath 'NetworkingDsc.Common.psm1')) + +Import-Module -Name (Join-Path -Path $modulePath -ChildPath 'DscResource.Common') + +# Import Localization Strings +$script:localizedData = Get-LocalizedData -DefaultUICulture 'en-US' + +<# + .SYNOPSIS + Returns the current state of an IP address option. + + .PARAMETER IPAddress + The desired IP address. + + .PARAMETER SkipAsSource + The skip as source option. +#> +function Get-TargetResource +{ + [CmdletBinding()] + [OutputType([System.Collections.Hashtable])] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $IPAddress, + + [Parameter()] + [System.Boolean] + $SkipAsSource = $false + ) + + Write-Verbose -Message ( @( "$($MyInvocation.MyCommand): " + $($script:localizedData.GettingIPAddressOptionMessage -f $IPAddress) + ) -join '') + + $currentIPAddress = Get-NetIPAddress -IPAddress $IPAddress + + $returnValue = @{ + IPAddress = $IPAddress + SkipAsSource = $currentIPAddress.SkipAsSource + } + + return $returnValue +} + +<# + .SYNOPSIS + Set the IP address options. + + .PARAMETER IPAddress + The desired IP address. + + .PARAMETER SkipAsSource + The skip as source option. +#> +function Set-TargetResource +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $IPAddress, + + [Parameter()] + [System.Boolean] + $SkipAsSource = $false + ) + + Write-Verbose -Message ( @( "$($MyInvocation.MyCommand): " + $($script:localizedData.ApplyingIPAddressOptionMessage -f $IPAddress) + ) -join '') + + $currentConfig = Get-TargetResource @PSBoundParameters + + if ($currentConfig.SkipAsSource -ne $SkipAsSource) + { + Set-NetIPAddress -IPAddress $IPAddress -SkipAsSource $SkipAsSource + } +} + +<# + .SYNOPSIS + Tests the IP address options. + + .PARAMETER IPAddress + The desired IP address. + + .PARAMETER SkipAsSource + The skip as source option. +#> +function Test-TargetResource +{ + [CmdletBinding()] + [OutputType([System.Boolean])] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $IPAddress, + + [Parameter()] + [System.Boolean] + $SkipAsSource = $false + ) + + # Flag to signal whether settings are correct + $desiredConfigurationMatch = $true + + Write-Verbose -Message ( @("$($MyInvocation.MyCommand): " + $($script:localizedData.CheckingIPAddressOptionMessage -f $IPAddress) + ) -join '') + + $currentConfig = Get-TargetResource @PSBoundParameters + + $desiredConfigurationMatch = $desiredConfigurationMatch -and + $currentConfig.SkipAsSource -eq $SkipAsSource + + return $desiredConfigurationMatch +} + +Export-ModuleMember -function *-TargetResource diff --git a/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_IPAddressOption/DSC_IPAddressOption.schema.mof b/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_IPAddressOption/DSC_IPAddressOption.schema.mof new file mode 100644 index 0000000..c046bdb --- /dev/null +++ b/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_IPAddressOption/DSC_IPAddressOption.schema.mof @@ -0,0 +1,6 @@ +[ClassVersion("1.0.0.0"), FriendlyName("IPAddressOption")] +class DSC_IPAddressOption : OMI_BaseResource +{ + [Key, Description("The target IP address, must already be configured on the system.")] string IPAddress; + [Write, Description("Skip as source option of the ip address.")] boolean SkipAsSource; +}; diff --git a/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_IPAddressOption/README.MD b/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_IPAddressOption/README.MD new file mode 100644 index 0000000..6c37c0e --- /dev/null +++ b/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_IPAddressOption/README.MD @@ -0,0 +1,8 @@ +# Description + +This resource is used to control a node's IP address options. This can be used +to enable or disable the SkipAsSource option of an IP address. + +This resource is used in addition to xIPAddress to set the SkipAsSource for a +single IP address when an adapter has more than one address assigned. When using +this resource it allows the options of a single IP address to be set. diff --git a/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_IPAddressOption/en-US/DSC_IPAddressOption.strings.psd1 b/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_IPAddressOption/en-US/DSC_IPAddressOption.strings.psd1 new file mode 100644 index 0000000..4ae29ce --- /dev/null +++ b/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_IPAddressOption/en-US/DSC_IPAddressOption.strings.psd1 @@ -0,0 +1,7 @@ +# Localized resources for DSC_IPAddressOption + +ConvertFrom-StringData @' + GettingIPAddressOptionMessage = Getting the options of the IP address {0}. + ApplyingIPAddressOptionMessage = Applying the options of the IP address {0}. + CheckingIPAddressOptionMessage = Checking the options of the IP address {0}. +'@ diff --git a/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_IPAddressOption/en-US/about_IPAddressOption.help.txt b/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_IPAddressOption/en-US/about_IPAddressOption.help.txt new file mode 100644 index 0000000..6ca9002 --- /dev/null +++ b/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_IPAddressOption/en-US/about_IPAddressOption.help.txt @@ -0,0 +1,38 @@ +.NAME + IPAddressOption + +.DESCRIPTION + This resource is used to control a node's IP address options. This can be used + to enable or disable the SkipAsSource option of an IP address. + + This resource is used in addition to xIPAddress to set the SkipAsSource for a + single IP address when an adapter has more than one address assigned. When using + this resource it allows the options of a single IP address to be set. + +.PARAMETER IPAddress + Key - String + The target IP address, must already be configured on the system. + +.PARAMETER SkipAsSource + Write - Boolean + Skip as source option of the ip address. + +.EXAMPLE 1 + +Change the SkipAsSource option for a single IP address. + +Configuration IPAddressOption_SetSkipAsSource_Config +{ + Import-DscResource -Module NetworkingDsc + + Node localhost + { + IPAddressOption SetSkipAsSource + { + IPAddress = '192.168.10.5' + SkipAsSource = $true + } + } +} + + diff --git a/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_NetAdapterAdvancedProperty/DSC_NetAdapterAdvancedProperty.psm1 b/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_NetAdapterAdvancedProperty/DSC_NetAdapterAdvancedProperty.psm1 new file mode 100644 index 0000000..b9034af --- /dev/null +++ b/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_NetAdapterAdvancedProperty/DSC_NetAdapterAdvancedProperty.psm1 @@ -0,0 +1,221 @@ +$modulePath = Join-Path -Path (Split-Path -Path (Split-Path -Path $PSScriptRoot -Parent) -Parent) -ChildPath 'Modules' + +# Import the Networking Common Modules +Import-Module -Name (Join-Path -Path $modulePath ` + -ChildPath (Join-Path -Path 'NetworkingDsc.Common' ` + -ChildPath 'NetworkingDsc.Common.psm1')) + +Import-Module -Name (Join-Path -Path $modulePath -ChildPath 'DscResource.Common') + +# Import Localization Strings +$script:localizedData = Get-LocalizedData -DefaultUICulture 'en-US' + +<# + .SYNOPSIS + Gets the current value of an advanced property. + + .PARAMETER NetworkAdapterName + Specifies the name of the network adapter to set the advanced property for. + + .PARAMETER RegistryKeyword + Specifies the registry keyword that should be in desired state. + + .PARAMETER RegistryValue + Specifies the value of the registry keyword. +#> +function Get-TargetResource +{ + [CmdletBinding()] + [OutputType([System.Collections.Hashtable])] + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $NetworkAdapterName, + + [Parameter(Mandatory = $true)] + [System.String] + $RegistryKeyword, + + [Parameter(Mandatory = $true)] + [System.String] + $RegistryValue + ) + + Write-Verbose -Message ( @( + "$($MyInvocation.MyCommand): " + $script:localizedData.CheckingNetAdapterMessage + ) -join '') + + try + { + $netAdapterAdvancedProperty = Get-NetAdapterAdvancedProperty ` + -Name $networkAdapterName ` + -RegistryKeyword $RegistryKeyword ` + -ErrorAction Stop + } + catch + { + New-InvalidOperationException ` + -Message ($script:localizedData.NetAdapterNotFoundMessage) + } + + if ($netAdapterAdvancedProperty) + { + Write-Verbose -Message ( @( + "$($MyInvocation.MyCommand): " + $($script:localizedData.NetAdapterTestingStateMessage -f $NetworkAdapterName, $RegistryKeyword) + ) -join '') + + $result = @{ + NetworkAdapterName = $NetworkAdapterName + RegistryKeyword = $RegistryKeyword + DisplayValue = $netAdapterAdvancedProperty.DisplayValue + RegistryValue = $netAdapterAdvancedProperty.RegistryValue + } + + return $result + } +} + +<# + .SYNOPSIS + Sets the current value of an advanced property. + + .PARAMETER NetworkAdapterName + Specifies the name of the network adapter to set the advanced property for. + + .PARAMETER RegistryKeyword + Specifies the registry keyword that should be in desired state. + + .PARAMETER RegistryValue + Specifies the value of the registry keyword. +#> +function Set-TargetResource +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $NetworkAdapterName, + + [Parameter(Mandatory = $true)] + [System.String] + $RegistryKeyword, + + [Parameter(Mandatory = $true)] + [System.String] + $RegistryValue + ) + + Write-Verbose -Message ( @( + "$($MyInvocation.MyCommand): " + $script:localizedData.CheckingNetAdapterMessage + ) -join '') + + try + { + $netAdapterAdvancedProperty = Get-NetAdapterAdvancedProperty ` + -Name $networkAdapterName ` + -RegistryKeyword $RegistryKeyword ` + -ErrorAction Stop + } + catch + { + New-InvalidOperationException ` + -Message ($script:localizedData.NetAdapterNotFoundMessage) + } + + if ($netAdapterAdvancedProperty) + { + Write-Verbose -Message ( @( + "$($MyInvocation.MyCommand): " + $($script:localizedData.NetAdapterTestingStateMessage -f $NetworkAdapterName, $RegistryKeyword) + ) -join '') + + if ($RegistryValue -ne $netAdapterAdvancedProperty.RegistryValue) + { + $netadapterRegistryValue = $netAdapterAdvancedProperty.RegistryValue + Write-Verbose -Message ( @( + "$($MyInvocation.MyCommand): " + $($script:localizedData.NetAdapterApplyingChangesMessage -f ` + $NetworkAdapterName, $RegistryKeyword, "$netadapterRegistryValue", $RegistryValue ) + ) -join '') + + Set-NetAdapterAdvancedProperty ` + -RegistryValue $RegistryValue ` + -Name $networkAdapterName ` + -RegistryKeyword $RegistryKeyword + } + } +} + +<# + .SYNOPSIS + Tests the current value of an advanced property. + + .PARAMETER NetworkAdapterName + Specifies the name of the network adapter to set the advanced property for. + + .PARAMETER RegistryKeyword + Specifies the registry keyword that should be in desired state. + + .PARAMETER RegistryValue + Specifies the value of the registry keyword. +#> +function Test-TargetResource +{ + [CmdletBinding()] + [OutputType([System.Boolean])] + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $NetworkAdapterName, + + [Parameter(Mandatory = $true)] + [System.String] + $RegistryKeyword, + + [Parameter(Mandatory = $true)] + [System.String] + $RegistryValue + ) + + Write-Verbose -Message ( @( + "$($MyInvocation.MyCommand): " + $script:localizedData.CheckingNetAdapterMessage + ) -join '') + + try + { + $netAdapterAdvancedProperty = Get-NetAdapterAdvancedProperty ` + -Name $networkAdapterName ` + -RegistryKeyword $RegistryKeyword ` + -ErrorAction Stop + } + catch + { + New-InvalidOperationException ` + -Message ($script:localizedData.NetAdapterNotFoundMessage) + } + + if ($netAdapterAdvancedProperty) + { + Write-Verbose -Message ( @( + "$($MyInvocation.MyCommand): " + $script:localizedData.NetAdapterTestingStateMessage -f ` + $NetworkAdapterName, $RegistryKeyword + ) -join '') + + if ($RegistryValue -eq $netAdapterAdvancedProperty.RegistryValue) + { + return $true + } + else + { + return $false + } + } +} diff --git a/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_NetAdapterAdvancedProperty/DSC_NetAdapterAdvancedProperty.schema.mof b/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_NetAdapterAdvancedProperty/DSC_NetAdapterAdvancedProperty.schema.mof new file mode 100644 index 0000000..4030093 --- /dev/null +++ b/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_NetAdapterAdvancedProperty/DSC_NetAdapterAdvancedProperty.schema.mof @@ -0,0 +1,8 @@ +[ClassVersion("1.0.0.0"), FriendlyName("NetAdapterAdvancedProperty")] +class DSC_NetAdapterAdvancedProperty : OMI_BaseResource +{ + [Key, Description("Specifies the name of the network adapter to set the advanced property for.")] String NetworkAdapterName; + [Key, Description("Specifies the registry keyword that should be in desired state.")] String RegistryKeyword; + [Required, Description("Specifies the value of the registry keyword.")] String RegistryValue; + [Read, Description("Output Display value of selected RegistryKeyword.")] String DisplayValue; +}; diff --git a/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_NetAdapterAdvancedProperty/README.MD b/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_NetAdapterAdvancedProperty/README.MD new file mode 100644 index 0000000..9413f89 --- /dev/null +++ b/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_NetAdapterAdvancedProperty/README.MD @@ -0,0 +1,59 @@ +# Description + +This resource is used to set advanced properties on a network adapter +by setting the value of a registry keyword. +The list of available registry keywords will be dependent on the network +adapter driver. +**Please check the supported registry keywords for your adapter before +creating a configuration.** + +The following is a list of common registry keywords that may apply to +your network adapter: + +- AdaptiveIFS +- ITR +- LogLinkStateEvent +- MasterSlave +- NetworkAddress +- MaxRxRing1Length +- NumRxBuffersSmall +- RxIntModeration +- RxIntModerationProfile +- TxIntModerationProfile +- VlanID +- WaitAutoNegComplete +- *DcbxMode +- *EncapsulatedPacketTaskOffload +- *FlowControl +- *InterruptModeration +- *IPChecksumOffloadIPv4 +- *JumboPacket +- *LsoV2IPv4 +- *LsoV2IPv6 +- *MaxRssProcessors +- *NetworkDirect +- *NumaNodeId +- *NumRssQueues +- *PacketDirect +- *PriorityVLANTag +- *QOS +- *ReceiveBuffers +- *RecvCompletionMethod +- *RoceMaxFrameSize +- *RscIPv4 +- *RSS +- *RssBaseProcNumber +- *RssMaxProcNumber +- *RssOnHostVPorts +- *RSSProfile +- *SpeedDuplex +- *Sriov +- *TCPChecksumOffloadIPv4 +- *TCPChecksumOffloadIPv6 +- *TCPUDPChecksumOffloadIPv4 +- *TCPUDPChecksumOffloadIPv6 +- *TransmitBuffers +- *UDPChecksumOffloadIPv4 +- *UDPChecksumOffloadIPv6 +- *VMQ +- *VMQVlanFiltering diff --git a/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_NetAdapterAdvancedProperty/en-US/DSC_NetAdapterAdvancedProperty.strings.psd1 b/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_NetAdapterAdvancedProperty/en-US/DSC_NetAdapterAdvancedProperty.strings.psd1 new file mode 100644 index 0000000..3af81a3 --- /dev/null +++ b/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_NetAdapterAdvancedProperty/en-US/DSC_NetAdapterAdvancedProperty.strings.psd1 @@ -0,0 +1,8 @@ +# Localized resources for DSC_NetAdapterAdvancedProperty + +ConvertFrom-StringData @' + CheckingNetAdapterMessage = Checking if network adapter exists or not. + NetAdapterNotFoundMessage = Network adapter not found. + NetAdapterTestingStateMessage = Checking if adapter {0} '{1}' is in desired state. + NetAdapterApplyingChangesMessage = Network adapter {0} '{1}' was '{2}', should be '{3}', applying changes. +'@ diff --git a/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_NetAdapterAdvancedProperty/en-US/about_NetAdapterAdvancedProperty.help.txt b/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_NetAdapterAdvancedProperty/en-US/about_NetAdapterAdvancedProperty.help.txt new file mode 100644 index 0000000..e350ea3 --- /dev/null +++ b/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_NetAdapterAdvancedProperty/en-US/about_NetAdapterAdvancedProperty.help.txt @@ -0,0 +1,98 @@ +.NAME + NetAdapterAdvancedProperty + +.DESCRIPTION + This resource is used to set advanced properties on a network adapter + by setting the value of a registry keyword. + The list of available registry keywords will be dependent on the network + adapter driver. + **Please check the supported registry keywords for your adapter before + creating a configuration.** + + The following is a list of common registry keywords that may apply to + your network adapter: + + - AdaptiveIFS + - ITR + - LogLinkStateEvent + - MasterSlave + - NetworkAddress + - MaxRxRing1Length + - NumRxBuffersSmall + - RxIntModeration + - RxIntModerationProfile + - TxIntModerationProfile + - VlanID + - WaitAutoNegComplete + - *DcbxMode + - *EncapsulatedPacketTaskOffload + - *FlowControl + - *InterruptModeration + - *IPChecksumOffloadIPv4 + - *JumboPacket + - *LsoV2IPv4 + - *LsoV2IPv6 + - *MaxRssProcessors + - *NetworkDirect + - *NumaNodeId + - *NumRssQueues + - *PacketDirect + - *PriorityVLANTag + - *QOS + - *ReceiveBuffers + - *RecvCompletionMethod + - *RoceMaxFrameSize + - *RscIPv4 + - *RSS + - *RssBaseProcNumber + - *RssMaxProcNumber + - *RssOnHostVPorts + - *RSSProfile + - *SpeedDuplex + - *Sriov + - *TCPChecksumOffloadIPv4 + - *TCPChecksumOffloadIPv6 + - *TCPUDPChecksumOffloadIPv4 + - *TCPUDPChecksumOffloadIPv6 + - *TransmitBuffers + - *UDPChecksumOffloadIPv4 + - *UDPChecksumOffloadIPv6 + - *VMQ + - *VMQVlanFiltering + +.PARAMETER NetworkAdapterName + Key - String + Specifies the name of the network adapter to set the advanced property for. + +.PARAMETER RegistryKeyword + Key - String + Specifies the registry keyword that should be in desired state. + +.PARAMETER RegistryValue + Required - String + Specifies the value of the registry keyword. + +.PARAMETER DisplayValue + Read - String + Output Display value of selected RegistryKeyword. + +.EXAMPLE 1 + +This configuration changes the JumboPacket Size. + +Configuration NetAdapterAdvancedProperty_JumboPacket_Config +{ + Import-DSCResource -ModuleName NetworkingDsc + + Node localhost + { + NetAdapterAdvancedProperty JumboPacket9014 + { + NetworkAdapterName = 'Ethernet' + RegistryKeyword = "*JumboPacket" + RegistryValue = 9014 + } + } +} + + diff --git a/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_NetAdapterBinding/DSC_NetAdapterBinding.psm1 b/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_NetAdapterBinding/DSC_NetAdapterBinding.psm1 new file mode 100644 index 0000000..b57d30d --- /dev/null +++ b/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_NetAdapterBinding/DSC_NetAdapterBinding.psm1 @@ -0,0 +1,278 @@ +$modulePath = Join-Path -Path (Split-Path -Path (Split-Path -Path $PSScriptRoot -Parent) -Parent) -ChildPath 'Modules' + +# Import the Networking Common Modules +Import-Module -Name (Join-Path -Path $modulePath ` + -ChildPath (Join-Path -Path 'NetworkingDsc.Common' ` + -ChildPath 'NetworkingDsc.Common.psm1')) + +Import-Module -Name (Join-Path -Path $modulePath -ChildPath 'DscResource.Common') + +# Import Localization Strings +$script:localizedData = Get-LocalizedData -DefaultUICulture 'en-US' + +<# + .SYNOPSIS + Returns the current state of an Adapter Binding on an interface. + + .PARAMETER InterfaceAlias + Specifies the alias of a network interface. Supports the use of '*'. + + .PARAMETER ComponentId + Specifies the underlying name of the transport or filter in the following + form - ms_xxxx, such as ms_tcpip. + + .PARAMETER State + Specifies if the component ID for the Interface should be Enabled or Disabled. +#> +function Get-TargetResource +{ + [CmdletBinding()] + [OutputType([System.Collections.Hashtable])] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $InterfaceAlias, + + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $ComponentId, + + [Parameter()] + [ValidateSet('Enabled', 'Disabled')] + [System.String] + $State = 'Enabled' + ) + + Write-Verbose -Message ( @( "$($MyInvocation.MyCommand): " + $($script:localizedData.GettingNetAdapterBindingMessage -f ` + $InterfaceAlias, $ComponentId) + ) -join '') + + $currentNetAdapterBinding = Get-Binding @PSBoundParameters + + $adapterState = $currentNetAdapterBinding.Enabled | + Sort-Object -Unique + + if ( $adapterState.Count -eq 2) + { + $currentEnabled = 'Mixed' + } + elseif ( $adapterState -eq $true ) + { + $currentEnabled = 'Enabled' + } + else + { + $currentEnabled = 'Disabled' + } + + $returnValue = @{ + InterfaceAlias = $InterfaceAlias + ComponentId = $ComponentId + State = $State + CurrentState = $currentEnabled + } + + return $returnValue +} # Get-TargetResource + +<# + .SYNOPSIS + Sets the Adapter Binding on a specific interface. + + .PARAMETER InterfaceAlias + Specifies the alias of a network interface. Supports the use of '*'. + + .PARAMETER ComponentId + Specifies the underlying name of the transport or filter in the following + form - ms_xxxx, such as ms_tcpip. + + .PARAMETER State + Specifies if the component ID for the Interface should be Enabled or Disabled. +#> +function Set-TargetResource +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $InterfaceAlias, + + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $ComponentId, + + [Parameter()] + [ValidateSet('Enabled', 'Disabled')] + [System.String] + $State = 'Enabled' + ) + + Write-Verbose -Message ( @( "$($MyInvocation.MyCommand): " + $($script:localizedData.ApplyingNetAdapterBindingMessage -f ` + $InterfaceAlias, $ComponentId) + ) -join '') + + $null = Get-Binding @PSBoundParameters + + # Remove the State so we can splat + $null = $PSBoundParameters.Remove('State') + + if ($State -eq 'Enabled') + { + Enable-NetAdapterBinding @PSBoundParameters + + Write-Verbose -Message ( @("$($MyInvocation.MyCommand): " + $($script:localizedData.NetAdapterBindingEnabledMessage -f ` + $InterfaceAlias, $ComponentId) + ) -join '' ) + } + else + { + Disable-NetAdapterBinding @PSBoundParameters + + Write-Verbose -Message ( @("$($MyInvocation.MyCommand): " + $($script:localizedData.NetAdapterBindingDisabledMessage -f ` + $InterfaceAlias, $ComponentId) + ) -join '' ) + } # if +} # Set-TargetResource + +<# + .SYNOPSIS + Tests the current state of an Adapter Binding on an interface. + + .PARAMETER InterfaceAlias + Specifies the alias of a network interface. Supports the use of '*'. + + .PARAMETER ComponentId + Specifies the underlying name of the transport or filter in the following + form - ms_xxxx, such as ms_tcpip. + + .PARAMETER State + Specifies if the component ID for the Interface should be Enabled or Disabled. +#> +function Test-TargetResource +{ + [CmdletBinding()] + [OutputType([System.Boolean])] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $InterfaceAlias, + + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $ComponentId, + + [Parameter()] + [ValidateSet('Enabled', 'Disabled')] + [System.String] + $State = 'Enabled' + ) + + Write-Verbose -Message ( @("$($MyInvocation.MyCommand): " + $($script:localizedData.CheckingNetAdapterBindingMessage -f ` + $InterfaceAlias, $ComponentId) + ) -join '') + + $currentNetAdapterBinding = Get-Binding @PSBoundParameters + + $adapterState = $currentNetAdapterBinding.Enabled | + Sort-Object -Unique + + if ( $adapterState.Count -eq 2) + { + $currentEnabled = 'Mixed' + } + elseif ( $adapterState -eq $true ) + { + $currentEnabled = 'Enabled' + } + else + { + $currentEnabled = 'Disabled' + } + + # Test if the binding is in the correct state + if ($currentEnabled -ne $State) + { + Write-Verbose -Message ( @("$($MyInvocation.MyCommand): " + $($script:localizedData.NetAdapterBindingDoesNotMatchMessage -f ` + $InterfaceAlias, $ComponentId, $State, $currentEnabled) + ) -join '' ) + + return $false + } + else + { + Write-Verbose -Message ( @("$($MyInvocation.MyCommand): " + $($script:localizedData.NetAdapterBindingMatchMessage -f ` + $InterfaceAlias, $ComponentId) + ) -join '' ) + + return $true + } # if +} # Test-TargetResource + +<# + .SYNOPSIS + Ensures the interface and component Id exists and returns the Net Adapter binding object. + + .PARAMETER InterfaceAlias + Specifies the alias of a network interface. Supports the use of '*'. + + .PARAMETER ComponentId + Specifies the underlying name of the transport or filter in the following + form - ms_xxxx, such as ms_tcpip. + + .PARAMETER State + Specifies if the component ID for the Interface should be Enabled or Disabled. +#> +function Get-Binding +{ + [CmdletBinding()] + [OutputType([Microsoft.Management.Infrastructure.CimInstance])] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $InterfaceAlias, + + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $ComponentId, + + [Parameter()] + [ValidateSet('Enabled', 'Disabled')] + [System.String] + $State = 'Enabled' + ) + + if (-not (Get-NetAdapter -Name $InterfaceAlias -ErrorAction SilentlyContinue)) + { + New-InvalidArgumentException ` + -Message ($script:localizedData.InterfaceNotAvailableError -f $InterfaceAlias) ` + -ArgumentName 'InterfaceAlias' + } # if + + $binding = Get-NetAdapterBinding ` + -InterfaceAlias $InterfaceAlias ` + -ComponentId $ComponentId ` + -ErrorAction Stop + + return $binding +} # Get-Binding + +Export-ModuleMember -function *-TargetResource diff --git a/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_NetAdapterBinding/DSC_NetAdapterBinding.schema.mof b/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_NetAdapterBinding/DSC_NetAdapterBinding.schema.mof new file mode 100644 index 0000000..6bf53d4 --- /dev/null +++ b/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_NetAdapterBinding/DSC_NetAdapterBinding.schema.mof @@ -0,0 +1,8 @@ +[ClassVersion("1.0.0"), FriendlyName("NetAdapterBinding")] +class DSC_NetAdapterBinding : OMI_BaseResource +{ + [Key, Description("Specifies the alias of a network interface. Supports the use of '*'.")] string InterfaceAlias; + [Key, Description("Specifies the underlying name of the transport or filter in the following form - ms_xxxx, such as ms_tcpip.")] string ComponentId; + [Write, Description("Specifies if the component ID for the Interface should be Enabled or Disabled."), ValueMap{"Enabled", "Disabled"}, Values{"Enabled", "Disabled"}] string State; + [Read, Description("Returns the current state of the component ID for the Interfaces."), ValueMap{"Enabled", "Disabled","Mixed"}, Values{"Enabled", "Disabled","Mixed"}] string CurrentState; +}; diff --git a/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_NetAdapterBinding/README.MD b/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_NetAdapterBinding/README.MD new file mode 100644 index 0000000..54cd030 --- /dev/null +++ b/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_NetAdapterBinding/README.MD @@ -0,0 +1,3 @@ +# Description + +This resource is used to bind or unbind transport or filters to a network interface. diff --git a/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_NetAdapterBinding/en-US/DSC_NetAdapterBinding.strings.psd1 b/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_NetAdapterBinding/en-US/DSC_NetAdapterBinding.strings.psd1 new file mode 100644 index 0000000..d920a08 --- /dev/null +++ b/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_NetAdapterBinding/en-US/DSC_NetAdapterBinding.strings.psd1 @@ -0,0 +1,12 @@ +# Localized resources for DSC_NetAdapterBinding + +ConvertFrom-StringData @' + GettingNetAdapterBindingMessage = Getting the '{0}' Inerface '{1}' Binding. + ApplyingNetAdapterBindingMessage = Applying the '{0}' Inerface '{1}' Binding. + NetAdapterBindingEnabledMessage = '{0}' Inerface '{1}' Binding was Enabled. + NetAdapterBindingDisabledMessage = '{0}' Inerface '{1}' Binding was Disabled. + CheckingNetAdapterBindingMessage = Checking the '{0}' Inerface '{1}' Binding. + NetAdapterBindingDoesNotMatchMessage = '{0}' Inerface '{1}' Binding does NOT match desired state. Expected '{2}', actual '{3}'. + NetAdapterBindingMatchMessage = '{0}' Inerface '{1}' Binding is in desired state. + InterfaceNotAvailableError = Interface '{0}' is not available. Please select a valid interface and try again. +'@ diff --git a/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_NetAdapterBinding/en-US/about_NetAdapterBinding.help.txt b/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_NetAdapterBinding/en-US/about_NetAdapterBinding.help.txt new file mode 100644 index 0000000..60997b7 --- /dev/null +++ b/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_NetAdapterBinding/en-US/about_NetAdapterBinding.help.txt @@ -0,0 +1,44 @@ +.NAME + NetAdapterBinding + +.DESCRIPTION + This resource is used to bind or unbind transport or filters to a network interface. + +.PARAMETER InterfaceAlias + Key - String + Specifies the alias of a network interface. Supports the use of '*'. + +.PARAMETER ComponentId + Key - String + Specifies the underlying name of the transport or filter in the following form - ms_xxxx, such as ms_tcpip. + +.PARAMETER State + Write - String + Allowed values: Enabled, Disabled + Specifies if the component ID for the Interface should be Enabled or Disabled. + +.PARAMETER CurrentState + Read - String + Allowed values: Enabled, Disabled, Mixed + Returns the current state of the component ID for the Interfaces. + +.EXAMPLE 1 + +Disabling IPv6 for the Ethernet adapter. + +Configuration NetAdapterBinding_DisableIPv6_Config +{ + Import-DSCResource -ModuleName NetworkingDsc + + Node localhost + { + NetAdapterBinding DisableIPv6 + { + InterfaceAlias = 'Ethernet' + ComponentId = 'ms_tcpip6' + State = 'Disabled' + } + } +} + + diff --git a/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_NetAdapterLso/DSC_NetAdapterLso.psm1 b/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_NetAdapterLso/DSC_NetAdapterLso.psm1 new file mode 100644 index 0000000..839e2d3 --- /dev/null +++ b/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_NetAdapterLso/DSC_NetAdapterLso.psm1 @@ -0,0 +1,256 @@ +$modulePath = Join-Path -Path (Split-Path -Path (Split-Path -Path $PSScriptRoot -Parent) -Parent) -ChildPath 'Modules' + +# Import the Networking Common Modules +Import-Module -Name (Join-Path -Path $modulePath ` + -ChildPath (Join-Path -Path 'NetworkingDsc.Common' ` + -ChildPath 'NetworkingDsc.Common.psm1')) + +Import-Module -Name (Join-Path -Path $modulePath -ChildPath 'DscResource.Common') + +# Import Localization Strings +$script:localizedData = Get-LocalizedData -DefaultUICulture 'en-US' + +<# + .SYNOPSIS + Gets the current state of NetAdapterLso for a adapter. + + .PARAMETER Name + Specifies the name of the network adapter to check. + + .PARAMETER Protocol + Specifies which protocol to target. + + .PARAMETER State + Specifies the LSO state for the protocol. +#> +function Get-TargetResource +{ + [CmdletBinding()] + [OutputType([Hashtable])] + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $Name, + + [Parameter(Mandatory = $true)] + [ValidateSet('V1IPv4', 'IPv4', 'IPv6')] + [System.String] + $Protocol, + + [Parameter(Mandatory = $true)] + [System.Boolean] + $State + ) + + Write-Verbose -Message ( @( + "$($MyInvocation.MyCommand): " + $script:localizedData.CheckingNetAdapterMessage + ) -join '') + + try + { + $netAdapter = Get-NetAdapterLso -Name $Name -ErrorAction Stop + } + catch + { + New-InvalidOperationException ` + -Message ($script:localizedData.NetAdapterNotFoundMessage) + } + + if ($netAdapter) + { + Write-Verbose -Message ( @( + "$($MyInvocation.MyCommand): " + $($script:localizedData.NetAdapterTestingStateMessage -f $Name, $Protocol) + ) -join '') + + $result = @{ + Name = $Name + Protocol = $Protocol + } + + switch ($Protocol) + { + 'V1IPv4' + { + $result.add('State', $netAdapter.V1IPv4Enabled) + } + + 'IPv4' + { + $result.add('State', $netAdapter.IPv4Enabled) + } + + 'IPv6' + { + $result.add('State', $netAdapter.IPv6Enabled) + } + } + + return $result + } +} + +<# + .SYNOPSIS + Sets the NetAdapterLso resource state. + + .PARAMETER Name + Specifies the name of the network adapter to check. + + .PARAMETER Protocol + Specifies which protocol to target. + + .PARAMETER State + Specifies the LSO state for the protocol. +#> +function Set-TargetResource +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $Name, + + [Parameter(Mandatory = $true)] + [ValidateSet('V1IPv4', 'IPv4', 'IPv6')] + [System.String] + $Protocol, + + [Parameter(Mandatory = $true)] + [System.Boolean] + $State + ) + + Write-Verbose -Message ( @( + "$($MyInvocation.MyCommand): " + $script:localizedData.CheckingNetAdapterMessage + ) -join '') + + try + { + $netAdapter = Get-NetAdapterLso -Name $Name -ErrorAction Stop + } + catch + { + New-InvalidOperationException ` + -Message ($script:localizedData.NetAdapterNotFoundMessage) + } + + if ($netAdapter) + { + Write-Verbose -Message ( @( + "$($MyInvocation.MyCommand): " + $($script:localizedData.NetAdapterTestingStateMessage -f $Name, $Protocol) + ) -join '') + + if ($Protocol -eq 'V1IPv4' -and $State -ne $netAdapter.V1IPv4Enabled) + { + Write-Verbose -Message ( @( + "$($MyInvocation.MyCommand): " + $($script:localizedData.NetAdapterApplyingChangesMessage -f ` + $Name, $Protocol, $($netAdapter.V1IPv4Enabled.ToString()), $($State.ToString()) ) + ) -join '') + + Set-NetAdapterLso -Name $Name -V1IPv4Enabled $State + } + elseif ($Protocol -eq 'IPv4' -and $State -ne $netAdapter.IPv4Enabled) + { + Write-Verbose -Message ( @( + "$($MyInvocation.MyCommand): " + $($script:localizedData.NetAdapterApplyingChangesMessage -f ` + $Name, $Protocol, $($netAdapter.IPv4Enabled.ToString()), $($State.ToString()) ) + ) -join '') + + Set-NetAdapterLso -Name $Name -IPv4Enabled $State + } + elseif ($Protocol -eq 'IPv6' -and $State -ne $netAdapter.IPv6Enabled) + { + Write-Verbose -Message ( @( + "$($MyInvocation.MyCommand): " + $($script:localizedData.NetAdapterApplyingChangesMessage -f ` + $Name, $Protocol, $($netAdapter.IPv6Enabled.ToString()), $($State.ToString()) ) + ) -join '') + + Set-NetAdapterLso -Name $Name -IPv6Enabled $State + } + } +} + +<# + .SYNOPSIS + Tests if the NetAdapterLso resource state is desired state. + + .PARAMETER Name + Specifies the name of the network adapter to check. + + .PARAMETER Protocol + Specifies which protocol to target. + + .PARAMETER State + Specifies the LSO state for the protocol. +#> +function Test-TargetResource +{ + [CmdletBinding()] + [OutputType([System.Boolean])] + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $Name, + + [Parameter(Mandatory = $true)] + [ValidateSet('V1IPv4', 'IPv4', 'IPv6')] + [System.String] + $Protocol, + + [Parameter(Mandatory = $true)] + [System.Boolean] + $State + ) + + Write-Verbose -Message ( @( + "$($MyInvocation.MyCommand): " + $script:localizedData.CheckingNetAdapterMessage + ) -join '') + + try + { + $netAdapter = Get-NetAdapterLso -Name $Name -ErrorAction Stop + } + catch + { + New-InvalidOperationException ` + -Message ($script:localizedData.NetAdapterNotFoundMessage) + } + + if ($netAdapter) + { + Write-Verbose -Message ( @( + "$($MyInvocation.MyCommand): " + $script:localizedData.NetAdapterTestingStateMessage -f ` + $Name, $Protocol + ) -join '') + + switch ($Protocol) + { + 'V1IPv4' + { + return ($State -eq $netAdapter.V1IPv4Enabled) + } + + 'IPv4' + { + return ($State -eq $netAdapter.IPv4Enabled) + } + + 'IPv6' + { + return ($State -eq $netAdapter.IPv6Enabled) + } + } + } +} diff --git a/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_NetAdapterLso/DSC_NetAdapterLso.schema.mof b/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_NetAdapterLso/DSC_NetAdapterLso.schema.mof new file mode 100644 index 0000000..783953a --- /dev/null +++ b/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_NetAdapterLso/DSC_NetAdapterLso.schema.mof @@ -0,0 +1,7 @@ +[ClassVersion("1.0.0.0"), FriendlyName("NetAdapterLso")] +class DSC_NetAdapterLso : OMI_BaseResource +{ + [Key, Description("Specifies the name of network adapter.")] String Name; + [Required, Description("Specifies which protocol to make changes to."), ValueMap{"V1IPv4","IPv4","IPv6"}, Values{"V1IPv4","IPv4","IPv6"}] String Protocol; + [Required, Description("Specifies whether LSO should be enabled or disabled.")] Boolean State; +}; diff --git a/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_NetAdapterLso/README.MD b/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_NetAdapterLso/README.MD new file mode 100644 index 0000000..5a4f473 --- /dev/null +++ b/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_NetAdapterLso/README.MD @@ -0,0 +1,4 @@ +# Description + +This resource is used to enable or disable LSO for specific protocols on a +network adapter. diff --git a/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_NetAdapterLso/en-US/DSC_NetAdapterLso.strings.psd1 b/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_NetAdapterLso/en-US/DSC_NetAdapterLso.strings.psd1 new file mode 100644 index 0000000..b0c6e30 --- /dev/null +++ b/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_NetAdapterLso/en-US/DSC_NetAdapterLso.strings.psd1 @@ -0,0 +1,8 @@ +# Localized resources for DSC_NetAdapterLso + +ConvertFrom-StringData @' + CheckingNetAdapterMessage = Checking if network adapter exists or not. + NetAdapterNotFoundMessage = Network adapter not found. + NetAdapterTestingStateMessage = Checking if adapter '{0}' '{1}' is in desired state. + NetAdapterApplyingChangesMessage = Network adapter '{0}' '{1}' was '{2}', should be '{3}', applying changes. +'@ diff --git a/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_NetAdapterLso/en-US/about_NetAdapterLso.help.txt b/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_NetAdapterLso/en-US/about_NetAdapterLso.help.txt new file mode 100644 index 0000000..3ee9fbf --- /dev/null +++ b/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_NetAdapterLso/en-US/about_NetAdapterLso.help.txt @@ -0,0 +1,59 @@ +.NAME + NetAdapterLso + +.DESCRIPTION + This resource is used to enable or disable LSO for specific protocols on a + network adapter. + +.PARAMETER Name + Key - String + Specifies the name of network adapter. + +.PARAMETER Protocol + Required - String + Allowed values: V1IPv4, IPv4, IPv6 + Specifies which protocol to make changes to. + +.PARAMETER State + Required - Boolean + Specifies whether LSO should be enabled or disabled. + +.EXAMPLE 1 + +This configuration disables LSO for IPv6 on the network adapter. + +Configuration NetAdapterLso_DisableLsoIPv6_Config +{ + Import-DSCResource -ModuleName NetworkingDsc + + Node localhost + { + NetAdapterLso DisableLsoIPv6 + { + Name = 'Ethernet' + Protocol = 'IPv6' + State = $false + } + } +} + +.EXAMPLE 2 + +This configuration disables LSO for IPv4 on the network adapter. + +Configuration NetAdapterLso_DisableLsoIPv4_Config +{ + Import-DSCResource -ModuleName NetworkingDsc + + Node localhost + { + NetAdapterLso DisableLsoIPv4 + { + Name = 'Ethernet' + Protocol = 'IPv4' + State = $false + } + } +} + + diff --git a/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_NetAdapterName/DSC_NetAdapterName.psm1 b/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_NetAdapterName/DSC_NetAdapterName.psm1 new file mode 100644 index 0000000..cb61f39 --- /dev/null +++ b/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_NetAdapterName/DSC_NetAdapterName.psm1 @@ -0,0 +1,399 @@ +$modulePath = Join-Path -Path (Split-Path -Path (Split-Path -Path $PSScriptRoot -Parent) -Parent) -ChildPath 'Modules' + +# Import the Networking Common Modules +Import-Module -Name (Join-Path -Path $modulePath ` + -ChildPath (Join-Path -Path 'NetworkingDsc.Common' ` + -ChildPath 'NetworkingDsc.Common.psm1')) + +Import-Module -Name (Join-Path -Path $modulePath -ChildPath 'DscResource.Common') + +# Import Localization Strings +$script:localizedData = Get-LocalizedData -DefaultUICulture 'en-US' + +<# + .SYNOPSIS + This function will get the network adapter based on the provided + parameters. + + .PARAMETER NewName + Specifies the new name of the network adapter. + + .PARAMETER Name + This is the name of the network adapter to find. + + .PARAMETER PhysicalMediaType + This is the media type of the network adapter to find. + + .PARAMETER Status + This is the status of the network adapter to find. + + .PARAMETER MacAddress + This is the MAC address of the network adapter to find. + + .PARAMETER InterfaceDescription + This is the interface description of the network adapter to find. + + .PARAMETER InterfaceIndex + This is the interface index of the network adapter to find. + + .PARAMETER InterfaceGuid + This is the interface GUID of the network adapter to find. + + .PARAMETER DriverDescription + This is the driver description of the network adapter. + + .PARAMETER InterfaceNumber + This is the interface number of the network adapter if more than one + are returned by the parameters. + + .PARAMETER IgnoreMultipleMatchingAdapters + This switch will suppress an error occurring if more than one matching + adapter matches the parameters passed. +#> +function Get-TargetResource +{ + [CmdletBinding()] + [OutputType([System.Collections.Hashtable])] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $NewName, + + [Parameter()] + [ValidateNotNullOrEmpty()] + [System.String] + $Name, + + [Parameter()] + [System.String] + $PhysicalMediaType, + + [Parameter()] + [ValidateNotNullOrEmpty()] + [ValidateSet('Up', 'Disconnected', 'Disabled')] + [System.String] + $Status = 'Up', + + [Parameter()] + [System.String] + $MacAddress, + + [Parameter()] + [System.String] + $InterfaceDescription, + + [Parameter()] + [System.UInt32] + $InterfaceIndex, + + [Parameter()] + [System.String] + $InterfaceGuid, + + [Parameter()] + [System.String] + $DriverDescription, + + [Parameter()] + [System.UInt32] + $InterfaceNumber = 1, + + [Parameter()] + [System.Boolean] + $IgnoreMultipleMatchingAdapters = $false + ) + + Write-Verbose -Message ( @("$($MyInvocation.MyCommand): " + $($script:localizedData.GettingNetAdapterNameMessage -f $NewName) + ) -join '') + + $adapter = Find-NetworkAdapter ` + -Name $NewName ` + -ErrorAction SilentlyContinue + + if (-not $adapter) + { + Write-Verbose -Message ( @( "$($MyInvocation.MyCommand): " + $($script:localizedData.FindNetAdapterMessage) + ) -join '') + + $null = $PSBoundParameters.Remove('NewName') + + $adapter = Find-NetworkAdapter ` + @PSBoundParameters ` + -ErrorAction Stop + } + + Write-Verbose -Message ( @( "$($MyInvocation.MyCommand): " + $($script:localizedData.NetAdapterNameFoundMessage -f $adapter.Name) + ) -join '') + + $returnValue = @{ + Name = $adapter.Name + PhysicalMediaType = $adapter.PhysicalMediaType + Status = $adapter.Status + MacAddress = $adapter.MacAddress + InterfaceDescription = $adapter.InterfaceDescription + InterfaceIndex = $adapter.InterfaceIndex + InterfaceGuid = $adapter.InterfaceGuid + DriverDescription = $adapter.DriverDescription + InterfaceNumber = $InterfaceNumber + IgnoreMultipleMatchingAdapters = $IgnoreMultipleMatchingAdapters + } + + return $returnValue +} # Get-TargetResource + +<# + .SYNOPSIS + This function will rename a network adapter that matches the parameters. + + .PARAMETER NewName + Specifies the new name of the network adapter. + + .PARAMETER Name + This is the name of the network adapter to find. + + .PARAMETER PhysicalMediaType + This is the media type of the network adapter to find. + + .PARAMETER Status + This is the status of the network adapter to find. + + .PARAMETER MacAddress + This is the MAC address of the network adapter to find. + + .PARAMETER InterfaceDescription + This is the interface description of the network adapter to find. + + .PARAMETER InterfaceIndex + This is the interface index of the network adapter to find. + + .PARAMETER InterfaceGuid + This is the interface GUID of the network adapter to find. + + .PARAMETER DriverDescription + This is the driver description of the network adapter. + + .PARAMETER InterfaceNumber + This is the interface number of the network adapter if more than one + are returned by the parameters. + + .PARAMETER IgnoreMultipleMatchingAdapters + This switch will suppress an error occurring if more than one matching + adapter matches the parameters passed. +#> +function Set-TargetResource +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $NewName, + + [Parameter()] + [ValidateNotNullOrEmpty()] + [System.String] + $Name, + + [Parameter()] + [System.String] + $PhysicalMediaType, + + [Parameter()] + [ValidateNotNullOrEmpty()] + [ValidateSet('Up', 'Disconnected', 'Disabled')] + [System.String] + $Status = 'Up', + + [Parameter()] + [System.String] + $MacAddress, + + [Parameter()] + [System.String] + $InterfaceDescription, + + [Parameter()] + [System.UInt32] + $InterfaceIndex, + + [Parameter()] + [System.String] + $InterfaceGuid, + + [Parameter()] + [System.String] + $DriverDescription, + + [Parameter()] + [System.UInt32] + $InterfaceNumber = 1, + + [Parameter()] + [System.Boolean] + $IgnoreMultipleMatchingAdapters = $false + ) + + Write-Verbose -Message ( @( "$($MyInvocation.MyCommand): " + $($script:localizedData.SettingNetAdapterNameMessage -f $NewName) + ) -join '') + + $null = $PSBoundParameters.Remove('NewName') + + $adapter = Find-NetworkAdapter ` + @PSBoundParameters ` + -ErrorAction Stop + + Write-Verbose -Message ( @( "$($MyInvocation.MyCommand): " + $($script:localizedData.RenamingNetAdapterNameMessage -f $adapter.Name, $NewName) + ) -join '') + + $adapter | Rename-NetAdapter -NewName $NewName + + Write-Verbose -Message ( @( "$($MyInvocation.MyCommand): " + $($script:localizedData.NetAdapterNameRenamedMessage -f $NewName) + ) -join '') +} # Set-TargetResource + +<# + .SYNOPSIS + This will check if the network adapter that matches the parameters needs + to be returned. + + .PARAMETER NewName + Specifies the new name of the network adapter. + + .PARAMETER Name + This is the name of the network adapter to find. + + .PARAMETER PhysicalMediaType + This is the media type of the network adapter to find. + + .PARAMETER Status + This is the status of the network adapter to find. + + .PARAMETER MacAddress + This is the MAC address of the network adapter to find. + + .PARAMETER InterfaceDescription + This is the interface description of the network adapter to find. + + .PARAMETER InterfaceIndex + This is the interface index of the network adapter to find. + + .PARAMETER InterfaceGuid + This is the interface GUID of the network adapter to find. + + .PARAMETER DriverDescription + This is the driver description of the network adapter. + + .PARAMETER InterfaceNumber + This is the interface number of the network adapter if more than one + are returned by the parameters. + + .PARAMETER IgnoreMultipleMatchingAdapters + This switch will suppress an error occurring if more than one matching + adapter matches the parameters passed. +#> +function Test-TargetResource +{ + [CmdletBinding()] + [OutputType([System.Boolean])] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $NewName, + + [Parameter()] + [ValidateNotNullOrEmpty()] + [System.String] + $Name, + + [Parameter()] + [System.String] + $PhysicalMediaType, + + [Parameter()] + [ValidateNotNullOrEmpty()] + [ValidateSet('Up', 'Disconnected', 'Disabled')] + [System.String] + $Status = 'Up', + + [Parameter()] + [System.String] + $MacAddress, + + [Parameter()] + [System.String] + $InterfaceDescription, + + [Parameter()] + [System.UInt32] + $InterfaceIndex, + + [Parameter()] + [System.String] + $InterfaceGuid, + + [Parameter()] + [System.String] + $DriverDescription, + + [Parameter()] + [System.UInt32] + $InterfaceNumber = 1, + + [Parameter()] + [System.Boolean] + $IgnoreMultipleMatchingAdapters = $false + ) + + Write-Verbose -Message ( @("$($MyInvocation.MyCommand): " + $($script:localizedData.TestingNetAdapterNameMessage -f $NewName) + ) -join '') + + $null = $PSBoundParameters.Remove('NewName') + + # Can an adapter be found with the new name? + $adapterWithNewName = Find-NetworkAdapter ` + -Name $NewName ` + -Verbose:$Verbose ` + -ErrorAction SilentlyContinue + + if ($adapterWithNewName) + { + # An adapter was found matching the new name + Write-Verbose -Message ( @("$($MyInvocation.MyCommand): " + $($script:localizedData.NetAdapterWithNewNameExistsMessage -f $adapterWithNewName.Name) + ) -join '') + + return $true + } + else + { + # Find an adapter matching the parameters - throw if none can be found + Write-Verbose -Message ( @( "$($MyInvocation.MyCommand): " + $($script:localizedData.FindNetAdapterMessage) + ) -join '') + + $adapter = Find-NetworkAdapter ` + @PSBoundParameters ` + -ErrorAction Stop + + # An adapter was found that needs to be changed to the new name + Write-Verbose -Message ( @("$($MyInvocation.MyCommand): " + $($script:localizedData.NetAdapterNameNotMatchMessage -f $adapter.Name, $NewName) + ) -join '') + + return $false + } # if +} # Test-TargetResource + +Export-ModuleMember -function *-TargetResource diff --git a/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_NetAdapterName/DSC_NetAdapterName.schema.mof b/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_NetAdapterName/DSC_NetAdapterName.schema.mof new file mode 100644 index 0000000..aa27f02 --- /dev/null +++ b/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_NetAdapterName/DSC_NetAdapterName.schema.mof @@ -0,0 +1,15 @@ +[ClassVersion("1.0.0"), FriendlyName("NetAdapterName")] +class DSC_NetAdapterName : OMI_BaseResource +{ + [Key, Description("Specifies the new name of the network adapter.")] String NewName; + [Write, Description("This is the name of the network adapter to find.")] String Name; + [Write, Description("This is the media type of the network adapter to find.")] String PhysicalMediaType; + [Write, Description("This is the status of the network adapter to find."), ValueMap{"Up", "Disconnected", "Disabled"}, Values{"Up", "Disconnected", "Disabled"}] String Status; + [Write, Description("This is the MAC address of the network adapter to find.")] String MacAddress; + [Write, Description("This is the interface description of the network adapter to find.")] String InterfaceDescription; + [Write, Description("This is the interface index of the network adapter to find.")] UInt32 InterfaceIndex; + [Write, Description("This is the interface GUID of the network adapter to find.")] String InterfaceGuid; + [Write, Description("This is the driver description of the network adapter.")] String DriverDescription; + [Write, Description("This is the interface number of the network adapter if more than one are returned by the parameters.")] UInt32 InterfaceNumber; + [Write, Description("This switch will suppress an error occurring if more than one matching adapter matches the parameters passed.")] Boolean IgnoreMultipleMatchingAdapters; +}; diff --git a/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_NetAdapterName/README.MD b/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_NetAdapterName/README.MD new file mode 100644 index 0000000..8ca22ad --- /dev/null +++ b/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_NetAdapterName/README.MD @@ -0,0 +1,3 @@ +# Description + +This resource is used to rename a network interface that matches the search parameters passed in. diff --git a/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_NetAdapterName/en-US/DSC_NetAdapterName.strings.psd1 b/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_NetAdapterName/en-US/DSC_NetAdapterName.strings.psd1 new file mode 100644 index 0000000..5c925d6 --- /dev/null +++ b/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_NetAdapterName/en-US/DSC_NetAdapterName.strings.psd1 @@ -0,0 +1,11 @@ +ConvertFrom-StringData @' + GettingNetAdapterNameMessage = Getting the network adapter Name '{0}'. + FindNetAdapterMessage = Finding network adapter matching search criteria. + NetAdapterNameFoundMessage = network adapter '{0}' found. + SettingNetAdapterNameMessage = Setting the network adapter Name '{0}'. + RenamingNetAdapterNameMessage = Renaming network adapter '{0}' to '{1}'. + NetAdapterNameRenamedMessage = network adapter renamed to '{0}'. + TestingNetAdapterNameMessage = Testing the network adapter Name '{0}'. + NetAdapterWithNewNameExistsMessage = A network adapter was found with the intended new name '{0}' of the Adapter. No rename required. + NetAdapterNameNotMatchMessage = network adapter Name '{0}' does not match the adapter '{1}' that was found. Rename required. +'@ diff --git a/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_NetAdapterName/en-US/about_NetAdapterName.help.txt b/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_NetAdapterName/en-US/about_NetAdapterName.help.txt new file mode 100644 index 0000000..a250fb5 --- /dev/null +++ b/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_NetAdapterName/en-US/about_NetAdapterName.help.txt @@ -0,0 +1,160 @@ +.NAME + NetAdapterName + +.DESCRIPTION + This resource is used to rename a network interface that matches the search parameters passed in. + +.PARAMETER NewName + Key - String + Specifies the new name of the network adapter. + +.PARAMETER Name + Write - String + This is the name of the network adapter to find. + +.PARAMETER PhysicalMediaType + Write - String + This is the media type of the network adapter to find. + +.PARAMETER Status + Write - String + Allowed values: Up, Disconnected, Disabled + This is the status of the network adapter to find. + +.PARAMETER MacAddress + Write - String + This is the MAC address of the network adapter to find. + +.PARAMETER InterfaceDescription + Write - String + This is the interface description of the network adapter to find. + +.PARAMETER InterfaceIndex + Write - UInt32 + This is the interface index of the network adapter to find. + +.PARAMETER InterfaceGuid + Write - String + This is the interface GUID of the network adapter to find. + +.PARAMETER DriverDescription + Write - String + This is the driver description of the network adapter. + +.PARAMETER InterfaceNumber + Write - UInt32 + This is the interface number of the network adapter if more than one are returned by the parameters. + +.PARAMETER IgnoreMultipleMatchingAdapters + Write - Boolean + This switch will suppress an error occurring if more than one matching adapter matches the parameters passed. + +.EXAMPLE 1 + +Rename three network adapters identified by MAC addresses to +Cluster, Management and SMB and then enable DHCP on them. + +Configuration NetAdapterName_RenameNetAdapterMacAddress_Config +{ + Import-DSCResource -ModuleName NetworkingDsc + + Node localhost + { + NetAdapterName RenameNetAdapterCluster + { + NewName = 'Cluster' + MacAddress = '9C-D2-1E-61-B5-DA' + } + + NetIPInterface EnableDhcpClientCluster + { + InterfaceAlias = 'Cluster' + AddressFamily = 'IPv4' + Dhcp = 'Enabled' + } + + NetAdapterName RenameNetAdapterManagement + { + NewName = 'Management' + MacAddress = '9C-D2-1E-61-B5-DB' + } + + NetIPInterface EnableDhcpClientManagement + { + InterfaceAlias = 'Management' + AddressFamily = 'IPv4' + Dhcp = 'Enabled' + } + + NetAdapterName RenameNetAdapterSMB + { + NewName = 'SMB' + MacAddress = '9C-D2-1E-61-B5-DC' + } + + NetIPInterface EnableDhcpClientSMB + { + InterfaceAlias = 'SMB' + AddressFamily = 'IPv4' + Dhcp = 'Enabled' + } + } +} + +.EXAMPLE 2 + +Rename the first three network adapters with Driver Description matching +'Hyper-V Virtual Ethernet Adapter' in consequtive order to Cluster, Management +and SMB and then enable DHCP on them. + +Configuration NetAdapterName_RenameNetAdapterDriver_Config +{ + Import-DSCResource -ModuleName NetworkingDsc + + Node localhost + { + NetAdapterName RenameNetAdapterCluster + { + NewName = 'Cluster' + DriverDescription = 'Hyper-V Virtual Ethernet Adapter' + InterfaceNumber = 1 + } + + NetIPInterface EnableDhcpClientCluster + { + InterfaceAlias = 'Cluster' + AddressFamily = 'IPv4' + Dhcp = 'Enabled' + } + + NetAdapterName RenameNetAdapterManagement + { + NewName = 'Management' + DriverDescription = 'Hyper-V Virtual Ethernet Adapter' + InterfaceNumber = 2 + } + + NetIPInterface EnableDhcpClientManagement + { + InterfaceAlias = 'Management' + AddressFamily = 'IPv4' + Dhcp = 'Enabled' + } + + NetAdapterName RenameNetAdapterSMB + { + NewName = 'SMB' + DriverDescription = 'Hyper-V Virtual Ethernet Adapter' + InterfaceNumber = 3 + } + + NetIPInterface EnableDhcpClientSMB + { + InterfaceAlias = 'SMB' + AddressFamily = 'IPv4' + Dhcp = 'Enabled' + } + } +} + + diff --git a/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_NetAdapterRdma/DSC_NetAdapterRdma.psm1 b/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_NetAdapterRdma/DSC_NetAdapterRdma.psm1 new file mode 100644 index 0000000..26d4c3c --- /dev/null +++ b/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_NetAdapterRdma/DSC_NetAdapterRdma.psm1 @@ -0,0 +1,165 @@ +$modulePath = Join-Path -Path (Split-Path -Path (Split-Path -Path $PSScriptRoot -Parent) -Parent) -ChildPath 'Modules' + +# Import the Networking Common Modules +Import-Module -Name (Join-Path -Path $modulePath ` + -ChildPath (Join-Path -Path 'NetworkingDsc.Common' ` + -ChildPath 'NetworkingDsc.Common.psm1')) + +Import-Module -Name (Join-Path -Path $modulePath -ChildPath 'DscResource.Common') + +# Import Localization Strings +$script:localizedData = Get-LocalizedData -DefaultUICulture 'en-US' + +<# + .SYNOPSIS + Gets the state of the network adapter RDMA. + + .PARAMETER Name + Specifies the name of network adapter for which RDMA needs + to be configured. +#> +function Get-TargetResource +{ + [CmdletBinding()] + [OutputType([Hashtable])] + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $Name + ) + + $configuration = @{ + Name = $Name + } + + try + { + Write-Verbose -Message ($script:localizedData.GetNetAdapterRdmaMessage -f $Name) + + $netAdapterRdma = Get-NetAdapterRdma -Name $Name -ErrorAction Stop + } + catch + { + New-InvalidOperationException ` + -Message ($script:localizedData.NetAdapterNotFoundError -f $Name) + } + + if ($netAdapterRdma) + { + Write-Verbose -Message ($script:localizedData.CheckNetAdapterRdmaMessage -f $Name) + + $configuration.Add('Enabled', $netAdapterRdma.Enabled) + } + + return $configuration +} + +<# + .SYNOPSIS + Sets the state of the network adapter RDMA. + + .PARAMETER Name + Specifies the name of network adapter for which RDMA needs + to be configured. + + .PARAMETER Enabled + Specifies if the RDMA configuration should be enabled or disabled. + Defaults to $true. +#> +function Set-TargetResource +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $Name, + + [Parameter()] + [System.Boolean] + $Enabled = $true + ) + + try + { + Write-Verbose -Message ($script:localizedData.GetNetAdapterRdmaMessage -f $Name) + + $netAdapterRdma = Get-NetAdapterRdma -Name $Name -ErrorAction Stop + } + catch + { + New-InvalidOperationException ` + -Message ($script:localizedData.NetAdapterNotFoundError -f $Name) + } + + if ($netAdapterRdma) + { + Write-Verbose -Message ($script:localizedData.CheckNetAdapterRdmaMessage -f $Name) + + if ($netAdapterRdma.Enabled -ne $Enabled) + { + Write-Verbose -Message ($script:localizedData.SetNetAdapterRdmaMessage -f $Name, $Enabled) + + Set-NetAdapterRdma -Name $Name -Enabled $Enabled + } + } +} + +<# + .SYNOPSIS + Tests the state of the network adapter RDMA. + + .PARAMETER Name + Specifies the name of network adapter for which RDMA needs + to be configured. + + .PARAMETER Enabled + Specifies if the RDMA configuration should be enabled or disabled. + Defaults to $true. +#> +function Test-TargetResource +{ + [CmdletBinding()] + [OutputType([System.Boolean])] + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $Name, + + [Parameter()] + [System.Boolean] + $Enabled = $true + ) + + try + { + Write-Verbose -Message ($script:localizedData.GetNetAdapterRdmaMessage -f $Name) + + $netAdapterRdma = Get-NetAdapterRdma -Name $Name -ErrorAction Stop + } + catch + { + New-InvalidOperationException ` + -Message ($script:localizedData.NetAdapterNotFoundError -f $Name) + } + + if ($netAdapterRdma) + { + Write-Verbose -Message ($script:localizedData.CheckNetAdapterRdmaMessage -f $Name) + + if ($netAdapterRdma.Enabled -ne $Enabled) + { + Write-Verbose -Message ($script:localizedData.NetAdapterRdmaDifferentMessage -f $Name) + + return $false + } + else + { + Write-Verbose -Message ($script:localizedData.NetAdapterRdmaMatchesMessage -f $Name) + + return $true + } + } +} diff --git a/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_NetAdapterRdma/DSC_NetAdapterRdma.schema.mof b/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_NetAdapterRdma/DSC_NetAdapterRdma.schema.mof new file mode 100644 index 0000000..1eb259d --- /dev/null +++ b/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_NetAdapterRdma/DSC_NetAdapterRdma.schema.mof @@ -0,0 +1,6 @@ +[ClassVersion("1.0.0.0"), FriendlyName("NetAdapterRdma")] +class DSC_NetAdapterRdma : OMI_BaseResource +{ + [Key, Description("Specifies the name of network adapter for which RDMA needs to be configured.")] String Name; + [Write, Description("Specifies if the RDMA configuration should be enabled or disabled. Defaults to $true.")] Boolean Enabled; +}; diff --git a/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_NetAdapterRdma/README.MD b/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_NetAdapterRdma/README.MD new file mode 100644 index 0000000..3aace01 --- /dev/null +++ b/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_NetAdapterRdma/README.MD @@ -0,0 +1,3 @@ +# Description + +This resource is used to enable or disable RDMA on a network adapter. diff --git a/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_NetAdapterRdma/en-US/DSC_NetAdapterRdma.strings.psd1 b/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_NetAdapterRdma/en-US/DSC_NetAdapterRdma.strings.psd1 new file mode 100644 index 0000000..70b6c78 --- /dev/null +++ b/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_NetAdapterRdma/en-US/DSC_NetAdapterRdma.strings.psd1 @@ -0,0 +1,10 @@ +# Localized resources for DSC_NetAdapterRdma + +ConvertFrom-StringData @' + GetNetAdapterRdmaMessage = Getting network adapter '{0}' RDMA setting. + NetAdapterNotFoundError = Network adapter '{0}' not found. + CheckNetAdapterRdmaMessage = Checking the RDMA enabled state for network adapter '{0}'. + SetNetAdapterRdmaMessage = Setting network adapter '{0}' RDMA enabled to '{1}'. + NetAdapterRdmaDifferentMessage = RDMA setting for network adapter '{0}' is not in desired state. This will be configured. + NetAdapterRdmaMatchesMessage = RDMA setting for network adapter '{0}' is in desired state. No action needed. +'@ diff --git a/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_NetAdapterRdma/en-US/about_NetAdapterRdma.help.txt b/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_NetAdapterRdma/en-US/about_NetAdapterRdma.help.txt new file mode 100644 index 0000000..df6b501 --- /dev/null +++ b/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_NetAdapterRdma/en-US/about_NetAdapterRdma.help.txt @@ -0,0 +1,51 @@ +.NAME + NetAdapterRdma + +.DESCRIPTION + This resource is used to enable or disable RDMA on a network adapter. + +.PARAMETER Name + Key - String + Specifies the name of network adapter for which RDMA needs to be configured. + +.PARAMETER Enabled + Write - Boolean + Specifies if the RDMA configuration should be enabled or disabled. Defaults to $true. + +.EXAMPLE 1 + +This configuration disables RDMA setting on the network adapter. + +Configuration NetAdapterRdma_DisableRdmaSettings_Config +{ + Import-DSCResource -ModuleName NetworkingDsc + + Node localhost + { + NetAdapterRdma DisableRdmaSettings + { + Name = 'SMB1_1' + Enabled = $false + } + } +} + +.EXAMPLE 2 + +This configuration enables RDMA setting on the network adapter. + +Configuration NetAdapterRdma_EnableRdmaSettings_Config +{ + Import-DSCResource -ModuleName NetworkingDsc + + Node localhost + { + NetAdapterRdma EnableRdmaSettings + { + Name = 'SMB1_1' + Enabled = $true + } + } +} + + diff --git a/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_NetAdapterRsc/DSC_NetAdapterRSC.schema.mof b/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_NetAdapterRsc/DSC_NetAdapterRSC.schema.mof new file mode 100644 index 0000000..15eb704 --- /dev/null +++ b/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_NetAdapterRsc/DSC_NetAdapterRSC.schema.mof @@ -0,0 +1,9 @@ +[ClassVersion("1.0.0.0"), FriendlyName("NetAdapterRsc")] +class DSC_NetAdapterRsc : OMI_BaseResource +{ + [Key, Description("Specifies the Name of network adapter.")] String Name; + [Required, Description("Specifies which protocol to make changes to."), ValueMap{"IPv4","IPv6","All"}, Values{"IPv4","IPv6","All"}] String Protocol; + [Required, Description("Specifies whether RSC should be enabled or disabled.")] Boolean State; + [Read, Description("Returns the current state of RSC for IPv4")] String StateIPv4; + [Read, Description("Returns the current state of RSC for IPv6")] String StateIPv6; +}; diff --git a/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_NetAdapterRsc/DSC_NetAdapterRsc.psm1 b/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_NetAdapterRsc/DSC_NetAdapterRsc.psm1 new file mode 100644 index 0000000..4246dc6 --- /dev/null +++ b/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_NetAdapterRsc/DSC_NetAdapterRsc.psm1 @@ -0,0 +1,245 @@ +$modulePath = Join-Path -Path (Split-Path -Path (Split-Path -Path $PSScriptRoot -Parent) -Parent) -ChildPath 'Modules' + +# Import the Networking Common Modules +Import-Module -Name (Join-Path -Path $modulePath ` + -ChildPath (Join-Path -Path 'NetworkingDsc.Common' ` + -ChildPath 'NetworkingDsc.Common.psm1')) + +Import-Module -Name (Join-Path -Path $modulePath -ChildPath 'DscResource.Common') + +# Import Localization Strings +$script:localizedData = Get-LocalizedData -DefaultUICulture 'en-US' + +<# + .SYNOPSIS + Gets the current state of NetAdapterRSC for a adapter. + + .PARAMETER Name + Specifies the Name of the network adapter to check. + + .PARAMETER Protocol + Specifies which protocol to target. + + .PARAMETER State + Specifies the RSC state for the protocol. +#> +function Get-TargetResource +{ + [CmdletBinding()] + [OutputType([Hashtable])] + param + ( + [Parameter(Mandatory = $true)] + [String] + $Name, + + [Parameter(Mandatory = $true)] + [ValidateSet("IPv4", "IPv6", "All")] + [String] + $Protocol, + + [Parameter(Mandatory = $true)] + [Boolean] + $State + ) + + Write-Verbose -Message ( @( + "$($MyInvocation.MyCommand): " + $script:localizedData.CheckingNetAdapterMessage + ) -join '') + + try + { + $netAdapter = Get-NetAdapterRsc -Name $Name -ErrorAction Stop + } + catch + { + New-InvalidOperationException ` + -Message ($script:localizedData.NetAdapterNotFoundMessage) + } + + if ($netAdapter) + { + Write-Verbose -Message ( @( + "$($MyInvocation.MyCommand): " + $($script:localizedData.NetAdapterTestingStateMessage -f $Name, $Protocol) + ) -join '') + + $result = @{ + Name = $Name + Protocol = $Protocol + } + switch ($Protocol) + { + 'IPv4' + { + $result.add('State', $netAdapter.IPv4Enabled) + $result.add('StateIPv4', $netAdapter.IPv4Enabled) + } + 'IPv6' + { + $result.add('State', $netAdapter.IPv6Enabled) + $result.add('StateIPv6', $netAdapter.IPv6Enabled) + } + 'All' + { + $result.add('State', $netAdapter.IPv4Enabled) + $result.add('StateIPv4', $netAdapter.IPv4Enabled) + $result.add('StateIPv6', $netAdapter.IPv6Enabled) + } + } + + return $result + } +} + +<# + .SYNOPSIS + Sets the NetAdapterRSC resource state. + + .PARAMETER Name + Specifies the Name of the network adapter to check. + + .PARAMETER Protocol + Specifies which protocol to target. + + .PARAMETER State + Specifies the RSC state for the protocol. +#> +function Set-TargetResource +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [String] + $Name, + + [Parameter(Mandatory = $true)] + [ValidateSet("IPv4", "IPv6", "All")] + [String] + $Protocol, + + [Parameter(Mandatory = $true)] + [Boolean] + $State + ) + + Write-Verbose -Message ( @( + "$($MyInvocation.MyCommand): " + $script:localizedData.CheckingNetAdapterMessage + ) -join '') + + try + { + $netAdapter = Get-NetAdapterRsc -Name $Name -ErrorAction Stop + } + catch + { + New-InvalidOperationException ` + -Message ($script:localizedData.NetAdapterNotFoundMessage) + } + + if ($netAdapter) + { + Write-Verbose -Message ( @( + "$($MyInvocation.MyCommand): " + $($script:localizedData.NetAdapterTestingStateMessage -f $Name, $Protocol) + ) -join '') + + if ($Protocol -in ('IPv4', 'All') -and $State -ne $netAdapter.IPv4Enabled) + { + Write-Verbose -Message ( @( + "$($MyInvocation.MyCommand): " + $($script:localizedData.NetAdapterApplyingChangesMessage -f ` + $Name, $Protocol, $($netAdapter.IPv4Enabled.ToString()), $($State.ToString()) ) + ) -join '') + + Set-NetAdapterRsc -Name $Name -IPv4Enabled $State + } + if ($Protocol -in ('IPv6', 'All') -and $State -ne $netAdapter.IPv6Enabled) + { + Write-Verbose -Message ( @( + "$($MyInvocation.MyCommand): " + $($script:localizedData.NetAdapterApplyingChangesMessage -f ` + $Name, $Protocol, $($netAdapter.IPv6Enabled.ToString()), $($State.ToString()) ) + ) -join '') + + Set-NetAdapterRsc -Name $Name -IPv6Enabled $State + } + } +} + +<# + .SYNOPSIS + Tests if the NetAdapterRsc resource state is desired state. + + .PARAMETER Name + Specifies the Name of the network adapter to check. + + .PARAMETER Protocol + Specifies which protocol to target. + + .PARAMETER State + Specifies the RSC state for the protocol. +#> +function Test-TargetResource +{ + [CmdletBinding()] + [OutputType([System.Boolean])] + param + ( + [Parameter(Mandatory = $true)] + [String] + $Name, + + [Parameter(Mandatory = $true)] + [ValidateSet("IPv4", "IPv6", "All")] + [String] + $Protocol, + + [Parameter(Mandatory = $true)] + [Boolean] + $State + ) + + Write-Verbose -Message ( @( + "$($MyInvocation.MyCommand): " + $script:localizedData.CheckingNetAdapterMessage + ) -join '') + + try + { + $netAdapter = Get-NetAdapterRsc -Name $Name -ErrorAction Stop + } + catch + { + New-InvalidOperationException ` + -Message ($script:localizedData.NetAdapterNotFoundMessage) + } + + if ($netAdapter) + { + Write-Verbose -Message ( @( + "$($MyInvocation.MyCommand): " + $script:localizedData.NetAdapterTestingStateMessage -f ` + $Name, $Protocol + ) -join '') + + switch ($Protocol) + { + 'IPv4' + { + return ($State -eq $netAdapter.IPv4Enabled) + } + 'IPv6' + { + return ($State -eq $netAdapter.IPv6Enabled) + } + 'All' + { + return ($State -eq $netAdapter.IPv4Enabled -and $State -eq $netAdapter.IPv6Enabled) + } + } + } +} diff --git a/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_NetAdapterRsc/README.MD b/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_NetAdapterRsc/README.MD new file mode 100644 index 0000000..f32bee7 --- /dev/null +++ b/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_NetAdapterRsc/README.MD @@ -0,0 +1,3 @@ +# Description + +This resource is used to enable or disable RSC (Recv Segment Coalescing) for specific protocols on a network adapter. diff --git a/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_NetAdapterRsc/en-US/DSC_NetAdapterRsc.strings.psd1 b/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_NetAdapterRsc/en-US/DSC_NetAdapterRsc.strings.psd1 new file mode 100644 index 0000000..fba0b5f --- /dev/null +++ b/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_NetAdapterRsc/en-US/DSC_NetAdapterRsc.strings.psd1 @@ -0,0 +1,8 @@ +# Localized resources for DSC_NetAdapterRsc + +ConvertFrom-StringData @' + CheckingNetAdapterMessage = Checking if network adapter exists or not. + NetAdapterNotFoundMessage = Network adapter not found. + NetAdapterTestingStateMessage = Checking if adapter {0} {1} is in desired state. + NetAdapterApplyingChangesMessage = Network adapter {0} {1} was {2}, should be {3}, applying changes. +'@ diff --git a/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_NetAdapterRsc/en-US/about_NetAdapterRsc.help.txt b/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_NetAdapterRsc/en-US/about_NetAdapterRsc.help.txt new file mode 100644 index 0000000..ae66f43 --- /dev/null +++ b/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_NetAdapterRsc/en-US/about_NetAdapterRsc.help.txt @@ -0,0 +1,85 @@ +.NAME + NetAdapterRsc + +.DESCRIPTION + This resource is used to enable or disable RSC (Recv Segment Coalescing) for specific protocols on a network adapter. + +.PARAMETER Name + Key - String + Specifies the Name of network adapter. + +.PARAMETER Protocol + Required - String + Allowed values: IPv4, IPv6, All + Specifies which protocol to make changes to. + +.PARAMETER State + Required - Boolean + Specifies whether RSC should be enabled or disabled. + +.PARAMETER StateIPv4 + Read - String + Returns the current state of RSC for IPv4 + +.PARAMETER StateIPv6 + Read - String + Returns the current state of RSC for IPv6 + +.EXAMPLE 1 + +This configuration disables RSC for IPv6 on the network adapter. + +Configuration NetAdapterRsc_DisableRscIPv6_Config +{ + Import-DSCResource -ModuleName NetworkingDsc + + Node localhost + { + NetAdapterRsc DisableRscIPv6 + { + Name = 'Ethernet' + Protocol = 'IPv6' + State = $false + } + } +} + +.EXAMPLE 2 + +This configuration disables RSC for IPv4 on the network adapter. + +Configuration NetAdapterRsc_DisableRscIPv4_Config +{ + Import-DSCResource -ModuleName NetworkingDsc + + Node localhost + { + NetAdapterRsc DisableRscIPv4 + { + Name = 'Ethernet' + Protocol = 'IPv4' + State = $false + } + } +} + +.EXAMPLE 3 + +This configuration disables RSC on the network adapter. + +Configuration NetAdapterRsc_DisableRscAll_Config +{ + Import-DSCResource -ModuleName NetworkingDsc + + Node localhost + { + NetAdapterRsc DisableRscAll + { + Name = 'Ethernet' + Protocol = 'All' + State = $false + } + } +} + + diff --git a/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_NetAdapterRss/DSC_NetAdapterRss.psm1 b/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_NetAdapterRss/DSC_NetAdapterRss.psm1 new file mode 100644 index 0000000..09b43f9 --- /dev/null +++ b/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_NetAdapterRss/DSC_NetAdapterRss.psm1 @@ -0,0 +1,178 @@ +$modulePath = Join-Path -Path (Split-Path -Path (Split-Path -Path $PSScriptRoot -Parent) -Parent) -ChildPath 'Modules' + +# Import the Networking Common Modules +Import-Module -Name (Join-Path -Path $modulePath ` + -ChildPath (Join-Path -Path 'NetworkingDsc.Common' ` + -ChildPath 'NetworkingDsc.Common.psm1')) + +Import-Module -Name (Join-Path -Path $modulePath -ChildPath 'DscResource.Common') + +# Import Localization Strings +$script:localizedData = Get-LocalizedData -DefaultUICulture 'en-US' + +<# + .SYNOPSIS + Gets the current state of NetAdapterRss for a adapter. + + .PARAMETER Name + Specifies the Name of the network adapter to check. + + .PARAMETER State + Specifies the Rss state for the protocol. +#> +function Get-TargetResource +{ + [CmdletBinding()] + [OutputType([Hashtable])] + param + ( + [Parameter(Mandatory = $true)] + [String] + $Name, + + [Parameter(Mandatory = $true)] + [Boolean] + $Enabled + ) + + Write-Verbose -Message ( @( + "$($MyInvocation.MyCommand): " + $script:localizedData.CheckingNetAdapterMessage + ) -join '') + + try + { + $netAdapter = Get-NetAdapterRss -Name $Name -ErrorAction Stop + } + catch + { + New-InvalidOperationException ` + -Message ($script:localizedData.NetAdapterNotFoundMessage) + } + + if ($netAdapter) + { + Write-Verbose -Message ( @( + "$($MyInvocation.MyCommand): " + $($script:localizedData.NetAdapterTestingStateMessage -f $Name, $Enabled) + ) -join '') + + $result = @{ + Name = $Name + Enabled = $netAdapter.Enabled + } + + return $result + } +} + +<# + .SYNOPSIS + Sets the NetAdapterRss resource state. + + .PARAMETER Name + Specifies the Name of the network adapter to check. + + .PARAMETER State + Specifies the Rss state for the protocol. +#> +function Set-TargetResource +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [String] + $Name, + + [Parameter(Mandatory = $true)] + [Boolean] + $Enabled + ) + + Write-Verbose -Message ( @( + "$($MyInvocation.MyCommand): " + $script:localizedData.CheckingNetAdapterMessage + ) -join '') + + try + { + $netAdapter = Get-NetAdapterRss -Name $Name -ErrorAction Stop + } + catch + { + New-InvalidOperationException ` + -Message ($script:localizedData.NetAdapterNotFoundMessage) + } + + if ($netAdapter) + { + Write-Verbose -Message ( @( + "$($MyInvocation.MyCommand): " + $($script:localizedData.NetAdapterTestingStateMessage -f $Name, $Enabled) + ) -join '') + + if ($Enabled -ne $netAdapter.Enabled) + { + Write-Verbose -Message ( @( + "$($MyInvocation.MyCommand): " + $($script:localizedData.NetAdapterApplyingChangesMessage -f ` + $Name, $Enabled, $($netAdapter.Enabled.ToString()), $($Enabled.ToString()) ) + ) -join '') + + Set-NetAdapterRss -Name $Name -Enabled:$Enabled + } + } +} + +<# + .SYNOPSIS + Tests if the NetAdapterRss resource state is desired state. + + .PARAMETER Name + Specifies the Name of the network adapter to check. + + .PARAMETER State + Specifies the Rss state for the protocol. +#> +function Test-TargetResource +{ + [CmdletBinding()] + [OutputType([System.Boolean])] + param + ( + [Parameter(Mandatory = $true)] + [String] + $Name, + + [Parameter(Mandatory = $true)] + [Boolean] + $Enabled + ) + + Write-Verbose -Message ( @( + "$($MyInvocation.MyCommand): " + $script:localizedData.CheckingNetAdapterMessage + ) -join '') + + try + { + $netAdapter = Get-NetAdapterRss -Name $Name -ErrorAction Stop + } + catch + { + New-InvalidOperationException ` + -Message ($script:localizedData.NetAdapterNotFoundMessage) + } + + if ($netAdapter) + { + Write-Verbose -Message ( @( + "$($MyInvocation.MyCommand): " + $script:localizedData.NetAdapterTestingStateMessage -f ` + $Name, $Enabled + ) -join '') + + return ($Enabled -eq $netAdapter.Enabled) + } +} diff --git a/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_NetAdapterRss/DSC_NetAdapterRss.schema.mof b/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_NetAdapterRss/DSC_NetAdapterRss.schema.mof new file mode 100644 index 0000000..e2b2779 --- /dev/null +++ b/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_NetAdapterRss/DSC_NetAdapterRss.schema.mof @@ -0,0 +1,6 @@ +[ClassVersion("1.0.0.0"), FriendlyName("NetAdapterRss")] +class DSC_NetAdapterRss : OMI_BaseResource +{ + [Key, Description("Specifies the Name of network adapter.")] String Name; + [Required, Description("Specifies whether RSS should be enabled or disabled.")] Boolean Enabled; +}; diff --git a/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_NetAdapterRss/README.MD b/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_NetAdapterRss/README.MD new file mode 100644 index 0000000..c8fb92e --- /dev/null +++ b/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_NetAdapterRss/README.MD @@ -0,0 +1,5 @@ +# Description + +This resource is used to enable or disable RSS (Receive Side Scaling) on a network adapter. +Not all adapters support RSS so there may be no option for this and it will throw an exception that the network adapter is not found. + diff --git a/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_NetAdapterRss/en-US/DSC_NetAdapterRss.strings.psd1 b/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_NetAdapterRss/en-US/DSC_NetAdapterRss.strings.psd1 new file mode 100644 index 0000000..cfd5e86 --- /dev/null +++ b/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_NetAdapterRss/en-US/DSC_NetAdapterRss.strings.psd1 @@ -0,0 +1,8 @@ +# Localized resources for DSC_NetAdapterRss + +ConvertFrom-StringData @' + CheckingNetAdapterMessage = Checking if network adapter exists or not. + NetAdapterNotFoundMessage = Network adapter not found. + NetAdapterTestingStateMessage = Checking if adapter {0} {1} is in desired state. + NetAdapterApplyingChangesMessage = Network adapter {0} {1} was {2}, should be {3}, applying changes. +'@ diff --git a/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_NetAdapterRss/en-US/about_NetAdapterRss.help.txt b/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_NetAdapterRss/en-US/about_NetAdapterRss.help.txt new file mode 100644 index 0000000..90009ca --- /dev/null +++ b/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_NetAdapterRss/en-US/about_NetAdapterRss.help.txt @@ -0,0 +1,35 @@ +.NAME + NetAdapterRss + +.DESCRIPTION + This resource is used to enable or disable RSS (Receive Side Scaling) on a network adapter. + Not all adapters support RSS so there may be no option for this and it will throw an exception that the network adapter is not found. + + +.PARAMETER Name + Key - String + Specifies the Name of network adapter. + +.PARAMETER Enabled + Required - Boolean + Specifies whether RSS should be enabled or disabled. + +.EXAMPLE 1 + +This configuration disables RSS on the network adapter. + +Configuration NetAdapterRss_EnableRss_Config +{ + Import-DSCResource -ModuleName NetworkingDsc + + Node localhost + { + NetAdapterRss EnableRss + { + Name = 'Ethernet' + Enabled = $True + } + } +} + + diff --git a/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_NetAdapterState/DSC_NetAdapterState.psm1 b/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_NetAdapterState/DSC_NetAdapterState.psm1 new file mode 100644 index 0000000..1f14ea7 --- /dev/null +++ b/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_NetAdapterState/DSC_NetAdapterState.psm1 @@ -0,0 +1,195 @@ +$modulePath = Join-Path -Path (Split-Path -Path (Split-Path -Path $PSScriptRoot -Parent) -Parent) -ChildPath 'Modules' + +# Import the Networking Common Modules +Import-Module -Name (Join-Path -Path $modulePath ` + -ChildPath (Join-Path -Path 'NetworkingDsc.Common' ` + -ChildPath 'NetworkingDsc.Common.psm1')) + +Import-Module -Name (Join-Path -Path $modulePath -ChildPath 'DscResource.Common') + +# Import Localization Strings +$script:localizedData = Get-LocalizedData -DefaultUICulture 'en-US' + +<# + .SYNOPSIS + Gets the current state of a network adapter. + + .PARAMETER Name + Specifies the name of the network adapter. + + .PARAMETER State + Specifies the desired state for the network adapter. + Not used in Get-TargetResource. +#> +function Get-TargetResource +{ + [CmdletBinding()] + [OutputType([Hashtable])] + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $Name, + + [Parameter(Mandatory = $true)] + [ValidateSet('Enabled', 'Disabled')] + [System.String] + $State + ) + + Write-Verbose -Message ( @( + "$($MyInvocation.MyCommand): " + $script:localizedData.CheckingNetAdapterMessage + ) -join '') + + try + { + $netAdapter = Get-NetAdapter -Name $Name -ErrorAction Stop + } + catch + { + Write-Warning -Message ( @( + "$($MyInvocation.MyCommand): " + $script:localizedData.NetAdapterNotFoundMessage -f $Name + ) -join '') + } + + if ($netAdapter) + { + Write-Verbose -Message ( @( + "$($MyInvocation.MyCommand): " + $($script:localizedData.NetAdapterTestingStateMessage -f $Name) + ) -join '') + + <# + Using NET_IF_ADMIN_STATUS as documented here: + https://docs.microsoft.com/en-us/windows/desktop/api/ifdef/ne-ifdef-net_if_admin_status + #> + + $enabled = [Microsoft.PowerShell.Cmdletization.GeneratedTypes.NetAdapter.NET_IF_ADMIN_STATUS]::Up + $disabled = [Microsoft.PowerShell.Cmdletization.GeneratedTypes.NetAdapter.NET_IF_ADMIN_STATUS]::Down + + $result = @{ + Name = $Name + State = switch ($netAdapter.AdminStatus) + { + $enabled { 'Enabled' } + $disabled { 'Disabled' } + default { 'Unsupported' } + } + } + + return $result + } +} + +<# + .SYNOPSIS + Sets the NetAdapterState resource state. + + .PARAMETER Name + Specifies the name of the network adapter. + + .PARAMETER State + Specifies the desired state for the network adapter. +#> +function Set-TargetResource +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $Name, + + [Parameter(Mandatory = $true)] + [ValidateSet('Enabled', 'Disabled')] + [System.String] + $State + ) + + Write-Verbose -Message ( @( + "$($MyInvocation.MyCommand): " + $script:localizedData.CheckingNetAdapterMessage + ) -join '') + + try + { + $netAdapter = Get-NetAdapter -Name $Name -ErrorAction Stop + } + catch + { + Write-Error -Message ( @( + "$($MyInvocation.MyCommand): " + $script:localizedData.NetAdapterNotFoundMessage -f $Name + ) -join '') + } + + if ($netAdapter) + { + try + { + if ($State -eq 'Disabled') + { + Disable-NetAdapter -Name $Name -Confirm:$false -ErrorAction Stop + } + else + { + Enable-NetAdapter -Name $Name -ErrorAction Stop + } + } + catch + { + Write-Error -Message ( @( + "$($MyInvocation.MyCommand): " + $($script:localizedData.NetAdapterSetFailedMessage -f $Name, $State, $_) + ) -join '') + } + } +} + +<# + .SYNOPSIS + Tests if the NetAdapterState resource state is desired state. + + .PARAMETER Name + Specifies the name of the network adapter. + + .PARAMETER State + Specifies the state of the network adapter. +#> +function Test-TargetResource +{ + [CmdletBinding()] + [OutputType([System.Boolean])] + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $Name, + + [Parameter(Mandatory = $true)] + [ValidateSet('Enabled', 'Disabled')] + [System.String] + $State + ) + + Write-Verbose -Message ( @( + "$($MyInvocation.MyCommand): " + $($script:localizedData.NetAdapterTestingStateMessage -f $Name) + ) -join '') + + $currentState = Get-TargetResource @PSBoundParameters + + if ($currentState) + { + Write-Verbose -Message ( @( + "$($MyInvocation.MyCommand): " + $($script:localizedData.NetAdapterStateMessage -f $Name, $currentState.State) + ) -join '') + + return $currentState.State -eq $State + } + + return $false +} diff --git a/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_NetAdapterState/DSC_NetAdapterState.schema.mof b/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_NetAdapterState/DSC_NetAdapterState.schema.mof new file mode 100644 index 0000000..96aec7a --- /dev/null +++ b/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_NetAdapterState/DSC_NetAdapterState.schema.mof @@ -0,0 +1,6 @@ +[ClassVersion("1.0.0.0"), FriendlyName("NetAdapterState")] +class DSC_NetAdapterState : OMI_BaseResource +{ + [Key, Description("Specifies the name of network adapter.")] String Name; + [Required, Description("Specifies the desired state of the network adapter"), ValueMap{"Enabled","Disabled"}, Values{"Enabled","Disabled"}] String State; +}; diff --git a/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_NetAdapterState/README.MD b/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_NetAdapterState/README.MD new file mode 100644 index 0000000..0c674b9 --- /dev/null +++ b/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_NetAdapterState/README.MD @@ -0,0 +1,3 @@ +# Description + +This resource is used to enable or disable a network adapter. diff --git a/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_NetAdapterState/en-US/DSC_NetAdapterState.strings.psd1 b/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_NetAdapterState/en-US/DSC_NetAdapterState.strings.psd1 new file mode 100644 index 0000000..7c7533f --- /dev/null +++ b/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_NetAdapterState/en-US/DSC_NetAdapterState.strings.psd1 @@ -0,0 +1,9 @@ +# Localized resources for DSC_NetAdapterState + +ConvertFrom-StringData @' + CheckingNetAdapterMessage = Checking if network adapter exists or not. + NetAdapterNotFoundMessage = Network adapter '{0}' not found. + NetAdapterTestingStateMessage = Checking if adapter '{0}' is in desired state. + NetAdapterStateMessage = Network adapter '{0}' returned state '{1}'. + NetAdapterSetFailedMessage = Failed to set network adapter '{0}' to state '{1}'. Error: '{2}'. +'@ diff --git a/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_NetAdapterState/en-US/about_NetAdapterState.help.txt b/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_NetAdapterState/en-US/about_NetAdapterState.help.txt new file mode 100644 index 0000000..9edd15f --- /dev/null +++ b/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_NetAdapterState/en-US/about_NetAdapterState.help.txt @@ -0,0 +1,52 @@ +.NAME + NetAdapterState + +.DESCRIPTION + This resource is used to enable or disable a network adapter. + +.PARAMETER Name + Key - String + Specifies the name of network adapter. + +.PARAMETER State + Required - String + Allowed values: Enabled, Disabled + Specifies the desired state of the network adapter + +.EXAMPLE 1 + +This configuration enables the network adapter named Ethernet. + +Configuration NetAdapterState_Enable_Config +{ + Import-DSCResource -ModuleName NetworkingDsc + + Node localhost + { + NetAdapterState EnableEthernet + { + Name = 'Ethernet' + State = 'Enabled' + } + } +} + +.EXAMPLE 2 + +This configuration disables the network adapter named Ethernet. + +Configuration NetAdapterState_Disable_Config +{ + Import-DSCResource -ModuleName NetworkingDsc + + Node localhost + { + NetAdapterState EnableEthernet + { + Name = 'Ethernet' + State = 'Disabled' + } + } +} + + diff --git a/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_NetBios/DSC_NetBios.psm1 b/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_NetBios/DSC_NetBios.psm1 new file mode 100644 index 0000000..064580b --- /dev/null +++ b/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_NetBios/DSC_NetBios.psm1 @@ -0,0 +1,236 @@ +$modulePath = Join-Path -Path (Split-Path -Path (Split-Path -Path $PSScriptRoot -Parent) -Parent) -ChildPath 'Modules' + +# Import the Networking Common Modules +Import-Module -Name (Join-Path -Path $modulePath ` + -ChildPath (Join-Path -Path 'NetworkingDsc.Common' ` + -ChildPath 'NetworkingDsc.Common.psm1')) + +Import-Module -Name (Join-Path -Path $modulePath -ChildPath 'DscResource.Common') + +# Import Localization Strings +$script:localizedData = Get-LocalizedData -DefaultUICulture 'en-US' + +#region check NetBiosSetting enum loaded, if not load +try +{ + [void][System.Reflection.Assembly]::GetAssembly([NetBiosSetting]) +} +catch +{ + Add-Type -TypeDefinition @' + public enum NetBiosSetting + { + Default, + Enable, + Disable + } +'@ +} +#endregion + +<# + .SYNOPSIS + Returns the current state of the Net Bios on an interface. + + .PARAMETER InterfaceAlias + Specifies the alias of a network interface. Supports the use of '*' and '%'. + + .PARAMETER Setting + Specifies if NetBIOS should be enabled or disabled or obtained from + the DHCP server (Default). If static IP, Enable NetBIOS. + + Parameter value is ignored. +#> +function Get-TargetResource +{ + [CmdletBinding()] + [OutputType([System.Collections.Hashtable])] + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $InterfaceAlias, + + [Parameter(Mandatory = $true)] + [ValidateSet('Default', 'Enable', 'Disable')] + [System.String] + $Setting + ) + + Write-Verbose -Message ($script:localizedData.GettingNetBiosSettingMessage -f $InterfaceAlias) + + $win32NetworkAdapterFilter = Format-Win32NetworkAdapterFilterByNetConnectionId -InterfaceAlias $InterfaceAlias + + $netAdapter = Get-CimInstance ` + -ClassName Win32_NetworkAdapter ` + -Filter $win32NetworkAdapterFilter + + if ($netAdapter) + { + Write-Verbose -Message ($script:localizedData.InterfaceDetectedMessage -f $InterfaceAlias, ($netAdapter.InterfaceIndex -Join ',')) + } + else + { + New-InvalidOperationException ` + -Message ($script:localizedData.InterfaceNotFoundError -f $InterfaceAlias) + } + + <# + If a wildcard was specified for the InterfaceAlias then + more than one adapter may be returned. If more than one + adapter is returned then the NetBiosSetting value should + be returned for the first adapter that does not match + the desired value. This is to ensure that when testing + the resource state it will return a mismatch if any adapters + don't have the correct setting. + #> + foreach ($netAdapterItem in $netAdapter) + { + $netAdapterConfig = $netAdapterItem | Get-CimAssociatedInstance ` + -ResultClassName Win32_NetworkAdapterConfiguration ` + -ErrorAction Stop + + $tcpipNetbiosOptions = $netAdapterConfig.TcpipNetbiosOptions + + if ($tcpipNetbiosOptions) + { + $interfaceSetting = $([NetBiosSetting].GetEnumValues()[$tcpipNetbiosOptions]) + } + else + { + $interfaceSetting = 'Default' + } + + Write-Verbose -Message ($script:localizedData.CurrentNetBiosSettingMessage -f $netAdapterItem.Name, $interfaceSetting) + + if ($interfaceSetting -ne $Setting) + { + $Setting = $interfaceSetting + break + } + } + + return @{ + InterfaceAlias = $InterfaceAlias + Setting = $Setting + } +} + +<# + .SYNOPSIS + Sets the state of the Net Bios on an interface. + + .PARAMETER InterfaceAlias + Specifies the alias of a network interface. Supports the use of '*' and '%'. + + .PARAMETER Setting + Specifies if NetBIOS should be enabled or disabled or obtained from + the DHCP server (Default). If static IP, Enable NetBIOS. +#> +function Set-TargetResource +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $InterfaceAlias, + + [Parameter(Mandatory = $true)] + [ValidateSet('Default', 'Enable', 'Disable')] + [System.String] + $Setting + ) + + Write-Verbose -Message ($script:localizedData.SettingNetBiosSettingMessage -f $InterfaceAlias) + + $win32NetworkAdapterFilter = Format-Win32NetworkAdapterFilterByNetConnectionId -InterfaceAlias $InterfaceAlias + + $netAdapter = Get-CimInstance ` + -ClassName Win32_NetworkAdapter ` + -Filter $win32NetworkAdapterFilter + + if ($netAdapter) + { + Write-Verbose -Message ($script:localizedData.InterfaceDetectedMessage -f $InterfaceAlias, ($netAdapter.InterfaceIndex -Join ',')) + } + else + { + New-InvalidOperationException ` + -Message ($script:localizedData.InterfaceNotFoundError -f $InterfaceAlias) + } + + foreach ($netAdapterItem in $netAdapter) + { + $netAdapterConfig = $netAdapterItem | Get-CimAssociatedInstance ` + -ResultClassName Win32_NetworkAdapterConfiguration ` + -ErrorAction Stop + + if ($Setting -eq [NetBiosSetting]::Default) + { + Write-Verbose -Message ($script:localizedData.ResetToDefaultMessage -f $netAdapterItem.Name) + + # If DHCP is not enabled, SetTcpipNetbios CIM Method won't take 0 so overwrite registry entry instead. + $setItemPropertyParameters = @{ + Path = "HKLM:\SYSTEM\CurrentControlSet\Services\NetBT\Parameters\Interfaces\Tcpip_$($NetAdapterConfig.SettingID)" + Name = 'NetbiosOptions' + Value = 0 + } + $null = Set-ItemProperty @setItemPropertyParameters + } + else + { + Write-Verbose -Message ($script:localizedData.SetNetBiosMessage -f $netAdapterItem.Name, $Setting) + + $result = $netAdapterConfig | + Invoke-CimMethod ` + -MethodName SetTcpipNetbios ` + -ErrorAction Stop ` + -Arguments @{ + TcpipNetbiosOptions = [uint32][NetBiosSetting]::$Setting.value__ + } + + if ($result.ReturnValue -ne 0) + { + New-InvalidOperationException ` + -Message ($script:localizedData.FailedUpdatingNetBiosError -f $netAdapterItem.Name, $result.ReturnValue, $Setting) + } + } + } +} + +<# + .SYNOPSIS + Tests the current state the Net Bios on an interface. + + .PARAMETER InterfaceAlias + Specifies the alias of a network interface. Supports the use of '*' and '%'. + + .PARAMETER Setting + Specifies if NetBIOS should be enabled or disabled or obtained from + the DHCP server (Default). If static IP, Enable NetBIOS. +#> +function Test-TargetResource +{ + [CmdletBinding()] + [OutputType([System.Boolean])] + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $InterfaceAlias, + + [Parameter(Mandatory = $true)] + [ValidateSet('Default', 'Enable', 'Disable')] + [System.String] + $Setting + ) + + Write-Verbose -Message ($script:localizedData.TestingNetBiosSettingMessage -f $InterfaceAlias) + + $currentState = Get-TargetResource @PSBoundParameters + + return Test-DscParameterState -CurrentValues $currentState -DesiredValues $PSBoundParameters +} + +Export-ModuleMember -Function *-TargetResource diff --git a/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_NetBios/DSC_NetBios.schema.mof b/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_NetBios/DSC_NetBios.schema.mof new file mode 100644 index 0000000..c456006 --- /dev/null +++ b/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_NetBios/DSC_NetBios.schema.mof @@ -0,0 +1,7 @@ + +[ClassVersion("1.0.0.0"), FriendlyName("NetBios")] +class DSC_NetBios : OMI_BaseResource +{ + [Key, Description("Specifies the alias of a network interface. Supports the use of '*' and '%'")] String InterfaceAlias; + [Required, Description("Specifies if NetBIOS should be enabled or disabled or obtained from the DHCP server (Default). If static IP, Enable NetBIOS."), ValueMap{"Default","Enable","Disable"}, Values{"Default","Enable","Disable"}] String Setting; +}; diff --git a/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_NetBios/README.MD b/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_NetBios/README.MD new file mode 100644 index 0000000..6dc0164 --- /dev/null +++ b/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_NetBios/README.MD @@ -0,0 +1,3 @@ +# Description + +This resource is used to enable or disable the NetBios on a network interface. diff --git a/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_NetBios/en-US/DSC_NetBios.strings.psd1 b/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_NetBios/en-US/DSC_NetBios.strings.psd1 new file mode 100644 index 0000000..cb43c73 --- /dev/null +++ b/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_NetBios/en-US/DSC_NetBios.strings.psd1 @@ -0,0 +1,13 @@ +# Localized resources for DSC_NetBIOS + +ConvertFrom-StringData @' + GettingNetBiosSettingMessage = Getting NetBIOS configuration for Interface '{0}'. + InterfaceDetectedMessage = Interface '{0}' detected with Index number {1}. + SettingNetBiosSettingMessage = Setting NetBIOS configuration for Interface '{0}'. + ResetToDefaultMessage = NetBIOS configuration for Interface '{0}' will be reset to default. + SetNetBiosMessage = NetBIOS configuration for Interface '{0}' will be set to '{1}'. + TestingNetBiosSettingMessage = Testing NetBIOS configuration for Interface '{0}'. + CurrentNetBiosSettingMessage = Current NetBIOS configuration for Interface '{0}' is '{1}'. + InterfaceNotFoundError = Interface '{0}' was not found. + FailedUpdatingNetBiosError = An error result of '{1}' was returned when attemting to set NetBIOS for Interface '{0}' configuration to '{2}'. +'@ diff --git a/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_NetBios/en-US/about_NetBios.help.txt b/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_NetBios/en-US/about_NetBios.help.txt new file mode 100644 index 0000000..9519a09 --- /dev/null +++ b/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_NetBios/en-US/about_NetBios.help.txt @@ -0,0 +1,52 @@ +.NAME + NetBios + +.DESCRIPTION + This resource is used to enable or disable the NetBios on a network interface. + +.PARAMETER InterfaceAlias + Key - String + Specifies the alias of a network interface. Supports the use of '*' and '%' + +.PARAMETER Setting + Required - String + Allowed values: Default, Enable, Disable + Specifies if NetBIOS should be enabled or disabled or obtained from the DHCP server (Default). If static IP, Enable NetBIOS. + +.EXAMPLE 1 + +Disable NetBios on Adapter. + +Configuration NetBios_DisableNetBios_Config +{ + Import-DscResource -ModuleName NetworkingDsc + + Node localhost + { + NetBios DisableNetBios + { + InterfaceAlias = 'Ethernet' + Setting = 'Disable' + } + } +} + +.EXAMPLE 2 + +Disable NetBios on all adapters. + +Configuration NetBios_DisableNetBios_Config_Wildcard +{ + Import-DscResource -ModuleName NetworkingDsc + + Node localhost + { + NetBios DisableNetBios + { + InterfaceAlias = '*' + Setting = 'Disable' + } + } +} + + diff --git a/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_NetConnectionProfile/DSC_NetConnectionProfile.psm1 b/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_NetConnectionProfile/DSC_NetConnectionProfile.psm1 new file mode 100644 index 0000000..9f9144a --- /dev/null +++ b/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_NetConnectionProfile/DSC_NetConnectionProfile.psm1 @@ -0,0 +1,230 @@ +$modulePath = Join-Path -Path (Split-Path -Path (Split-Path -Path $PSScriptRoot -Parent) -Parent) -ChildPath 'Modules' + +# Import the Networking Common Modules +Import-Module -Name (Join-Path -Path $modulePath ` + -ChildPath (Join-Path -Path 'NetworkingDsc.Common' ` + -ChildPath 'NetworkingDsc.Common.psm1')) + +Import-Module -Name (Join-Path -Path $modulePath -ChildPath 'DscResource.Common') + +# Import Localization Strings +$script:localizedData = Get-LocalizedData -DefaultUICulture 'en-US' + +<# + .SYNOPSIS + Returns the current Networking Connection Profile for the specified interface. + + .PARAMETER InterfaceAlias + Specifies the alias for the Interface that is being changed. +#> +function Get-TargetResource +{ + [CmdletBinding()] + [OutputType([System.Collections.Hashtable])] + param + ( + [Parameter(Position = 0, Mandatory = $true)] + [System.String] + $InterfaceAlias + ) + + Write-Verbose -Message ( @( "$($MyInvocation.MyCommand): " + $($script:localizedData.GettingNetConnectionProfile) -f $InterfaceAlias + ) -join '') + + $result = Get-NetConnectionProfile -InterfaceAlias $InterfaceAlias + + return @{ + InterfaceAlias = $result.InterfaceAlias + NetworkCategory = $result.NetworkCategory + IPv4Connectivity = $result.IPv4Connectivity + IPv6Connectivity = $result.IPv6Connectivity + } +} + +<# + .SYNOPSIS + Sets the Network Connection Profile for a specified interface. + + .PARAMETER InterfaceAlias + Specifies the alias for the Interface that is being changed. + + .PARAMETER IPv4Connectivity + Specifies the IPv4 Connection Value. + + .PARAMETER IPv6Connectivity + Specifies the IPv6 Connection Value. + + .PARAMETER NetworkCategory + Sets the Network Category for the interface +#> +function Set-TargetResource +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $InterfaceAlias, + + [Parameter()] + [ValidateSet('Disconnected', 'NoTraffic', 'Subnet', 'LocalNetwork', 'Internet')] + [System.String] + $IPv4Connectivity, + + [Parameter()] + [ValidateSet('Disconnected', 'NoTraffic', 'Subnet', 'LocalNetwork', 'Internet')] + [System.String] + $IPv6Connectivity, + + [Parameter()] + [ValidateSet('Public', 'Private')] + [System.String] + $NetworkCategory + ) + + Write-Verbose -Message ( @( "$($MyInvocation.MyCommand): " + $($script:localizedData.SetNetConnectionProfile) -f $InterfaceAlias + ) -join '') + + Assert-ResourceProperty @PSBoundParameters + + Set-NetConnectionProfile @PSBoundParameters +} + +<# + .SYNOPSIS + Tests is the Network Connection Profile for the specified interface is in the correct state. + + .PARAMETER InterfaceAlias + Specifies the alias for the Interface that is being changed. + + .PARAMETER IPv4Connectivity + Specifies the IPv4 Connection Value. + + .PARAMETER IPv6Connectivity + Specifies the IPv6 Connection Value. + + .PARAMETER NetworkCategory + Sets the NetworkCategory for the interface +#> +function Test-TargetResource +{ + [CmdletBinding()] + [OutputType([System.Boolean])] + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $InterfaceAlias, + + [Parameter()] + [ValidateSet('Disconnected', 'NoTraffic', 'Subnet', 'LocalNetwork', 'Internet')] + [System.String] + $IPv4Connectivity, + + [Parameter()] + [ValidateSet('Disconnected', 'NoTraffic', 'Subnet', 'LocalNetwork', 'Internet')] + [System.String] + $IPv6Connectivity, + + [Parameter()] + [ValidateSet('Public', 'Private')] + [System.String] + $NetworkCategory + ) + + Assert-ResourceProperty @PSBoundParameters + + $current = Get-TargetResource -InterfaceAlias $InterfaceAlias + + if (-not [System.String]::IsNullOrEmpty($IPv4Connectivity) -and ` + ($IPv4Connectivity -ne $current.IPv4Connectivity)) + { + Write-Verbose -Message ( @( "$($MyInvocation.MyCommand): " + $($script:localizedData.TestIPv4Connectivity) -f $IPv4Connectivity, $current.IPv4Connectivity + ) -join '') + + return $false + } + + if (-not [System.String]::IsNullOrEmpty($IPv6Connectivity) -and ` + ($IPv6Connectivity -ne $current.IPv6Connectivity)) + { + Write-Verbose -Message ( @( "$($MyInvocation.MyCommand): " + $($script:localizedData.TestIPv6Connectivity) -f $IPv6Connectivity, $current.IPv6Connectivity + ) -join '') + + return $false + } + + if (-not [System.String]::IsNullOrEmpty($NetworkCategory) -and ` + ($NetworkCategory -ne $current.NetworkCategory)) + { + Write-Verbose -Message ( @( "$($MyInvocation.MyCommand): " + $($script:localizedData.TestNetworkCategory) -f $NetworkCategory, $current.NetworkCategory + ) -join '') + + return $false + } + + return $true +} + +<# + .SYNOPSIS + Check the parameter combination that was passed was valid. + Ensures interface exists. If any problems are detected an + exception will be thrown. + + .PARAMETER InterfaceAlias + Specifies the alias for the Interface that is being changed. + + .PARAMETER IPv4Connectivity + Specifies the IPv4 Connection Value. + + .PARAMETER IPv6Connectivity + Specifies the IPv6 Connection Value. + + .PARAMETER NetworkCategory + Sets the NetworkCategory for the interface +#> +function Assert-ResourceProperty +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $InterfaceAlias, + + [Parameter()] + [ValidateSet('Disconnected', 'NoTraffic', 'Subnet', 'LocalNetwork', 'Internet')] + [System.String] + $IPv4Connectivity, + + [Parameter()] + [ValidateSet('Disconnected', 'NoTraffic', 'Subnet', 'LocalNetwork', 'Internet')] + [System.String] + $IPv6Connectivity, + + [Parameter()] + [ValidateSet('Public', 'Private')] + [System.String] + $NetworkCategory + ) + + if (-not (Get-NetAdapter | Where-Object -Property Name -EQ $InterfaceAlias )) + { + New-InvalidOperationException ` + -Message ($script:localizedData.InterfaceNotAvailableError -f $InterfaceAlias) + } + + if ([System.String]::IsNullOrEmpty($IPv4Connectivity) -and ` + [System.String]::IsNullOrEmpty($IPv6Connectivity) -and ` + [System.String]::IsNullOrEmpty($NetworkCategory)) + { + New-InvalidOperationException ` + -Message ($script:localizedData.ParameterCombinationError) + } +} # Assert-ResourceProperty diff --git a/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_NetConnectionProfile/DSC_NetConnectionProfile.schema.mof b/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_NetConnectionProfile/DSC_NetConnectionProfile.schema.mof new file mode 100644 index 0000000..a84a438 --- /dev/null +++ b/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_NetConnectionProfile/DSC_NetConnectionProfile.schema.mof @@ -0,0 +1,8 @@ +[ClassVersion("1.0"), FriendlyName("NetConnectionProfile")] +class DSC_NetConnectionProfile : OMI_BaseResource +{ + [Key, Description("Specifies the alias for the Interface that is being changed.")] string InterfaceAlias; + [Write, Description("Sets the Network Category for the interface."), ValueMap{"Disconnected", "NoTraffic", "Subnet", "LocalNetwork", "Internet"}, Values{"Disconnected", "NoTraffic", "Subnet", "LocalNetwork", "Internet"}] string IPv4Connectivity; + [Write, Description("Specifies the IPv4 Connection Value."), ValueMap{"Disconnected", "NoTraffic", "Subnet", "LocalNetwork", "Internet"}, Values{"Disconnected", "NoTraffic", "Subnet", "LocalNetwork", "Internet"}] string IPv6Connectivity; + [Write, Description("Specifies the IPv6 Connection Value."), ValueMap{"Public", "Private"}, Values{"Public", "Private"}] string NetworkCategory; +}; diff --git a/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_NetConnectionProfile/README.MD b/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_NetConnectionProfile/README.MD new file mode 100644 index 0000000..ffa5f5a --- /dev/null +++ b/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_NetConnectionProfile/README.MD @@ -0,0 +1,3 @@ +# Description + +This resource is used to set a node's connection profile. diff --git a/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_NetConnectionProfile/en-US/DSC_NetConnectionProfile.strings.psd1 b/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_NetConnectionProfile/en-US/DSC_NetConnectionProfile.strings.psd1 new file mode 100644 index 0000000..e3fe160 --- /dev/null +++ b/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_NetConnectionProfile/en-US/DSC_NetConnectionProfile.strings.psd1 @@ -0,0 +1,11 @@ +# Localized resources for DSC_NetConnectionProfile + +ConvertFrom-StringData @' + GettingNetConnectionProfile = Getting NetConnectionProfile from interface '{0}'. + TestIPv4Connectivity = IPv4Connectivity '{0}' does not match set IPv4Connectivity '{1}' + TestIPv6Connectivity = IPv6Connectivity '{0}' does not match set IPv6Connectivity '{1}' + TestNetworkCategory = NetworkCategory '{0}' does not match set NetworkCategory '{1}' + SetNetConnectionProfile = Setting NetConnectionProfile on interface '{0}' + InterfaceNotAvailableError = Interface "{0}" is not available. Please select a valid interface and try again. + ParameterCombinationError = At least one of the parameters IPv4Connectivity, IPv6Connectivity or NetworkCategory must be set. +'@ diff --git a/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_NetConnectionProfile/en-US/about_NetConnectionProfile.help.txt b/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_NetConnectionProfile/en-US/about_NetConnectionProfile.help.txt new file mode 100644 index 0000000..050d68b --- /dev/null +++ b/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_NetConnectionProfile/en-US/about_NetConnectionProfile.help.txt @@ -0,0 +1,65 @@ +.NAME + NetConnectionProfile + +.DESCRIPTION + This resource is used to set a node's connection profile. + +.PARAMETER InterfaceAlias + Key - String + Specifies the alias for the Interface that is being changed. + +.PARAMETER IPv4Connectivity + Write - String + Allowed values: Disconnected, NoTraffic, Subnet, LocalNetwork, Internet + Sets the Network Category for the interface. + +.PARAMETER IPv6Connectivity + Write - String + Allowed values: Disconnected, NoTraffic, Subnet, LocalNetwork, Internet + Specifies the IPv4 Connection Value. + +.PARAMETER NetworkCategory + Write - String + Allowed values: Public, Private + Specifies the IPv6 Connection Value. + +.EXAMPLE 1 + +Sets the Ethernet adapter to Public and IPv4/6 to Internet Connectivity. + +Configuration NetConnectionProfile_SetPublicEnableInternet_Config +{ + Import-DscResource -Module NetworkingDsc + + Node localhost + { + NetConnectionProfile SetPublicEnableInternet + { + InterfaceAlias = 'Ethernet' + NetworkCategory = 'Public' + IPv4Connectivity = 'Internet' + IPv6Connectivity = 'Internet' + } + } +} + +.EXAMPLE 2 + +Sets the Ethernet adapter to Private but does not change +IPv4 or IPv6 connectivity. + +Configuration NetConnectionProfile_SetPrivate_Config +{ + Import-DscResource -Module NetworkingDsc + + Node localhost + { + NetConnectionProfile SetPrivate + { + InterfaceAlias = 'Ethernet' + NetworkCategory = 'Private' + } + } +} + + diff --git a/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_NetIPInterface/DSC_NetIPInterface.data.psd1 b/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_NetIPInterface/DSC_NetIPInterface.data.psd1 new file mode 100644 index 0000000..b533a0d --- /dev/null +++ b/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_NetIPInterface/DSC_NetIPInterface.data.psd1 @@ -0,0 +1,72 @@ +@{ + ParameterList = @( + @{ + ParameterName = 'AdvertiseDefaultRoute' + PropertyName = 'AdvertiseDefaultRoute' + }, + @{ + ParameterName = 'Advertising' + PropertyName = 'Advertising' + }, + @{ + ParameterName = 'AutomaticMetric' + PropertyName = 'AutomaticMetric' + }, + @{ + ParameterName = 'DirectedMacWolPattern' + PropertyName = 'DirectedMacWolPattern' + }, + @{ + ParameterName = 'Dhcp' + PropertyName = 'Dhcp' + }, + @{ + ParameterName = 'EcnMarking' + PropertyName = 'EcnMarking' + }, + @{ + ParameterName = 'ForceArpNdWolPattern' + PropertyName = 'ForceArpNdWolPattern' + }, + @{ + ParameterName = 'Forwarding' + PropertyName = 'Forwarding' + }, + @{ + ParameterName = 'IgnoreDefaultRoutes' + PropertyName = 'IgnoreDefaultRoutes' + }, + @{ + ParameterName = 'ManagedAddressConfiguration' + PropertyName = 'ManagedAddressConfiguration' + }, + @{ + ParameterName = 'NeighborUnreachabilityDetection' + PropertyName = 'NeighborUnreachabilityDetection' + }, + @{ + ParameterName = 'OtherStatefulConfiguration' + PropertyName = 'OtherStatefulConfiguration' + }, + @{ + ParameterName = 'RouterDiscovery' + PropertyName = 'RouterDiscovery' + }, + @{ + ParameterName = 'WeakHostReceive' + PropertyName = 'WeakHostReceive' + }, + @{ + ParameterName = 'WeakHostSend' + PropertyName = 'WeakHostSend' + }, + @{ + ParameterName = 'NlMtuBytes' + PropertyName = 'NlMtu' + }, + @{ + ParameterName = 'InterfaceMetric' + PropertyName = 'InterfaceMetric' + } + ) +} diff --git a/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_NetIPInterface/DSC_NetIPInterface.psm1 b/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_NetIPInterface/DSC_NetIPInterface.psm1 new file mode 100644 index 0000000..5cb3caa --- /dev/null +++ b/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_NetIPInterface/DSC_NetIPInterface.psm1 @@ -0,0 +1,519 @@ +$modulePath = Join-Path -Path (Split-Path -Path (Split-Path -Path $PSScriptRoot -Parent) -Parent) -ChildPath 'Modules' + +# Import the Networking Common Modules +Import-Module -Name (Join-Path -Path $modulePath ` + -ChildPath (Join-Path -Path 'NetworkingDsc.Common' ` + -ChildPath 'NetworkingDsc.Common.psm1')) + +Import-Module -Name (Join-Path -Path $modulePath -ChildPath 'DscResource.Common') + +# Import Localization Strings +$script:localizedData = Get-LocalizedData -DefaultUICulture 'en-US' + +<# + This is an array of all the parameters used by this resource. + The PropertyName is the name of the property that is returned when + getting the object. The ParameterName is the parameter name when + setting the value. These are usually the same, but do differ in + some cases. +#> +$script:resourceData = Import-LocalizedData ` + -BaseDirectory $PSScriptRoot ` + -FileName 'DSC_NetIPInterface.data.psd1' +$script:parameterList = $script:resourceData.ParameterList + +<# + .SYNOPSIS + Returns the current state of the Network Interface. + + .PARAMETER InterfaceAlias + Alias of the network interface to configure. + + .PARAMETER AddressFamily + IP address family on the interface to configure. +#> +function Get-TargetResource +{ + [CmdletBinding()] + [OutputType([System.Collections.Hashtable])] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $InterfaceAlias, + + [Parameter(Mandatory = $true)] + [ValidateSet('IPv4', 'IPv6')] + [System.String] + $AddressFamily + ) + + Write-Verbose -Message ( @( "$($MyInvocation.MyCommand): " + $($script:localizedData.GettingNetIPInterfaceMessage) -f $InterfaceAlias, $AddressFamily + ) -join '') + + $getNetworkIPInterfaceParameters = @{ + InterfaceAlias = $InterfaceAlias + AddressFamily = $AddressFamily + } + + return Get-NetworkIPInterface @getNetworkIPInterfaceParameters +} + +<# + .SYNOPSIS + Sets the current state of the Network Interface. + + .PARAMETER InterfaceAlias + Alias of the network interface to configure. + + .PARAMETER AddressFamily + IP address family on the interface to configure. + + .PARAMETER AdvertiseDefaultRoute + Specifies the default router advertisement for an interface. + + .PARAMETER Advertising + Specifies the router advertisement value for the IP interface. + + .PARAMETER AutomaticMetric + Specifies the value for automatic metric calculation. + + .PARAMETER Dhcp + Specifies the Dynamic Host Configuration Protocol (DHCP) value for an IP interface. + + .PARAMETER DirectedMacWolPattern + Specifies the wake-up packet value for an IP interface. + + .PARAMETER EcnMarking + Specifies the value for Explicit Congestion Notification (ECN) marking. + + .PARAMETER ForceArpNdWolPattern + Specifies the Wake On LAN (WOL) value for the IP interface. + + .PARAMETER Forwarding + Specifies the packet forwarding value for the IP interface. + + .PARAMETER IgnoreDefaultRoutes + Specifies a value for Default Route advertisements. + + .PARAMETER ManagedAddressConfiguration + Specifies the value for managed address configuration. + + .PARAMETER NeighborUnreachabilityDetection + Specifies the value for Neighbor Unreachability Detection (NUD). + + .PARAMETER OtherStatefulConfiguration + Specifies the value for configuration other than addresses. + + .PARAMETER RouterDiscovery + Specifies the value for router discovery for an IP interface. + + .PARAMETER WeakHostReceive + Specifies the receive value for a weak host model. + + .PARAMETER WeakHostSend + Specifies the send value for a weak host model. + + .PARAMETER NlMtu + Specifies the network layer Maximum Transmission Unit (MTU) value, in bytes, for an IP interface. + + .PARAMETER InterfaceMetric + Specifies the metric for an IP interface. +#> +function Set-TargetResource +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $InterfaceAlias, + + [Parameter(Mandatory = $true)] + [ValidateSet('IPv4', 'IPv6')] + [System.String] + $AddressFamily, + + [Parameter()] + [ValidateSet('Enabled', 'Disabled')] + [System.String] + $AdvertiseDefaultRoute, + + [Parameter()] + [ValidateSet('Enabled', 'Disabled')] + [System.String] + $Advertising, + + [Parameter()] + [ValidateSet('Enabled', 'Disabled')] + [System.String] + $AutomaticMetric, + + [Parameter()] + [ValidateSet('Enabled', 'Disabled')] + [System.String] + $Dhcp, + + [Parameter()] + [ValidateSet('Enabled', 'Disabled')] + [System.String] + $DirectedMacWolPattern, + + [Parameter()] + [ValidateSet('Disabled', 'UseEct1', 'UseEct0', 'AppDecide')] + [System.String] + $EcnMarking, + + [Parameter()] + [ValidateSet('Enabled', 'Disabled')] + [System.String] + $ForceArpNdWolPattern, + + [Parameter()] + [ValidateSet('Enabled', 'Disabled')] + [System.String] + $Forwarding, + + [Parameter()] + [ValidateSet('Enabled', 'Disabled')] + [System.String] + $IgnoreDefaultRoutes, + + [Parameter()] + [ValidateSet('Enabled', 'Disabled')] + [System.String] + $ManagedAddressConfiguration, + + [Parameter()] + [ValidateSet('Enabled', 'Disabled')] + [System.String] + $NeighborUnreachabilityDetection, + + [Parameter()] + [ValidateSet('Enabled', 'Disabled')] + [System.String] + $OtherStatefulConfiguration, + + [Parameter()] + [ValidateSet('Enabled', 'Disabled', 'ControlledByDHCP')] + [System.String] + $RouterDiscovery, + + [Parameter()] + [ValidateSet('Enabled', 'Disabled')] + [System.String] + $WeakHostReceive, + + [Parameter()] + [ValidateSet('Enabled', 'Disabled')] + [System.String] + $WeakHostSend, + + [Parameter()] + [System.UInt32] + $NlMtu, + + [Parameter()] + [System.UInt32] + $InterfaceMetric + ) + + Write-Verbose -Message ( @( "$($MyInvocation.MyCommand): " + $($script:localizedData.ApplyingNetIPInterfaceMessage) ` + -f $InterfaceAlias, $AddressFamily + ) -join '') + + $getTargetResourceParameters = @{ + InterfaceAlias = $InterfaceAlias + AddressFamily = $AddressFamily + } + + $currentState = Get-TargetResource @getTargetResourceParameters + + <# + Loop through each possible property and if it is passed to the resource + and the current value of the property is different then add it to the + netIPInterfaceParameters array that will be used to update the + net IP interface settings. + #> + $parameterUpdated = $false + $setNetIPInterfaceParameters = @{ + InterfaceAlias = $InterfaceAlias + AddressFamily = $AddressFamily + } + + foreach ($parameter in $script:parameterList) + { + if ($PSBoundParameters.ContainsKey($parameter.PropertyName)) + { + $currentPropertyValue = $currentState.$($parameter.PropertyName) + $newParameterValue = (Get-Variable -Name ($parameter.PropertyName)).Value + + if ($newParameterValue -and ($currentPropertyValue -ne $newParameterValue)) + { + $null = $setNetIPInterfaceParameters.Add($parameter.ParameterName, $newParameterValue) + + Write-Verbose -Message ( @( "$($MyInvocation.MyCommand): " + $($script:localizedData.SettingNetIPInterfaceParameterValueMessage) ` + -f $InterfaceAlias, $AddressFamily, $parameter.ParameterName, $newParameterValue + ) -join '') + + $parameterUpdated = $true + } + } + } + + if ($parameterUpdated) + { + $null = Set-NetIPInterface @setNetIPInterfaceParameters + } +} + +<# + .SYNOPSIS + Sets the current state of the Network Interface. + + .PARAMETER InterfaceAlias + Alias of the network interface to configure. + + .PARAMETER AddressFamily + IP address family on the interface to configure. + + .PARAMETER AdvertiseDefaultRoute + Specifies the default router advertisement for an interface. + + .PARAMETER Advertising + Specifies the router advertisement value for the IP interface. + + .PARAMETER AutomaticMetric + Specifies the value for automatic metric calculation. + + .PARAMETER Dhcp + Specifies the Dynamic Host Configuration Protocol (DHCP) value for an IP interface. + + .PARAMETER DirectedMacWolPattern + Specifies the wake-up packet value for an IP interface. + + .PARAMETER EcnMarking + Specifies the value for Explicit Congestion Notification (ECN) marking. + + .PARAMETER ForceArpNdWolPattern + Specifies the Wake On LAN (WOL) value for the IP interface. + + .PARAMETER Forwarding + Specifies the packet forwarding value for the IP interface. + + .PARAMETER IgnoreDefaultRoutes + Specifies a value for Default Route advertisements. + + .PARAMETER ManagedAddressConfiguration + Specifies the value for managed address configuration. + + .PARAMETER NeighborUnreachabilityDetection + Specifies the value for Neighbor Unreachability Detection (NUD). + + .PARAMETER OtherStatefulConfiguration + Specifies the value for configuration other than addresses. + + .PARAMETER RouterDiscovery + Specifies the value for router discovery for an IP interface. + + .PARAMETER WeakHostReceive + Specifies the receive value for a weak host model. + + .PARAMETER WeakHostSend + Specifies the send value for a weak host model. + + .PARAMETER NlMtu + Specifies the network layer Maximum Transmission Unit (MTU) value, in bytes, for an IP interface. + + .PARAMETER InterfaceMetric + Specifies the metric for an IP interface. +#> +function Test-TargetResource +{ + [CmdletBinding()] + [OutputType([System.Boolean])] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $InterfaceAlias, + + [Parameter(Mandatory = $true)] + [ValidateSet('IPv4', 'IPv6')] + [System.String] + $AddressFamily, + + [Parameter()] + [ValidateSet('Enabled', 'Disabled')] + [System.String] + $AdvertiseDefaultRoute, + + [Parameter()] + [ValidateSet('Enabled', 'Disabled')] + [System.String] + $Advertising, + + [Parameter()] + [ValidateSet('Enabled', 'Disabled')] + [System.String] + $AutomaticMetric, + + [Parameter()] + [ValidateSet('Enabled', 'Disabled')] + [System.String] + $Dhcp, + + [Parameter()] + [ValidateSet('Enabled', 'Disabled')] + [System.String] + $DirectedMacWolPattern, + + [Parameter()] + [ValidateSet('Disabled', 'UseEct1', 'UseEct0', 'AppDecide')] + [System.String] + $EcnMarking, + + [Parameter()] + [ValidateSet('Enabled', 'Disabled')] + [System.String] + $ForceArpNdWolPattern, + + [Parameter()] + [ValidateSet('Enabled', 'Disabled')] + [System.String] + $Forwarding, + + [Parameter()] + [ValidateSet('Enabled', 'Disabled')] + [System.String] + $IgnoreDefaultRoutes, + + [Parameter()] + [ValidateSet('Enabled', 'Disabled')] + [System.String] + $ManagedAddressConfiguration, + + [Parameter()] + [ValidateSet('Enabled', 'Disabled')] + [System.String] + $NeighborUnreachabilityDetection, + + [Parameter()] + [ValidateSet('Enabled', 'Disabled')] + [System.String] + $OtherStatefulConfiguration, + + [Parameter()] + [ValidateSet('Enabled', 'Disabled', 'ControlledByDHCP')] + [System.String] + $RouterDiscovery, + + [Parameter()] + [ValidateSet('Enabled', 'Disabled')] + [System.String] + $WeakHostReceive, + + [Parameter()] + [ValidateSet('Enabled', 'Disabled')] + [System.String] + $WeakHostSend, + + [Parameter()] + [System.UInt32] + $NlMtu, + + [Parameter()] + [System.UInt32] + $InterfaceMetric + ) + + Write-Verbose -Message ( @( "$($MyInvocation.MyCommand): " + $($script:localizedData.CheckingNetIPInterfaceMessage) -f $InterfaceAlias, $AddressFamily + ) -join '') + + $getTargetResourceParameters = @{ + InterfaceAlias = $InterfaceAlias + AddressFamily = $AddressFamily + } + + $currentState = Get-TargetResource @getTargetResourceParameters + + return Test-DscParameterState -CurrentValues $currentState -DesiredValues $PSBoundParameters -TurnOffTypeChecking +} + +<# + .SYNOPSIS + Get the network IP interface for the address family. + If the network interface is not found or the address family + is not bound to the inerface then an exception will be thrown. + + It will return an hash table with only the parameters in found + in the $script:parameterList array and the InterfaceAlias and + AddressFamily parameters. + + .PARAMETER InterfaceAlias + Alias of the network interface to configure. + + .PARAMETER AddressFamily + IP address family on the interface to configure. +#> +function Get-NetworkIPInterface +{ + [CmdletBinding()] + [OutputType([System.Collections.Hashtable])] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $InterfaceAlias, + + [Parameter(Mandatory = $true)] + [ValidateSet('IPv4', 'IPv6')] + [System.String] + $AddressFamily + ) + + Write-Verbose -Message ( @( "$($MyInvocation.MyCommand): " + $($script:localizedData.GettingNetIPInterfaceMessage) -f $InterfaceAlias, $AddressFamily + ) -join '') + + $netIPInterface = Get-NetIPInterface @PSBoundParameters -ErrorAction SilentlyContinue + + if (-not $netIPInterface) + { + # The Net IP Interface does not exist or address family is not bound + New-InvalidOperationException ` + -Message ($script:localizedData.NetworkIPInterfaceDoesNotExistMessage -f $InterfaceAlias, $AddressFamily) + } + + <# + Populate the properties for get target resource by looping through + the parameter array list and adding the values to the result array + #> + $networkIPInterface = @{ + InterfaceAlias = $InterfaceAlias + AddressFamily = $AddressFamily + } + + foreach ($parameter in $script:parameterList) + { + $propertyValue = $netIPInterface.$($parameter.PropertyName) + $null = $networkIPInterface.Add($parameter.PropertyName, $propertyValue) + + Write-Verbose -Message ( @( "$($MyInvocation.MyCommand): " + $($script:localizedData.NetworkIPInterfaceParameterValueMessage) -f ` + $InterfaceAlias, $AddressFamily, $parameter.PropertyName, $propertyValue + ) -join '') + } + + return $networkIPInterface +} + +Export-ModuleMember -Function *-TargetResource diff --git a/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_NetIPInterface/DSC_NetIPInterface.schema.mof b/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_NetIPInterface/DSC_NetIPInterface.schema.mof new file mode 100644 index 0000000..35dd679 --- /dev/null +++ b/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_NetIPInterface/DSC_NetIPInterface.schema.mof @@ -0,0 +1,23 @@ +[ClassVersion("1.0.0"), FriendlyName("NetIPInterface")] +class DSC_NetIPInterface : OMI_BaseResource +{ + [Key, Description("Alias of the network interface to configure.")] String InterfaceAlias; + [Key, Description("IP address family on the interface to configure."), ValueMap{"IPv4", "IPv6"}, Values{"IPv4", "IPv6"}] String AddressFamily; + [Write, Description("Specifies the default router advertisement for an interface."), ValueMap{"Enabled", "Disabled"},Values{"Enabled", "Disabled"}] String AdvertiseDefaultRoute; + [Write, Description("Specifies the router advertisement value for the IP interface."), ValueMap{"Enabled", "Disabled"},Values{"Enabled", "Disabled"}] String Advertising; + [Write, Description("Specifies the value for automatic metric calculation."), ValueMap{"Enabled", "Disabled"},Values{"Enabled", "Disabled"}] String AutomaticMetric; + [Write, Description("Specifies the Dynamic Host Configuration Protocol (DHCP) value for an IP interface."), ValueMap{"Enabled", "Disabled"},Values{"Enabled", "Disabled"}] String Dhcp; + [Write, Description("Specifies the wake-up packet value for an IP interface."), ValueMap{"Enabled", "Disabled"},Values{"Enabled", "Disabled"}] String DirectedMacWolPattern; + [Write, Description("Specifies the value for Explicit Congestion Notification (ECN) marking."), ValueMap{"Disabled", "UseEct1", "UseEct0", "AppDecide"},Values{"Disabled", "UseEct1", "UseEct0", "AppDecide"}] String EcnMarking; + [Write, Description("Specifies the Wake On LAN (WOL) value for the IP interface."), ValueMap{"Enabled", "Disabled"},Values{"Enabled", "Disabled"}] String ForceArpNdWolPattern; + [Write, Description("Specifies the packet forwarding value for the IP interface."), ValueMap{"Enabled", "Disabled"},Values{"Enabled", "Disabled"}] String Forwarding; + [Write, Description("Specifies a value for Default Route advertisements."), ValueMap{"Enabled", "Disabled"},Values{"Enabled", "Disabled"}] String IgnoreDefaultRoutes; + [Write, Description("Specifies the value for managed address configuration."), ValueMap{"Enabled", "Disabled"},Values{"Enabled", "Disabled"}] String ManagedAddressConfiguration; + [Write, Description("Specifies the value for Neighbor Unreachability Detection (NUD)."), ValueMap{"Enabled", "Disabled"},Values{"Enabled", "Disabled"}] String NeighborUnreachabilityDetection; + [Write, Description("Specifies the value for configuration other than addresses."), ValueMap{"Enabled", "Disabled"},Values{"Enabled", "Disabled"}] String OtherStatefulConfiguration; + [Write, Description("Specifies the value for router discovery for an IP interface."), ValueMap{"Enabled", "Disabled", "ControlledByDHCP"},Values{"Enabled", "Disabled", "ControlledByDHCP"}] String RouterDiscovery; + [Write, Description("Specifies the receive value for a weak host model."), ValueMap{"Enabled", "Disabled"},Values{"Enabled", "Disabled"}] String WeakHostReceive; + [Write, Description("Specifies the send value for a weak host model."), ValueMap{"Enabled", "Disabled"},Values{"Enabled", "Disabled"}] String WeakHostSend; + [Write, Description("Specifies the network layer Maximum Transmission Unit (MTU) value, in bytes, for an IP interface.")] UInt32 NlMtu; + [Write, Description("Specifies the metric for an IP interface.")] UInt32 InterfaceMetric; +}; diff --git a/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_NetIPInterface/README.MD b/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_NetIPInterface/README.MD new file mode 100644 index 0000000..5d936d1 --- /dev/null +++ b/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_NetIPInterface/README.MD @@ -0,0 +1,9 @@ +# Description + +This resource is used to configure the IP interface settings for a network interface. + +## Known Issues + +- If you define a value for `InterfaceMetric`, the `AutomaticMetric` +setting is ignored. PowerShell ignores `AutomaticMetric` when you +use both arguments with the `Set-NetIPInterface` cmdlet. diff --git a/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_NetIPInterface/en-US/DSC_NetIPInterface.strings.psd1 b/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_NetIPInterface/en-US/DSC_NetIPInterface.strings.psd1 new file mode 100644 index 0000000..6cd55b8 --- /dev/null +++ b/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_NetIPInterface/en-US/DSC_NetIPInterface.strings.psd1 @@ -0,0 +1,10 @@ +# Localized resources for DSC_NetIPInterface + +ConvertFrom-StringData @' + GettingNetIPInterfaceMessage = Getting settings for net IP interface for alias '{0}' and address family '{1}'. + NetworkIPInterfaceDoesNotExistMessage = Net IP interface for alias '{0}' and address family '{1}' does not exist. + NetworkIPInterfaceParameterValueMessage = Net IP interface for alias '{0}' and address family '{1}' parameter '{2}' is '{3}'. + ApplyingNetIPInterfaceMessage = Applying settings to net IP interface for alias '{0}' and address family '{1}'. + SettingNetIPInterfaceParameterValueMessage = Setting Net IP interface for alias '{0}' and address family '{1}' parameter '{2}' to '{3}'. + CheckingNetIPInterfaceMessage = Checking settings on net IP interface for alias '{0}' and address family '{1}'. +'@ diff --git a/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_NetIPInterface/en-US/about_NetIPInterface.help.txt b/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_NetIPInterface/en-US/about_NetIPInterface.help.txt new file mode 100644 index 0000000..3233dc8 --- /dev/null +++ b/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_NetIPInterface/en-US/about_NetIPInterface.help.txt @@ -0,0 +1,241 @@ +.NAME + NetIPInterface + +.DESCRIPTION + This resource is used to configure the IP interface settings for a network interface. + + ## Known Issues + + - If you define a value for `InterfaceMetric`, the `AutomaticMetric` + setting is ignored. PowerShell ignores `AutomaticMetric` when you + use both arguments with the `Set-NetIPInterface` cmdlet. + +.PARAMETER InterfaceAlias + Key - String + Alias of the network interface to configure. + +.PARAMETER AddressFamily + Key - String + Allowed values: IPv4, IPv6 + IP address family on the interface to configure. + +.PARAMETER AdvertiseDefaultRoute + Write - String + Allowed values: Enabled, Disabled + Specifies the default router advertisement for an interface. + +.PARAMETER Advertising + Write - String + Allowed values: Enabled, Disabled + Specifies the router advertisement value for the IP interface. + +.PARAMETER AutomaticMetric + Write - String + Allowed values: Enabled, Disabled + Specifies the value for automatic metric calculation. + +.PARAMETER Dhcp + Write - String + Allowed values: Enabled, Disabled + Specifies the Dynamic Host Configuration Protocol (DHCP) value for an IP interface. + +.PARAMETER DirectedMacWolPattern + Write - String + Allowed values: Enabled, Disabled + Specifies the wake-up packet value for an IP interface. + +.PARAMETER EcnMarking + Write - String + Allowed values: Disabled, UseEct1, UseEct0, AppDecide + Specifies the value for Explicit Congestion Notification (ECN) marking. + +.PARAMETER ForceArpNdWolPattern + Write - String + Allowed values: Enabled, Disabled + Specifies the Wake On LAN (WOL) value for the IP interface. + +.PARAMETER Forwarding + Write - String + Allowed values: Enabled, Disabled + Specifies the packet forwarding value for the IP interface. + +.PARAMETER IgnoreDefaultRoutes + Write - String + Allowed values: Enabled, Disabled + Specifies a value for Default Route advertisements. + +.PARAMETER ManagedAddressConfiguration + Write - String + Allowed values: Enabled, Disabled + Specifies the value for managed address configuration. + +.PARAMETER NeighborUnreachabilityDetection + Write - String + Allowed values: Enabled, Disabled + Specifies the value for Neighbor Unreachability Detection (NUD). + +.PARAMETER OtherStatefulConfiguration + Write - String + Allowed values: Enabled, Disabled + Specifies the value for configuration other than addresses. + +.PARAMETER RouterDiscovery + Write - String + Allowed values: Enabled, Disabled, ControlledByDHCP + Specifies the value for router discovery for an IP interface. + +.PARAMETER WeakHostReceive + Write - String + Allowed values: Enabled, Disabled + Specifies the receive value for a weak host model. + +.PARAMETER WeakHostSend + Write - String + Allowed values: Enabled, Disabled + Specifies the send value for a weak host model. + +.PARAMETER NlMtu + Write - UInt32 + Specifies the network layer Maximum Transmission Unit (MTU) value, in bytes, for an IP interface. + +.PARAMETER InterfaceMetric + Write - UInt32 + Specifies the metric for an IP interface. + +.EXAMPLE 1 + +This example enables the following settings on the IPv4 network interface with alias +'Ethernet': +- AdvertiseDefaultRoute +- Avertising +- AutomaticMetric +- DirectedMacWolPattern +- ForceArpNdWolPattern +- Forwarding +- IgnoreDefaultRoute +- ManagedAddressConfiguration +- NeighborUnreachabilityDetection +- OtherStatefulConfiguration +- RouterDiscovery +- NlMtu +The EcnMarking parameter will be set to AppDecide. + +Configuration NetIPInterface_MultipleSettings_Config +{ + Import-DscResource -Module NetworkingDsc + + Node localhost + { + NetIPInterface MultipleSettings + { + InterfaceAlias = 'Ethernet' + AddressFamily = 'IPv4' + AdvertiseDefaultRoute = 'Enabled' + Advertising = 'Enabled' + AutomaticMetric = 'Enabled' + DirectedMacWolPattern = 'Enabled' + EcnMarking = 'AppDecide' + ForceArpNdWolPattern = 'Enabled' + Forwarding = 'Enabled' + IgnoreDefaultRoutes = 'Enabled' + ManagedAddressConfiguration = 'Enabled' + NeighborUnreachabilityDetection = 'Enabled' + OtherStatefulConfiguration = 'Enabled' + RouterDiscovery = 'Enabled' + NlMtu = 1576 + } + } +} + +.EXAMPLE 2 + +Enabling DHCP for the IPv4 Address and DNS on the adapter with alias 'Ethernet'. + +Configuration NetIPInterface_EnableDHCP_Config +{ + Import-DscResource -Module NetworkingDsc + + Node localhost + { + NetIPInterface EnableDhcp + { + InterfaceAlias = 'Ethernet' + AddressFamily = 'IPv4' + Dhcp = 'Enabled' + } + + DnsServerAddress EnableDhcpDNS + { + InterfaceAlias = 'Ethernet' + AddressFamily = 'IPv4' + } + } +} + +.EXAMPLE 3 + +Disable the weak host receive IPv4 setting for the network adapter with alias 'Ethernet'. + +Configuration NetIPInterface_DisableWeakHostReceive_Config +{ + Import-DscResource -Module NetworkingDsc + + Node localhost + { + NetIPInterface DisableWeakHostReceiving + { + InterfaceAlias = 'Ethernet' + AddressFamily = 'IPv4' + WeakHostReceive = 'Disabled' + } + } +} + +.EXAMPLE 4 + +Disable the weak host send IPv4 setting for the network adapter with alias 'Ethernet'. + +Configuration NetIPInterface_DisableWeakHostSend_Config +{ + Import-DscResource -Module NetworkingDsc + + Node localhost + { + NetIPInterface DisableWeakHostSend + { + InterfaceAlias = 'Ethernet' + AddressFamily = 'IPv4' + WeakHostSend = 'Disabled' + } + } +} + +.EXAMPLE 5 + +Set a specified interface metrics for the network adapters with alias 'Ethernet' and 'Ethernet 2'. + +Configuration NetIPInterface_SetInterfaceMetric +{ + Import-DscResource -Module NetworkingDsc + + Node localhost + { + NetIPInterface EthernetMetric + { + InterfaceAlias = 'Ethernet' + AddressFamily = 'IPv4' + AutomaticMetric = 'Disabled' + InterfaceMetric = 10 + } + + NetIPInterface Ethernet2Metric + { + InterfaceAlias = 'Ethernet 2' + AddressFamily = 'IPv4' + AutomaticMetric = 'Disabled' + InterfaceMetric = 20 + } + } +} + + diff --git a/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_NetworkTeam/DSC_NetworkTeam.psm1 b/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_NetworkTeam/DSC_NetworkTeam.psm1 new file mode 100644 index 0000000..c533558 --- /dev/null +++ b/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_NetworkTeam/DSC_NetworkTeam.psm1 @@ -0,0 +1,327 @@ +$modulePath = Join-Path -Path (Split-Path -Path (Split-Path -Path $PSScriptRoot -Parent) -Parent) -ChildPath 'Modules' + +# Import the Networking Common Modules +Import-Module -Name (Join-Path -Path $modulePath ` + -ChildPath (Join-Path -Path 'NetworkingDsc.Common' ` + -ChildPath 'NetworkingDsc.Common.psm1')) + +Import-Module -Name (Join-Path -Path $modulePath -ChildPath 'DscResource.Common') + +# Import Localization Strings +$script:localizedData = Get-LocalizedData -DefaultUICulture 'en-US' + +<# + .SYNOPSIS + Returns the current state of a Network Team. + + .PARAMETER Name + Specifies the name of the network team to create. + + .PARAMETER TeamMembers + Specifies the network interfaces that should be a part of the network team. + This is a comma-separated list. +#> +function Get-TargetResource +{ + [CmdletBinding()] + [OutputType([System.Collections.Hashtable])] + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $Name, + + [Parameter(Mandatory = $true)] + [System.String[]] + $TeamMembers + ) + + $configuration = @{ + Name = $Name + TeamMembers = $TeamMembers + Ensure = 'Absent' + } + + Write-Verbose -Message ($script:localizedData.GetTeamInfo -f $Name) + $networkTeam = Get-NetLBFOTeam -Name $Name -ErrorAction SilentlyContinue + + if ($networkTeam) + { + Write-Verbose -Message ($script:localizedData.FoundTeam -f $Name) + $configuration.Add('LoadBalancingAlgorithm', $networkTeam.LoadBalancingAlgorithm) + $configuration.Add('TeamingMode', $networkTeam.TeamingMode) + $configuration.Ensure = 'Present' + + if ($null -eq (Compare-Object -ReferenceObject $TeamMembers -DifferenceObject $networkTeam.Members)) + { + Write-Verbose -Message ($script:localizedData.TeamMembersMatch -f $Name) + } + else + { + $configuration.TeamMembers = $networkTeam.Members + Write-Verbose -Message ($script:localizedData.TeamMembersNotMatch -f $Name) + } + } + else + { + Write-Verbose -Message ($script:localizedData.TeamNotFound -f $Name) + } + + return $configuration +} + +<# + .SYNOPSIS + Adds, updates or removes a Network Team. + + .PARAMETER Name + Specifies the name of the network team to create. + + .PARAMETER TeamMembers + Specifies the network interfaces that should be a part of the network team. + This is a comma-separated list. + + .PARAMETER TeamingMode + Specifies the teaming mode configuration. + + .PARAMETER LoadBalancingAlgorithm + Specifies the load balancing algorithm for the network team. + + .PARAMETER Ensure + Specifies if the network team should be created or deleted. +#> +function Set-TargetResource +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $Name, + + [Parameter(Mandatory = $true)] + [System.String[]] + $TeamMembers, + + [Parameter()] + [ValidateSet('SwitchIndependent', 'LACP', 'Static')] + [System.String] + $TeamingMode = 'SwitchIndependent', + + [Parameter()] + [ValidateSet('Dynamic', 'HyperVPort', 'IPAddresses', 'MacAddresses', 'TransportPorts')] + [System.String] + $LoadBalancingAlgorithm = 'HyperVPort', + + [Parameter()] + [ValidateSet('Present', 'Absent')] + [System.String] + $Ensure = 'Present' + ) + + Write-Verbose -Message ($script:localizedData.GetTeamInfo -f $Name) + + $networkTeam = Get-NetLBFOTeam -Name $Name -ErrorAction SilentlyContinue + + if ($Ensure -eq 'Present') + { + if ($networkTeam) + { + Write-Verbose -Message ($script:localizedData.FoundTeam -f $Name) + + $setArguments = @{ + Name = $Name + } + + if ($networkTeam.loadBalancingAlgorithm -ne $LoadBalancingAlgorithm) + { + Write-Verbose -Message ($script:localizedData.LoadBalancingAlgorithmDifferent -f $LoadBalancingAlgorithm) + + $setArguments.Add('LoadBalancingAlgorithm', $LoadBalancingAlgorithm) + $isNetModifyRequired = $true + } + + if ($networkTeam.TeamingMode -ne $TeamingMode) + { + Write-Verbose -Message ($script:localizedData.TeamingModeDifferent -f $TeamingMode) + + $setArguments.Add('TeamingMode', $TeamingMode) + $isNetModifyRequired = $true + } + + if ($isNetModifyRequired) + { + Write-Verbose -Message ($script:localizedData.ModifyTeam -f $Name) + + Set-NetLbfoTeam @setArguments -ErrorAction Stop -Confirm:$false + } + + $netTeamMembers = Compare-Object ` + -ReferenceObject $TeamMembers ` + -DifferenceObject $networkTeam.Members + + if ($null -ne $netTeamMembers) + { + Write-Verbose -Message ($script:localizedData.MembersDifferent -f $Name) + + $membersToRemove = ($netTeamMembers | Where-Object -FilterScript { + $_.SideIndicator -eq '=>' + }).InputObject + + if ($membersToRemove) + { + Write-Verbose -Message ($script:localizedData.RemovingMembers -f ($membersToRemove -join ',')) + + $null = Remove-NetLbfoTeamMember -Name $membersToRemove ` + -Team $Name ` + -ErrorAction Stop ` + -Confirm:$false + } + + $membersToAdd = ($netTeamMembers | Where-Object -FilterScript { + $_.SideIndicator -eq '<=' + }).InputObject + + if ($membersToAdd) + { + Write-Verbose -Message ($script:localizedData.AddingMembers -f ($membersToAdd -join ',')) + + $null = Add-NetLbfoTeamMember -Name $membersToAdd ` + -Team $Name ` + -ErrorAction Stop ` + -Confirm:$false + } + } + } + else + { + Write-Verbose -Message ($script:localizedData.CreateTeam -f $Name) + + try + { + $null = New-NetLbfoTeam ` + -Name $Name ` + -TeamMembers $teamMembers ` + -TeamingMode $TeamingMode ` + -LoadBalancingAlgorithm $loadBalancingAlgorithm ` + -ErrorAction Stop ` + -Confirm:$false + + Write-Verbose -Message $script:localizedData.CreatedNetTeam + } + + catch + { + New-InvalidOperationException ` + -Message ($script:localizedData.failedToCreateTeam -f $_.Exception.Message) + } + } + } + else + { + Write-Verbose -Message ($script:localizedData.RemoveTeam -f $Name) + + $null = Remove-NetLbfoTeam -Name $name -ErrorAction Stop -Confirm:$false + } +} + +<# + .SYNOPSIS + Tests is a specified Network Team is in the correct state. + + .PARAMETER Name + Specifies the name of the network team to create. + + .PARAMETER TeamMembers + Specifies the network interfaces that should be a part of the network team. + This is a comma-separated list. + + .PARAMETER TeamingMode + Specifies the teaming mode configuration. + + .PARAMETER LoadBalancingAlgorithm + Specifies the load balancing algorithm for the network team. + + .PARAMETER Ensure + Specifies if the network team should be created or deleted. +#> +function Test-TargetResource +{ + [CmdletBinding()] + [OutputType([System.Boolean])] + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $Name, + + [Parameter(Mandatory = $true)] + [System.String[]] + $TeamMembers, + + [Parameter()] + [ValidateSet('SwitchIndependent', 'LACP', 'Static')] + [System.String] + $TeamingMode = 'SwitchIndependent', + + [Parameter()] + [ValidateSet('Dynamic', 'HyperVPort', 'IPAddresses', 'MacAddresses', 'TransportPorts')] + [System.String] + $LoadBalancingAlgorithm = 'HyperVPort', + + [Parameter()] + [ValidateSet('Present', 'Absent')] + [System.String] + $Ensure = 'Present' + ) + + Write-Verbose -Message ($script:localizedData.GetTeamInfo -f $Name) + + $networkTeam = Get-NetLbfoTeam -Name $Name -ErrorAction SilentlyContinue + + if ($ensure -eq 'Present') + { + if ($networkTeam) + { + Write-Verbose -Message ($script:localizedData.FoundTeam -f $Name) + + if ( + ($networkTeam.LoadBalancingAlgorithm -eq $LoadBalancingAlgorithm) -and + ($networkTeam.teamingMode -eq $TeamingMode) -and + ($null -eq (Compare-Object -ReferenceObject $TeamMembers -DifferenceObject $networkTeam.Members)) + ) + { + Write-Verbose -Message ($script:localizedData.TeamExistsNoAction -f $Name) + + return $true + } + else + { + Write-Verbose -Message ($script:localizedData.TeamExistsWithDifferentConfig -f $Name) + + return $false + } + } + else + { + Write-Verbose -Message ($script:localizedData.TeamDoesNotExistShouldCreate -f $Name) + + return $false + } + } + else + { + if ($networkTeam) + { + Write-Verbose -Message ($script:localizedData.TeamExistsShouldRemove -f $Name) + + return $false + } + else + { + Write-Verbose -Message ($script:localizedData.TeamDoesNotExistNoAction -f $Name) + + return $true + } + } +} diff --git a/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_NetworkTeam/DSC_NetworkTeam.schema.mof b/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_NetworkTeam/DSC_NetworkTeam.schema.mof new file mode 100644 index 0000000..d28cfa7 --- /dev/null +++ b/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_NetworkTeam/DSC_NetworkTeam.schema.mof @@ -0,0 +1,9 @@ +[ClassVersion("1.1"), FriendlyName("NetworkTeam")] +class DSC_NetworkTeam : OMI_BaseResource +{ + [Key, Description("Specifies the name of the network team to create.")] String Name; + [Required, Description("Specifies the network interfaces that should be a part of the network team. This is a comma-separated list.")] String TeamMembers[]; + [Write, Description("Specifies the teaming mode configuration."), ValueMap{"SwitchIndependent","LACP","Static"}, Values{"SwitchIndependent","LACP","Static"}] String TeamingMode; + [Write, Description("Specifies the load balancing algorithm for the network team."), ValueMap{"Dynamic","HyperVPort","IPAddresses","MacAddresses","TransportPorts"}, Values{"Dynamic","HyperVPort","IPAddresses","MacAddresses","TransportPorts"}] String LoadBalancingAlgorithm; + [Write, Description("Specifies if the network team should be created or deleted. Defaults to 'Present'."), ValueMap{"Present","Absent"}, Values{"Present","Absent"}] String Ensure; +}; diff --git a/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_NetworkTeam/README.MD b/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_NetworkTeam/README.MD new file mode 100644 index 0000000..8653475 --- /dev/null +++ b/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_NetworkTeam/README.MD @@ -0,0 +1,3 @@ +# Description + +This resource is used to setup network teams on a node. diff --git a/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_NetworkTeam/en-US/DSC_NetworkTeam.strings.psd1 b/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_NetworkTeam/en-US/DSC_NetworkTeam.strings.psd1 new file mode 100644 index 0000000..01c922c --- /dev/null +++ b/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_NetworkTeam/en-US/DSC_NetworkTeam.strings.psd1 @@ -0,0 +1,24 @@ +# Localized resources for DSC_NetworkTeam + +ConvertFrom-StringData @' + GetTeamInfo = Getting network team information for '{0}'. + FoundTeam = Found a network team with name '{0}'. + TeamMembersMatch = Members in the network team '{0}' exist as per the configuration. + TeamMembersNotMatch = Members in the network team '{0}' do not match configuration. + TeamNotFound = Network team with name '{0}' not found. + LoadBalancingAlgorithmDifferent = Load Balancing algorithm is different from the requested '{0}' algorithm. + TeamingModeDifferent = Teaming mode is different from the requested '{0}' mode. + ModifyTeam = Modifying the network team named '{0}'. + MembersDifferent = Members within the team named '{0}' are different from that requested in the configuration. + RemovingMembers = Removing members '{0}' not specified in the configuration. + AddingMembers = Adding members '{0}' that are not a part of the team configuration. + CreateTeam = Creating a network team with the name '{0}'. + RemoveTeam = Removing a network team with the name '{0}'. + TeamExistsNoAction = Network team with name '{0}' exists. No action needed. + TeamExistsWithDifferentConfig = Network team with name '{0}' exists but with different configuration. This will be modified. + TeamDoesNotExistShouldCreate = Network team with name '{0}' does not exist. It will be created. + TeamExistsShouldRemove = Network team with name '{0}' exists. It will be removed. + TeamDoesNotExistNoAction = Network team with name '{0}' does not exist. No action needed. + CreatedNetTeam = Network Team was created successfully. + FailedToCreateTeam = Failed to create the network team with specific configuration: {0}. +'@ diff --git a/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_NetworkTeam/en-US/about_NetworkTeam.help.txt b/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_NetworkTeam/en-US/about_NetworkTeam.help.txt new file mode 100644 index 0000000..8c700d9 --- /dev/null +++ b/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_NetworkTeam/en-US/about_NetworkTeam.help.txt @@ -0,0 +1,78 @@ +.NAME + NetworkTeam + +.DESCRIPTION + This resource is used to setup network teams on a node. + +.PARAMETER Name + Key - String + Specifies the name of the network team to create. + +.PARAMETER TeamMembers + Required - StringArray + Specifies the network interfaces that should be a part of the network team. This is a comma-separated list. + +.PARAMETER TeamingMode + Write - String + Allowed values: SwitchIndependent, LACP, Static + Specifies the teaming mode configuration. + +.PARAMETER LoadBalancingAlgorithm + Write - String + Allowed values: Dynamic, HyperVPort, IPAddresses, MacAddresses, TransportPorts + Specifies the load balancing algorithm for the network team. + +.PARAMETER Ensure + Write - String + Allowed values: Present, Absent + Specifies if the network team should be created or deleted. Defaults to 'Present'. + +.EXAMPLE 1 + +Creates the switch independent Network Team 'HostTeam' using the NIC1 +and NIC2 interfaces. It sets the load balacing algorithm to 'HyperVPort'. +The config will then wait for the 'HostTeam' to achieve the 'Up' status. + +Configuration NetworkTeam_AddNetworkTeam_Config +{ + Import-DSCResource -ModuleName NetworkingDsc + + Node localhost + { + NetworkTeam AddNetworkTeam + { + Name = 'HostTeam' + TeamingMode = 'SwitchIndependent' + LoadBalancingAlgorithm = 'HyperVPort' + TeamMembers = 'NIC1', 'NIC2' + Ensure = 'Present' + } + + WaitForNetworkTeam WaitForHostTeam + { + Name = 'HostTeam' + DependsOn = '[NetworkTeam]AddNetworkTeam' + } + } +} + +.EXAMPLE 2 + +Removes the NIC Team 'HostTeam' from the interfaces NIC1, NIC2 and NIC3. + +Configuration NetworkTeam_RemoveNetworkTeam_Config +{ + Import-DSCResource -ModuleName NetworkingDsc + + Node localhost + { + NetworkTeam RemoveNetworkTeam + { + Name = 'HostTeam' + Ensure = 'Absent' + TeamMembers = 'NIC1', 'NIC2', 'NIC3' + } + } +} + + diff --git a/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_NetworkTeamInterface/DSC_NetworkTeamInterface.psm1 b/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_NetworkTeamInterface/DSC_NetworkTeamInterface.psm1 new file mode 100644 index 0000000..7495e66 --- /dev/null +++ b/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_NetworkTeamInterface/DSC_NetworkTeamInterface.psm1 @@ -0,0 +1,307 @@ +$modulePath = Join-Path -Path (Split-Path -Path (Split-Path -Path $PSScriptRoot -Parent) -Parent) -ChildPath 'Modules' + +# Import the Networking Common Modules +Import-Module -Name (Join-Path -Path $modulePath ` + -ChildPath (Join-Path -Path 'NetworkingDsc.Common' ` + -ChildPath 'NetworkingDsc.Common.psm1')) + +Import-Module -Name (Join-Path -Path $modulePath -ChildPath 'DscResource.Common') + +# Import Localization Strings +$script:localizedData = Get-LocalizedData -DefaultUICulture 'en-US' + +<# + .SYNOPSIS + Returns the current state of a network team interface in a Network Team. + + .PARAMETER Name + Specifies the name of the network team interface to create. + + .PARAMETER TeamName + Specifies the name of the network team on which this particular interface should exist. +#> +function Get-TargetResource +{ + [CmdletBinding()] + [OutputType([System.Collections.Hashtable])] + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $Name, + + [Parameter(Mandatory = $true)] + [System.String] + $TeamName + ) + + $configuration = @{ + Name = $Name + TeamName = $TeamName + } + + Write-Verbose -Message ($script:localizedData.GetTeamNicInfo -f $Name) + + $getNetLbfoTeamNicParameters = @{ + Name = $Name + Team = $TeamName + ErrorAction = 'SilentlyContinue' + } + $teamNic = Get-NetLbfoTeamNic @getNetLbfoTeamNicParameters + + if ($teamNic) + { + Write-Verbose -Message ($script:localizedData.FoundTeamNic -f $Name) + + $configuration.Add('VlanId', $teamNic.VlanId) + $configuration.Add('Ensure', 'Present') + } + else + { + Write-Verbose -Message ($script:localizedData.TeamNicNotFound -f $Name) + + $configuration.Add('Ensure', 'Absent') + } + + return $configuration +} + +<# + .SYNOPSIS + Adds, updates or removes a network team interface from a Network Team. + + .PARAMETER Name + Specifies the name of the network team interface to create. + + .PARAMETER TeamName + Specifies the name of the network team on which this particular interface should exist. + + .PARAMETER VlanId + Specifies VlanId to be set on network team interface. + + .PARAMETER Ensure + Specifies if the network team interface should be created or deleted. +#> +function Set-TargetResource +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $Name, + + [Parameter(Mandatory = $true)] + [System.String] + $TeamName, + + [Parameter()] + [System.UInt32] + $VlanId, + + [Parameter()] + [ValidateSet('Present', 'Absent')] + [System.String] + $Ensure = 'Present' + ) + + Write-Verbose -Message ($script:localizedData.GetTeamNicInfo -f $Name) + + $getNetLbfoTeamNicParameters = @{ + Name = $Name + Team = $TeamName + ErrorAction = 'SilentlyContinue' + } + $teamNic = Get-NetLbfoTeamNic @getNetLbfoTeamNicParameters + + if ($Ensure -eq 'Present') + { + if ($teamNic) + { + Write-Verbose -Message ($script:localizedData.FoundTeamNic -f $Name) + + if ($teamNic.VlanId -ne $VlanId) + { + Write-Verbose -Message ($script:localizedData.TeamNicVlanMismatch -f $VlanId) + + $isNetModifyRequired = $true + } + + if ($isNetModifyRequired) + { + Write-Verbose -Message ($script:localizedData.ModifyTeamNic -f $Name) + + if ($VlanId -eq 0) + { + $setNetLbfoTeamNicParameters = @{ + Name = $Name + Team = $TeamName + Default = $true + ErrorAction = 'Stop' + Confirm = $false + } + Set-NetLbfoTeamNic @setNetLbfoTeamNicParameters + } + else + { + <# + Required in case of primary interface, whose name gets changed + to include VLAN ID, if specified + #> + $setNetLbfoTeamNicParameters = @{ + Name = $Name + Team = $TeamName + VlanId = $VlanId + ErrorAction = 'Stop' + Confirm = $false + } + $renameNetAdapterParameters = @{ + NewName = $Name + ErrorAction = 'SilentlyContinue' + Confirm = $false + } + $null = Set-NetLbfoTeamNic @setNetLbfoTeamNicParameters | + Rename-NetAdapter @renameNetAdapterParameters + } + } + } + else + { + Write-Verbose -Message ($script:localizedData.CreateTeamNic -f $Name) + + if ($VlanId -ne 0) + { + $addNetLbfoTeamNicParameters = @{ + Name = $Name + Team = $TeamName + VlanId = $VlanId + ErrorAction = 'Stop' + Confirm = $false + } + $null = Add-NetLbfoTeamNic @addNetLbfoTeamNicParameters + + Write-Verbose -Message ($script:localizedData.CreatedNetTeamNic -f $Name) + } + else + { + New-InvalidOperationException ` + -Message ($script:localizedData.FailedToCreateTeamNic) + } + } + } + else + { + Write-Verbose -Message ($script:localizedData.RemoveTeamNic -f $Name) + + $removeNetLbfoTeamNicParameters = @{ + Team = $teamNic.Team + VlanId = $teamNic.VlanId + ErrorAction = 'Stop' + Confirm = $false + } + $null = Remove-NetLbfoTeamNic @removeNetLbfoTeamNicParameters + } +} + +<# + .SYNOPSIS + Tests is a specified Network Team Interface is in the correct state. + + .PARAMETER Name + Specifies the name of the network team interface to create. + + .PARAMETER TeamName + Specifies the name of the network team on which this particular interface should exist. + + .PARAMETER VlanId + Specifies VlanId to be set on network team interface. + + .PARAMETER Ensure + Specifies if the network team interface should be created or deleted. +#> +function Test-TargetResource +{ + [CmdletBinding()] + [OutputType([System.Boolean])] + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $Name, + + [Parameter(Mandatory = $true)] + [System.String] + $TeamName, + + [Parameter()] + [System.UInt32] + $VlanId, + + [Parameter()] + [ValidateSet('Present', 'Absent')] + [System.String] + $Ensure = 'Present' + ) + + Write-Verbose -Message ($script:localizedData.GetTeamNicInfo -f $Name) + + $getNetLbfoTeamNicParameters = @{ + Name = $Name + Team = $TeamName + ErrorAction = 'SilentlyContinue' + } + $teamNic = Get-NetLbfoTeamNic @getNetLbfoTeamNicParameters + + if ($VlanId -eq 0) + { + $VlanValue = $null + } + else + { + $VlanValue = $VlanId + } + + if ($Ensure -eq 'Present') + { + if ($teamNic) + { + Write-Verbose -Message ($script:localizedData.FoundTeamNic -f $Name) + + if ($teamNic.VlanId -eq $VlanValue) + { + Write-Verbose -Message ($script:localizedData.TeamNicExistsNoAction -f $Name) + + return $true + } + else + { + Write-Verbose -Message ($script:localizedData.TeamNicExistsWithDifferentConfig -f $Name) + + return $false + } + } + else + { + Write-Verbose -Message ($script:localizedData.TeamNicDoesNotExistShouldCreate -f $Name) + + return $false + } + } + else + { + if ($teamNic) + { + Write-Verbose -Message ($script:localizedData.TeamNicExistsShouldRemove -f $Name) + + return $false + } + else + { + Write-Verbose -Message ($script:localizedData.TeamNicExistsNoAction -f $Name) + + return $true + } + } +} + +Export-ModuleMember -Function *-TargetResource diff --git a/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_NetworkTeamInterface/DSC_NetworkTeamInterface.schema.mof b/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_NetworkTeamInterface/DSC_NetworkTeamInterface.schema.mof new file mode 100644 index 0000000..b7b3641 --- /dev/null +++ b/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_NetworkTeamInterface/DSC_NetworkTeamInterface.schema.mof @@ -0,0 +1,9 @@ + +[ClassVersion("1.0"), FriendlyName("NetworkTeamInterface")] +class DSC_NetworkTeamInterface : OMI_BaseResource +{ + [Key, Description("Specifies the name of the network team interface to create.")] String Name; + [Required, Description("Specifies the name of the network team on which this particular interface should exist.")] String TeamName; + [Write, Description("Specifies VLAN ID to be set on network team interface.")] Uint32 VlanId; + [Write, Description("Specifies if the network team interface should be created or deleted. Defaults to 'Present'."), ValueMap{"Present","Absent"}, Values{"Present","Absent"}] String Ensure; +}; diff --git a/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_NetworkTeamInterface/README.MD b/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_NetworkTeamInterface/README.MD new file mode 100644 index 0000000..1caa55c --- /dev/null +++ b/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_NetworkTeamInterface/README.MD @@ -0,0 +1,3 @@ +# Description + +This resource is used to add network interfaces to a network team. diff --git a/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_NetworkTeamInterface/en-US/DSC_NetworkTeamInterface.strings.psd1 b/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_NetworkTeamInterface/en-US/DSC_NetworkTeamInterface.strings.psd1 new file mode 100644 index 0000000..1fcff91 --- /dev/null +++ b/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_NetworkTeamInterface/en-US/DSC_NetworkTeamInterface.strings.psd1 @@ -0,0 +1,17 @@ +# Localized resources for DSC_NetwrkTeamInterface + +ConvertFrom-StringData @" + GetTeamNicInfo = Getting network team interface information for '{0}'. + FoundTeamNic = Found a network team interface with name '{0}'. + TeamNicNotFound = Network team interface with name '{0}' not found. + TeamNicVlanMismatch = Vlan ID is different from the requested ID of '{0}'. + ModifyTeamNic = Modifying the network team interface named '{0}'. + CreateTeamNic = Creating a network team interface with the name '{0}'. + RemoveTeamNic = Removing a network team interface with the name '{0}'. + TeamNicExistsNoAction = Network team interface with name '{0}' exists. No action needed. + TeamNicExistsWithDifferentConfig = Network team interface with name '{0}' exists but with different configuration. This will be modified. + TeamNicDoesNotExistShouldCreate = Network team interface with name '{0}' does not exist. It will be created. + TeamNicExistsShouldRemove = Network team interface with name '{0}' exists. It will be removed. + CreatedNetTeamNic = Network Team Interface was created successfully. + FailedToCreateTeamNic = Failed to create the network team interface with specific configuration. +"@ diff --git a/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_NetworkTeamInterface/en-US/about_NetworkTeamInterface.help.txt b/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_NetworkTeamInterface/en-US/about_NetworkTeamInterface.help.txt new file mode 100644 index 0000000..234f3bf --- /dev/null +++ b/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_NetworkTeamInterface/en-US/about_NetworkTeamInterface.help.txt @@ -0,0 +1,83 @@ +.NAME + NetworkTeamInterface + +.DESCRIPTION + This resource is used to add network interfaces to a network team. + +.PARAMETER Name + Key - String + Specifies the name of the network team interface to create. + +.PARAMETER TeamName + Required - String + Specifies the name of the network team on which this particular interface should exist. + +.PARAMETER VlanId + Write - UInt32 + Specifies VLAN ID to be set on network team interface. + +.PARAMETER Ensure + Write - String + Allowed values: Present, Absent + Specifies if the network team interface should be created or deleted. Defaults to 'Present'. + +.EXAMPLE 1 + +Add New Network Team Interface. + +Configuration NetworkTeamInterface_AddInterface_Config +{ + Import-DSCResource -ModuleName NetworkingDsc + + Node localhost + { + NetworkTeam HostTeam + { + Name = 'HostTeam' + TeamingMode = 'SwitchIndependent' + LoadBalancingAlgorithm = 'HyperVPort' + TeamMembers = 'NIC1','NIC2' + Ensure = 'Present' + } + + NetworkTeamInterface NewInterface + { + Name = 'NewInterface' + TeamName = 'HostTeam' + VlanID = 100 + Ensure = 'Present' + DependsOn = '[NetworkTeam]HostTeam' + } + } +} + +.EXAMPLE 2 + +Remove a Network Team Interface. + +Configuration NetworkTeamInterface_RemoveInterface_Config +{ + Import-DSCResource -ModuleName NetworkingDsc + + Node localhost + { + NetworkTeam HostTeam + { + Name = 'HostTeam' + TeamingMode = 'SwitchIndependent' + LoadBalancingAlgorithm = 'HyperVPort' + TeamMembers = 'NIC1','NIC2' + Ensure = 'Present' + } + + NetworkTeamInterface NewInterface + { + Name = 'NewInterface' + TeamName = 'HostTeam' + Ensure = 'Absent' + DependsOn = '[NetworkTeam]HostTeam' + } + } +} + + diff --git a/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_ProxySettings/DSC_ProxySettings.psm1 b/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_ProxySettings/DSC_ProxySettings.psm1 new file mode 100644 index 0000000..a9472a8 --- /dev/null +++ b/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_ProxySettings/DSC_ProxySettings.psm1 @@ -0,0 +1,853 @@ +$modulePath = Join-Path -Path (Split-Path -Path (Split-Path -Path $PSScriptRoot -Parent) -Parent) -ChildPath 'Modules' + +# Import the Networking Common Modules +Import-Module -Name (Join-Path -Path $modulePath ` + -ChildPath (Join-Path -Path 'NetworkingDsc.Common' ` + -ChildPath 'NetworkingDsc.Common.psm1')) + +Import-Module -Name (Join-Path -Path $modulePath -ChildPath 'DscResource.Common') + +# Import Localization Strings +$script:localizedData = Get-LocalizedData -DefaultUICulture 'en-US' + + +# Registry key paths for proxy settings +$script:connectionsRegistryKeyPath = 'SOFTWARE\Microsoft\Windows\CurrentVersion\Internet Settings\Connections' + +<# + .SYNOPSIS + Returns the current state of the proxy settings for + the computer. + + .PARAMETER IsSingleInstance + Specifies the resource is a single instance, the + value must be 'Yes'. Not used in Get-TargetResource. +#> +function Get-TargetResource +{ + [OutputType([System.Collections.Hashtable])] + param + ( + [Parameter(Mandatory = $true)] + [ValidateSet('Yes')] + [System.String] + $IsSingleInstance + ) + + Write-Verbose -Message ( @("$($MyInvocation.MyCommand): " + $($script:localizedData.GettingProxySettingsMessage) + ) -join '') + + $returnValue = @{} + + # Get the registry values in the Connections registry key + $connectionsRegistryValues = Get-ItemProperty ` + -Path "HKLM:\$($script:connectionsRegistryKeyPath)" ` + -ErrorAction SilentlyContinue + + $proxySettingsRegistryBinary = $null + + if ($connectionsRegistryValues.DefaultConnectionSettings) + { + $proxySettingsRegistryBinary = $connectionsRegistryValues.DefaultConnectionSettings + } + elseif ($connectionsRegistryValues.SavedLegacySettings) + { + $proxySettingsRegistryBinary = $connectionsRegistryValues.SavedLegacySettings + } + + if ($proxySettingsRegistryBinary) + { + $returnValue.Add('Ensure','Present') + + $proxySettings = ConvertFrom-ProxySettingsBinary -ProxySettings $proxySettingsRegistryBinary + + $returnValue.Add('EnableManualProxy',$proxySettings.EnableManualProxy) + $returnValue.Add('EnableAutoConfiguration',$proxySettings.EnableAutoConfiguration) + $returnValue.Add('EnableAutoDetection',$proxySettings.EnableAutoDetection) + $returnValue.Add('ProxyServer',$proxySettings.ProxyServer) + $returnValue.Add('ProxyServerBypassLocal',$proxySettings.ProxyServerBypassLocal) + $returnValue.Add('ProxyServerExceptions',$proxySettings.ProxyServerExceptions) + $returnValue.Add('AutoConfigURL',$proxySettings.AutoConfigURL) + } + else + { + $returnValue.Add('Ensure','Absent') + } + + return $returnValue +} + +<# + .SYNOPSIS + Sets the current state of the proxy settings for + the computer. + + .PARAMETER IsSingleInstance + Specifies the resource is a single instance, the + value must be 'Yes'. + + .PARAMETER Ensure + Specifies if computer proxy settings should be set. + Defaults to 'Present'. + + .PARAMETER ConnectionType + Defines if the proxy settings should be configured + for default connections, legacy connections or all + connections. Defaults to 'All'. + + .PARAMETER EnableAutoDetection + Enable automatic detection of the proxy settings. Defaults + to 'False'. + + .PARAMETER EnableAutoConfiguration + Use automatic configuration script for specifying proxy + settings. Defaults to 'False'. + + .PARAMETER EnableManualProxy + Use manual proxy server settings. Defaults to 'False'. + + .PARAMETER AutoConfigURL + The URL of the automatic configuration script to specify + the proxy settings. Should be specified if 'EnableAutoConfiguration' + is 'True'. + + .PARAMETER ProxyServer + The address and port of the manual proxy server to use. + Should be specified if 'EnableManualProxy' is 'True'. + + .PARAMETER ProxyServerExceptions + Bypass proxy server for addresses starting with addresses + in this list. + + .PARAMETER ProxyServerBypassLocal + Bypass proxy server for local addresses. Defaults to 'False'. +#> +function Set-TargetResource +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [ValidateSet('Yes')] + [System.String] + $IsSingleInstance, + + [Parameter()] + [ValidateSet('Present','Absent')] + [System.String] + $Ensure = 'Present', + + [Parameter()] + [ValidateSet('All','Default','Legacy')] + [System.String] + $ConnectionType = 'All', + + [Parameter()] + [System.Boolean] + $EnableAutoDetection = $false, + + [Parameter()] + [System.Boolean] + $EnableAutoConfiguration = $false, + + [Parameter()] + [System.Boolean] + $EnableManualProxy = $false, + + [Parameter()] + [System.String] + $AutoConfigURL, + + [Parameter()] + [System.String] + $ProxyServer, + + [Parameter()] + [System.String[]] + $ProxyServerExceptions = @(), + + [Parameter()] + [System.Boolean] + $ProxyServerBypassLocal = $false + ) + + Write-Verbose -Message ( @("$($MyInvocation.MyCommand): " + $($script:localizedData.ApplyingProxySettingsMessage -f $Ensure) + ) -join '') + + if ($Ensure -eq 'Absent') + { + # Remove all the Proxy Settings + Write-Verbose -Message ( @("$($MyInvocation.MyCommand): " + $($script:localizedData.DisablingComputerProxyMessage) + ) -join '') + + if ($ConnectionType -in ('All','Default')) + { + Remove-ItemProperty ` + -Path "HKLM:\$($script:connectionsRegistryKeyPath)" ` + -Name 'DefaultConnectionSettings' ` + -ErrorAction SilentlyContinue + } + + if ($ConnectionType -in ('All','Legacy')) + { + Remove-ItemProperty ` + -Path "HKLM:\$($script:connectionsRegistryKeyPath)" ` + -Name 'SavedLegacySettings' ` + -ErrorAction SilentlyContinue + } + } + else + { + Write-Verbose -Message ( @("$($MyInvocation.MyCommand): " + $($script:localizedData.EnablingComputerProxyMessage) + ) -join '') + + # Generate the Proxy Settings binary value + $convertToProxySettingsBinaryParameters = @{} + $PSBoundParameters + + $convertToProxySettingsBinaryParameters.Remove('IsSingleInstance') + $convertToProxySettingsBinaryParameters.Remove('Ensure') + $convertToProxySettingsBinaryParameters.Remove('ConnectionType') + + $proxySettings = ConvertTo-ProxySettingsBinary @convertToProxySettingsBinaryParameters + + if ($ConnectionType -in ('All','Default')) + { + Write-Verbose -Message ( @("$($MyInvocation.MyCommand): " + $($script:localizedData.WritingComputerProxyBinarySettingsMessage -f 'DefaultConnectionSettings',($proxySettings -join ',')) + ) -join '') + + Set-BinaryRegistryValue ` + -Path "HKEY_LOCAL_MACHINE\$($script:connectionsRegistryKeyPath)" ` + -Name 'DefaultConnectionSettings' ` + -Value $proxySettings + } + + if ($ConnectionType -in ('All','Legacy')) + { + Write-Verbose -Message ( @("$($MyInvocation.MyCommand): " + $($script:localizedData.WritingComputerProxyBinarySettingsMessage -f 'SavedLegacySettings',($proxySettings -join ',')) + ) -join '') + + Set-BinaryRegistryValue ` + -Path "HKEY_LOCAL_MACHINE\$($script:connectionsRegistryKeyPath)" ` + -Name 'SavedLegacySettings' ` + -Value $proxySettings + } + } +} # Set-TargetResource + +<# + .SYNOPSIS + Tests the current state of the proxy settings for + the computer. + + .PARAMETER IsSingleInstance + Specifies the resource is a single instance, the + value must be 'Yes'. + + .PARAMETER Ensure + Specifies if computer proxy settings should be set. + Defaults to 'Present'. + + .PARAMETER ConnectionType + Defines if the proxy settings should be configured + for default connections, legacy connections or all + connections. Defaults to 'All'. + + .PARAMETER EnableAutoDetection + Enable automatic detection of the proxy settings. Defaults + to 'False'. + + .PARAMETER EnableAutoConfiguration + Use automatic configuration script for specifying proxy + settings. Defaults to 'False'. + + .PARAMETER EnableManualProxy + Use manual proxy server settings. Defaults to 'False'. + + .PARAMETER AutoConfigURL + The URL of the automatic configuration script to specify + the proxy settings. Should be specified if 'EnableAutoConfiguration' + is 'True'. + + .PARAMETER ProxyServer + The address and port of the manual proxy server to use. + Should be specified if 'EnableManualProxy' is 'True'. + + .PARAMETER ProxyServerExceptions + Bypass proxy server for addresses starting with addresses + in this list. + + .PARAMETER ProxyServerBypassLocal + Bypass proxy server for local addresses. Defaults to 'False'. +#> +function Test-TargetResource +{ + [CmdletBinding()] + [OutputType([System.Boolean])] + param + ( + [Parameter(Mandatory = $true)] + [ValidateSet('Yes')] + [System.String] + $IsSingleInstance, + + [Parameter()] + [ValidateSet('Present','Absent')] + [System.String] + $Ensure = 'Present', + + [Parameter()] + [ValidateSet('All','Default','Legacy')] + [System.String] + $ConnectionType = 'All', + + [Parameter()] + [System.Boolean] + $EnableAutoDetection = $false, + + [Parameter()] + [System.Boolean] + $EnableAutoConfiguration = $false, + + [Parameter()] + [System.Boolean] + $EnableManualProxy = $false, + + [Parameter()] + [System.String] + $AutoConfigURL, + + [Parameter()] + [System.String] + $ProxyServer, + + [Parameter()] + [System.String[]] + $ProxyServerExceptions = @(), + + [Parameter()] + [System.Boolean] + $ProxyServerBypassLocal = $false + ) + + Write-Verbose -Message ( @("$($MyInvocation.MyCommand): " + $($script:localizedData.CheckingProxySettingsMessage -f $Ensure) + ) -join '') + + [System.Boolean] $desiredConfigurationMatch = $true + + # Get the registry values in the Connections registry key + $connectionsRegistryValues = Get-ItemProperty ` + -Path "HKLM:\$($script:connectionsRegistryKeyPath)" ` + -ErrorAction SilentlyContinue + + if ($Ensure -eq 'Absent') + { + # Check if any of the Proxy Settings need to be removed + if ($ConnectionType -in ('All','Default')) + { + # Does the Default Connection Settings need to be removed? + if ($connectionsRegistryValues.DefaultConnectionSettings) + { + Write-Verbose -Message ( @("$($MyInvocation.MyCommand): " + $($script:localizedData.ComputerProxyBinarySettingsRequiresRemovalMessage -f 'DefaultConnectionSettings') + ) -join '') + + $desiredConfigurationMatch = $false + } + } + + # Does the Saved Legacy Settings need to be removed? + if ($ConnectionType -in ('All','Legacy')) + { + if ($connectionsRegistryValues.SavedLegacySettings) + { + Write-Verbose -Message ( @("$($MyInvocation.MyCommand): " + $($script:localizedData.ComputerProxyBinarySettingsRequiresRemovalMessage -f 'SavedLegacySettings') + ) -join '') + + $desiredConfigurationMatch = $false + } + } + } + else + { + $desiredValues = @{} + $PSBoundParameters + + $desiredValues.Remove('IsSingleInstance') + $desiredValues.Remove('Ensure') + $desiredValues.Remove('ConnectionType') + + if ($ConnectionType -in ('All','Default')) + { + # Check if the Default Connection proxy settings are in the desired state + Write-Verbose -Message ( @("$($MyInvocation.MyCommand): " + $($script:localizedData.CheckingComputerProxyBinarySettingsMessage -f 'DefaultConnectionSettings') + ) -join '') + + if ($connectionsRegistryValues.DefaultConnectionSettings) + { + $defaultConnectionSettings = ConvertFrom-ProxySettingsBinary -ProxySettings $connectionsRegistryValues.DefaultConnectionSettings + } + else + { + $defaultConnectionSettings = @{} + } + + $inDesiredState = Test-ProxySettings -CurrentValues $defaultConnectionSettings -DesiredValues $desiredValues + + if (-not $inDesiredState) + { + Write-Verbose -Message ( @("$($MyInvocation.MyCommand): " + $($script:localizedData.ComputerProxyBinarySettingsNoMatchMessage -f 'DefaultConnectionSettings') + ) -join '') + + $desiredConfigurationMatch = $false + } + } + + if ($ConnectionType -in ('All','Legacy')) + { + # Check if the Saved Legacy proxy settings are in the desired state + Write-Verbose -Message ( @("$($MyInvocation.MyCommand): " + $($script:localizedData.CheckingComputerProxyBinarySettingsMessage -f 'SavedLegacySettings') + ) -join '') + + if ($connectionsRegistryValues.SavedLegacySettings) + { + $savedLegacySettings = ConvertFrom-ProxySettingsBinary -ProxySettings $connectionsRegistryValues.SavedLegacySettings + } + else + { + $savedLegacySettings = @{} + } + + $inDesiredState = Test-ProxySettings -CurrentValues $savedLegacySettings -DesiredValues $desiredValues + + if (-not $inDesiredState) + { + Write-Verbose -Message ( @("$($MyInvocation.MyCommand): " + $($script:localizedData.ComputerProxyBinarySettingsNoMatchMessage -f 'SavedLegacySettings') + ) -join '') + + $desiredConfigurationMatch = $false + } + } + } + + return $desiredConfigurationMatch +} # Test-TargetResource + +<# + .SYNOPSIS + Sets a binary value in a Registry Key. + + .PARAMETER Path + The path to the registry key containing the value. + + .PARAMETER Name + The name of the registry value. + + .PARAMETER Value + The value to put into the binary registry value. +#> +function Set-BinaryRegistryValue +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $Path, + + [Parameter(Mandatory = $true)] + [System.String] + $Name, + + [Parameter(Mandatory = $true)] + [System.Byte[]] + $Value + ) + + $null = [Microsoft.Win32.Registry]::SetValue($Path, $Name, $Value, 'Binary') +} + +<# + .SYNOPSIS + Checks if the current proxy setting values are in the desired + state. Returns $true if in the desired state. + + .PARAMETER CurrentValues + An object containing the current values of the Proxy Settings. + + .PARAMETER DesiredValues + An object containing the desired values of the Proxy Settings. +#> +function Test-ProxySettings +{ + [CmdletBinding()] + [OutputType([System.Boolean])] + param + ( + [Parameter(Mandatory = $true)] + [System.Collections.Hashtable] + $CurrentValues, + + [Parameter(Mandatory = $true)] + [System.Object] + $DesiredValues + ) + + [System.Boolean] $inState = $true + + $proxySettingsToCompare = @( + 'EnableManualProxy' + 'EnableAutoConfiguration' + 'EnableAutoDetection' + 'ProxyServer' + 'ProxyServerBypassLocal' + 'AutoConfigURL' + ) + + # Test the string and boolean values + foreach ($proxySetting in $proxySettingsToCompare) + { + if ($DesiredValues.ContainsKey($proxySetting) -and ($DesiredValues.$proxySetting -ne $CurrentValues.$proxySetting)) + { + Write-Verbose -Message ( @("$($MyInvocation.MyCommand): " + $($script:localizedData.ProxySettingMismatchMessage -f $proxySetting,$CurrentValues.$proxySetting,$DesiredValues.$proxySetting) + ) -join '') + + $inState = $false + } + } + + # Test the array value + if ($DesiredValues.ContainsKey('ProxyServerExceptions') ` + -and $CurrentValues.ProxyServerExceptions ` + -and @(Compare-Object ` + -ReferenceObject $DesiredValues.ProxyServerExceptions ` + -DifferenceObject $CurrentValues.ProxyServerExceptions).Count -gt 0) + { + Write-Verbose -Message ( @("$($MyInvocation.MyCommand): " + $($script:localizedData.ProxySettingMismatchMessage -f 'ProxyServerExceptions',($CurrentValues.ProxyServerExceptions -join ';'),($DesiredValues.ProxyServerExceptions -join ';')) + ) -join '') + + $inState = $false + } + + return $inState +} + +<# + .SYNOPSIS + Get the length of a string in the format of an array + of hexidecimal format strings. + + .PARAMETER Value + The string to return the length for. +#> +function Get-StringLengthInHexBytes +{ + [CmdletBinding()] + [OutputType([System.Object[]])] + param + ( + [Parameter(Mandatory = $true)] + [AllowEmptyString()] + [System.String] + $Value + ) + + $hex = '{0:x8}' -f $Value.Length + $stringLength = @() + $stringLength += @('0x' + $hex.Substring(6,2)) + $stringLength += @('0x' + $hex.Substring(4,2)) + $stringLength += @('0x' + $hex.Substring(2,2)) + $stringLength += @('0x' + $hex.Substring(0,2)) + + return $stringLength +} + +<# + .SYNOPSIS + Gets an int32 from 4 little endian bytes containing in a + byte array. + + .PARAMETER Bytes + The bytes containing the little endian int32. +#> +function Get-Int32FromByteArray +{ + [CmdletBinding()] + [OutputType([System.Int32])] + param + ( + [Parameter(Mandatory = $true)] + [System.Byte[]] + $Byte, + + [Parameter(Mandatory = $true)] + [System.Int32] + $StartByte + ) + + $value = [System.Int32] 0 + $value += [System.Int32] $Byte[$StartByte] + $value += [System.Int32] $Byte[$StartByte + 1] -shl 8 + $value += [System.Int32] $Byte[$StartByte + 2] -shl 16 + $value += [System.Int32] $Byte[$StartByte + 3] -shl 24 + + return $value +} + +<# + .SYNOPSIS + Convert the proxy settings parameters to a Byte Array that + can be used to populate the DefaultConnectionSettings and + SavedLegacySettings registry settings. + + .PARAMETER EnableAutoDetection + Enable automatic detection of the proxy settings. Defaults + to 'False'. + + .PARAMETER EnableAutoConfiguration + Use automatic configuration script for specifying proxy + settings. Defaults to 'False'. + + .PARAMETER EnableManualProxy + Use manual proxy server settings. Defaults to 'False'. + + .PARAMETER AutoConfigURL + The URL of the automatic configuration script to specify + the proxy settings. Should be specified if 'EnableAutoConfiguration' + is 'True'. + + .PARAMETER ProxyServer + The address and port of the manual proxy server to use. + Should be specified if 'EnableManualProxy' is 'True'. + + .PARAMETER ProxyServerExceptions + Bypass proxy server for addresses starting with addresses + in this list. + + .PARAMETER ProxyServerBypassLocal + Bypass proxy server for local addresses. Defaults to 'False'. +#> +function ConvertTo-ProxySettingsBinary +{ + [CmdletBinding()] + [OutputType([System.Byte[]])] + param + ( + [Parameter()] + [System.Boolean] + $EnableAutoDetection = $false, + + [Parameter()] + [System.Boolean] + $EnableAutoConfiguration = $false, + + [Parameter()] + [System.Boolean] + $EnableManualProxy = $false, + + [Parameter()] + [System.String] + $AutoConfigURL, + + [Parameter()] + [System.String] + $ProxyServer, + + [Parameter()] + [System.String[]] + $ProxyServerExceptions = @(), + + [Parameter()] + [System.Boolean] + $ProxyServerBypassLocal = $false + ) + + [System.Byte[]] $proxySettings = @(0x46, 0x0, 0x0, 0x0, 0x8, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0) + + if ($EnableManualProxy) + { + $proxySettings[8] = $proxySettings[8] + 2 + } + + if ($EnableAutoConfiguration) + { + $proxySettings[8] = $proxySettings[8] + 4 + } + + if ($EnableAutoDetection) + { + $proxySettings[8] = $proxySettings[8] + 8 + } + + if ($PSBoundParameters.ContainsKey('ProxyServer')) + { + $proxySettings += Get-StringLengthInHexBytes -Value $ProxyServer + $proxySettings += [Byte[]][Char[]] $ProxyServer + } + else + { + $proxySettings += @(0x0, 0x0, 0x0, 0x0) + } + + if ($ProxyServerBypassLocal -eq $true) + { + $ProxyServerExceptions += @('') + } + + if ($ProxyServerExceptions.Count -gt 0) + { + $proxyServerExceptionsString = $ProxyServerExceptions -join ';' + $proxySettings += Get-StringLengthInHexBytes -Value $proxyServerExceptionsString + $proxySettings += [Byte[]][Char[]] $proxyServerExceptionsString + } + else + { + $proxySettings += @(0x0, 0x0, 0x0, 0x0) + } + + if ($PSBoundParameters.ContainsKey('AutoConfigURL')) + { + $proxySettings += Get-StringLengthInHexBytes -Value $AutoConfigURL + $proxySettings += [Byte[]][Char[]] $AutoConfigURL + } + + $proxySettings += @(0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0) + $proxySettings += @(0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0) + + return [System.Byte[]] $proxySettings +} + +<# + .SYNOPSIS + Convert from a Byte Array pulled from the proxy settings + DefaultConnectionSettings and SavedLegacySettings in the + registry into an object. + + .PARAMETER ProxySettings + The binary extracted from the registry key + DefaultConnectionSettings or SavedLegacySettings. + +#> +function ConvertFrom-ProxySettingsBinary +{ + [CmdletBinding()] + [OutputType([PSObject])] + param + ( + [Parameter(Mandatory = $true)] + [System.Byte[]] + $ProxySettings + ) + + $proxyParameters = @{} + + if ($ProxySettings.Count -gt 0) + { + # Do a smoke test on the binary to check it looks valid + if ($ProxySettings[0] -ne 0x46) + { + New-InvalidOperationException ` + -Message ($script:localizedData.ProxySettingsBinaryInvalidError -f $ProxySettings[0]) + } + + # Figure out the proxy settings that are enabled + $proxyBits = $ProxySettings[8] + + $enableManualProxy = $false + $enableAutoConfiguration = $false + $enableAutoDetection = $false + + if (($proxyBits -band 0x2) -gt 0) + { + $enableManualProxy = $true + } + + if (($proxyBits -band 0x4) -gt 0) + { + $enableAutoConfiguration = $true + } + + if (($proxyBits -band 0x8) -gt 0) + { + $enableAutoDetection = $true + } + + $proxyParameters.Add('EnableManualProxy',$enableManualProxy) + $proxyParameters.Add('EnableAutoConfiguration',$enableAutoConfiguration) + $proxyParameters.Add('EnableAutoDetection',$enableAutoDetection) + + $stringPointer = 12 + + # Extract the Proxy Server string + $proxyServer = '' + $stringLength = Get-Int32FromByteArray -Byte $ProxySettings -StartByte $stringPointer + $stringPointer += 4 + + if ($stringLength -gt 0) + { + $stringBytes = New-Object -TypeName Byte[] -ArgumentList $stringLength + $null = [System.Buffer]::BlockCopy($ProxySettings,$stringPointer,$stringBytes,0,$stringLength) + $proxyServer = [System.Text.Encoding]::ASCII.GetString($stringBytes) + $stringPointer += $stringLength + } + + $proxyParameters.Add('ProxyServer',$proxyServer) + + # Extract the Proxy Server Exceptions string + $proxyServerExceptions = @() + $stringLength = Get-Int32FromByteArray -Byte $ProxySettings -StartByte $stringPointer + $stringPointer += 4 + + if ($stringLength -gt 0) + { + $stringBytes = New-Object -TypeName Byte[] -ArgumentList $stringLength + $null = [System.Buffer]::BlockCopy($ProxySettings,$stringPointer,$stringBytes,0,$stringLength) + $proxyServerExceptionsString = [System.Text.Encoding]::ASCII.GetString($stringBytes) + $stringPointer += $stringLength + $proxyServerExceptions = [System.String[]] ($proxyServerExceptionsString -split ';') + } + + if ($proxyServerExceptions.Contains('')) + { + $proxyServerExceptions = $proxyServerExceptions | Where-Object -FilterScript { $_ -ne '' } + $proxyParameters.Add('ProxyServerBypassLocal',$true) + } + else + { + $proxyParameters.Add('ProxyServerBypassLocal',$false) + } + + $proxyParameters.Add('ProxyServerExceptions',$proxyServerExceptions) + + # Extract the Auto Config URL string + $autoConfigURL = '' + $stringLength = Get-Int32FromByteArray -Byte $ProxySettings -StartByte $stringPointer + $stringPointer += 4 + + if ($stringLength -gt 0) + { + $stringBytes = New-Object -TypeName Byte[] -ArgumentList $stringLength + $null = [System.Buffer]::BlockCopy($ProxySettings,$stringPointer,$stringBytes,0,$stringLength) + $autoConfigURL = [System.Text.Encoding]::ASCII.GetString($stringBytes) + $stringPointer += $stringLength + } + + $proxyParameters.Add('AutoConfigURL',$autoConfigURL) + } + + return [PSObject] $proxyParameters +} + +Export-ModuleMember -function *-TargetResource diff --git a/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_ProxySettings/DSC_ProxySettings.schema.mof b/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_ProxySettings/DSC_ProxySettings.schema.mof new file mode 100644 index 0000000..8c270af --- /dev/null +++ b/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_ProxySettings/DSC_ProxySettings.schema.mof @@ -0,0 +1,14 @@ +[ClassVersion("1.0.0"), FriendlyName("ProxySettings")] +class DSC_ProxySettings : OMI_BaseResource +{ + [Key, Description("Specifies the resource is a single instance, the value must be 'Yes'."), ValueMap{"Yes"}, Values{"Yes"}] String IsSingleInstance; + [Write, Description("Specifies if computer proxy settings should be set. Defaults to 'Present'."), ValueMap{"Present","Absent"}, Values{"Present","Absent"}] String Ensure; + [Write, Description("Defines if the proxy settings should be configured for default connections, legacy connections or all connections. Defaults to 'All'."), ValueMap{"All","Default","Legacy"}, Values{"All","Default","Legacy"}] String ConnectionType; + [Write, Description("Enable automatic detection of the proxy settings. Defaults to 'False'.")] Boolean EnableAutoDetection; + [Write, Description("Use automatic configuration script for specifying proxy settings. Defaults to 'False'.")] Boolean EnableAutoConfiguration; + [Write, Description("Use manual proxy server settings. Defaults to 'False'.")] Boolean EnableManualProxy; + [Write, Description("The URL of the automatic configuration script to specify the proxy settings. Should be specified if 'EnableAutoConfiguration' is 'True'.")] String AutoConfigURL; + [Write, Description("The address and port of the manual proxy server to use. Should be specified if 'EnableManualProxy' is 'True'.")] String ProxyServer; + [Write, Description("Bypass proxy server for addresses starting with addresses in this list.")] String ProxyServerExceptions[]; + [Write, Description("Bypass proxy server for local addresses. Defaults to 'False'.")] Boolean ProxyServerBypassLocal; +}; diff --git a/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_ProxySettings/README.md b/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_ProxySettings/README.md new file mode 100644 index 0000000..208f811 --- /dev/null +++ b/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_ProxySettings/README.md @@ -0,0 +1,3 @@ +# Description + +The resource is used to configure internet proxy settings for a computer. diff --git a/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_ProxySettings/en-US/DSC_ProxySettings.strings.psd1 b/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_ProxySettings/en-US/DSC_ProxySettings.strings.psd1 new file mode 100644 index 0000000..3bea22a --- /dev/null +++ b/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_ProxySettings/en-US/DSC_ProxySettings.strings.psd1 @@ -0,0 +1,15 @@ +# Localized resources for DSC_ProxySettings + +ConvertFrom-StringData @' + GettingProxySettingsMessage = Getting the computer proxy settings. + ApplyingProxySettingsMessage = Applying the computer proxy settings to ensure '{0}'. + CheckingProxySettingsMessage = Checking the computer proxy settings to ensure '{0}'. + ComputerProxyBinarySettingsRequiresRemovalMessage = The computer proxy settings '{0}' need to be removed. + CheckingComputerProxyBinarySettingsMessage = Checking that the computer proxy settings '{0}' are in the desired state. + ComputerProxyBinarySettingsNoMatchMessage = Computer proxy settings '{0}' are not in the desired state. + DisablingComputerProxyMessage = Disabling computer proxy settings. + EnablingComputerProxyMessage = Enabling computer proxy settings. + ProxySettingMismatchMessage = The proxy setting '{0}' value '{1}' does not match the desired value '{2}'. + WritingComputerProxyBinarySettingsMessage = Writing computer proxy settings '{0}' binary '{1}'. + ProxySettingsBinaryInvalidError = The first byte of the proxy settings binary was '{0}' but should have been 0x46. +'@ diff --git a/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_ProxySettings/en-US/about_ProxySettings.help.txt b/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_ProxySettings/en-US/about_ProxySettings.help.txt new file mode 100644 index 0000000..411717a --- /dev/null +++ b/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_ProxySettings/en-US/about_ProxySettings.help.txt @@ -0,0 +1,121 @@ +.NAME + ProxySettings + +.DESCRIPTION + The resource is used to configure internet proxy settings for a computer. + +.PARAMETER IsSingleInstance + Key - String + Allowed values: Yes + Specifies the resource is a single instance, the value must be 'Yes'. + +.PARAMETER Ensure + Write - String + Allowed values: Present, Absent + Specifies if computer proxy settings should be set. Defaults to 'Present'. + +.PARAMETER ConnectionType + Write - String + Allowed values: All, Default, Legacy + Defines if the proxy settings should be configured for default connections, legacy connections or all connections. Defaults to 'All'. + +.PARAMETER EnableAutoDetection + Write - Boolean + Enable automatic detection of the proxy settings. Defaults to 'False'. + +.PARAMETER EnableAutoConfiguration + Write - Boolean + Use automatic configuration script for specifying proxy settings. Defaults to 'False'. + +.PARAMETER EnableManualProxy + Write - Boolean + Use manual proxy server settings. Defaults to 'False'. + +.PARAMETER AutoConfigURL + Write - String + The URL of the automatic configuration script to specify the proxy settings. Should be specified if 'EnableAutoConfiguration' is 'True'. + +.PARAMETER ProxyServer + Write - String + The address and port of the manual proxy server to use. Should be specified if 'EnableManualProxy' is 'True'. + +.PARAMETER ProxyServerExceptions + Write - StringArray + Bypass proxy server for addresses starting with addresses in this list. + +.PARAMETER ProxyServerBypassLocal + Write - Boolean + Bypass proxy server for local addresses. Defaults to 'False'. + +.EXAMPLE 1 + +Sets the computer to automatically detect the proxy settings. + +Configuration ProxySettings_AutoDetectProxy_Config +{ + Import-DSCResource -ModuleName NetworkingDsc + + Node localhost + { + ProxySettings AutoDetectProxy + { + IsSingleInstance = 'Yes' + Ensure = 'Present' + EnableAutoDetection = $true + EnableAutoConfiguration = $false + EnableManualProxy = $false + } + } +} + +.EXAMPLE 2 + +Sets the computer to use an automatic WPAD configuration script that will +be downloaded from the URL 'http://wpad.contoso.com/wpad.dat'. + +Configuration ProxySettings_AutoConfigurationProxy_Config +{ + Import-DSCResource -ModuleName NetworkingDsc + + Node localhost + { + ProxySettings AutoConfigurationProxy + { + IsSingleInstance = 'Yes' + Ensure = 'Present' + EnableAutoDetection = $false + EnableAutoConfiguration = $true + EnableManualProxy = $false + AutoConfigURL = 'http://wpad.contoso.com/wpad.dat' + } + } +} + +.EXAMPLE 3 + +Sets the computer to use a manually configured proxy server +with the address 'proxy.contoso.com' on port 8888. Traffic to addresses +starting with 'web1' or 'web2' or any local addresses will not be sent +to the proxy. + +Configuration ProxySettings_ManualProxy_Config +{ + Import-DSCResource -ModuleName NetworkingDsc + + Node localhost + { + ProxySettings ManualProxy + { + IsSingleInstance = 'Yes' + Ensure = 'Present' + EnableAutoDetection = $false + EnableAutoConfiguration = $false + EnableManualProxy = $true + ProxyServer = 'proxy.contoso.com:8888' + ProxyServerExceptions = 'web1', 'web2' + ProxyServerBypassLocal = $true + } + } +} + + diff --git a/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_Route/DSC_Route.psm1 b/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_Route/DSC_Route.psm1 new file mode 100644 index 0000000..fbdc1c2 --- /dev/null +++ b/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_Route/DSC_Route.psm1 @@ -0,0 +1,633 @@ +$modulePath = Join-Path -Path (Split-Path -Path (Split-Path -Path $PSScriptRoot -Parent) -Parent) -ChildPath 'Modules' + +# Import the Networking Common Modules +Import-Module -Name (Join-Path -Path $modulePath ` + -ChildPath (Join-Path -Path 'NetworkingDsc.Common' ` + -ChildPath 'NetworkingDsc.Common.psm1')) + +Import-Module -Name (Join-Path -Path $modulePath -ChildPath 'DscResource.Common') + +# Import Localization Strings +$script:localizedData = Get-LocalizedData -DefaultUICulture 'en-US' + +<# + .SYNOPSIS + Returns the current state of a Route for an interface. + + .PARAMETER InterfaceAlias + Specifies the alias of a network interface. + + .PARAMETER AddressFamily + Specify the IP address family. + + .PARAMETER DestinationPrefix + Specifies a destination prefix of an IP route. + A destination prefix consists of an IP address prefix + and a prefix length, separated by a slash (/). + + .PARAMETER NextHop + Specifies the next hop for the IP route. +#> +function Get-TargetResource +{ + [CmdletBinding()] + [OutputType([System.Collections.Hashtable])] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $InterfaceAlias, + + [Parameter(Mandatory = $true)] + [ValidateSet('IPv4', 'IPv6')] + [System.String] + $AddressFamily, + + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $DestinationPrefix, + + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $NextHop + ) + + Write-Verbose -Message ( @( + "$($MyInvocation.MyCommand): " + $($script:localizedData.GettingRouteMessage) ` + -f $AddressFamily, $InterfaceAlias, $DestinationPrefix, $NextHop ` + ) -join '' ) + + # Lookup the existing Route + $route = Get-Route @PSBoundParameters + + $returnValue = @{ + InterfaceAlias = $InterfaceAlias + AddressFamily = $AddressFamily + DestinationPrefix = $DestinationPrefix + NextHop = $NextHop + } + + if ($route) + { + Write-Verbose -Message ( @( + "$($MyInvocation.MyCommand): " + $($script:localizedData.RouteExistsMessage) ` + -f $AddressFamily, $InterfaceAlias, $DestinationPrefix, $NextHop ` + ) -join '' ) + + $returnValue += @{ + Ensure = 'Present' + RouteMetric = [System.Uint16] $route.RouteMetric + Publish = $route.Publish + PreferredLifetime = [System.Double] $route.PreferredLifetime.TotalSeconds + } + } + else + { + Write-Verbose -Message ( @( + "$($MyInvocation.MyCommand): " + $($script:localizedData.RouteDoesNotExistMessage) ` + -f $AddressFamily, $InterfaceAlias, $DestinationPrefix, $NextHop ` + ) -join '' ) + + $returnValue += @{ + Ensure = 'Absent' + } + } + + return $returnValue +} # Get-TargetResource + +<# + .SYNOPSIS + Sets a Route for an interface. + + .PARAMETER InterfaceAlias + Specifies the alias of a network interface. + + .PARAMETER AddressFamily + Specify the IP address family. + + .PARAMETER DestinationPrefix + Specifies a destination prefix of an IP route. + A destination prefix consists of an IP address prefix + and a prefix length, separated by a slash (/). + + .PARAMETER NextHop + Specifies the next hop for the IP route. + + .PARAMETER Ensure + Specifies whether the route should exist. + Defaults to 'Present'. + + .PARAMETER RouteMetric + Specifies an integer route metric for an IP route. + Defaults to 256. + + .PARAMETER Publish + Specifies the publish setting of an IP route. + Defaults to 'No'. + + .PARAMETER PreferredLifetime + Specifies a preferred lifetime in seconds of an IP route. +#> +function Set-TargetResource +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $InterfaceAlias, + + [Parameter(Mandatory = $true)] + [ValidateSet('IPv4', 'IPv6')] + [System.String] + $AddressFamily, + + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $DestinationPrefix, + + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $NextHop, + + [Parameter()] + [ValidateSet('Present', 'Absent')] + [System.String] + $Ensure = 'Present', + + [Parameter()] + [System.Uint16] + $RouteMetric = 256, + + [Parameter()] + [ValidateSet('No', 'Yes', 'Age')] + [System.String] + $Publish = 'No', + + [Parameter()] + [System.Double] + $PreferredLifetime + ) + + Write-Verbose -Message ( @( + "$($MyInvocation.MyCommand): " + $($script:localizedData.SettingRouteMessage) ` + -f $AddressFamily, $InterfaceAlias, $DestinationPrefix, $NextHop ` + ) -join '' ) + + # Remove any parameters that can't be splatted. + $null = $PSBoundParameters.Remove('Ensure') + + # Lookup the existing Route + $route = Get-Route @PSBoundParameters + + if ($Ensure -eq 'Present') + { + Write-Verbose -Message ( @( + "$($MyInvocation.MyCommand): " + $($script:localizedData.EnsureRouteExistsMessage) ` + -f $AddressFamily, $InterfaceAlias, $DestinationPrefix, $NextHop ` + ) -join '' ) + + if ($route) + { + # The Route exists - update it + Set-NetRoute @PSBoundParameters ` + -Confirm:$false ` + -ErrorAction Stop + + Write-Verbose -Message ( @( + "$($MyInvocation.MyCommand): " + $($script:localizedData.RouteUpdatedMessage) ` + -f $AddressFamily, $InterfaceAlias, $DestinationPrefix, $NextHop ` + ) -join '' ) + } + else + { + # The Route does not exit - create it + New-NetRoute @PSBoundParameters ` + -ErrorAction Stop + + Write-Verbose -Message ( @( + "$($MyInvocation.MyCommand): " + $($script:localizedData.RouteCreatedMessage) ` + -f $AddressFamily, $InterfaceAlias, $DestinationPrefix, $NextHop ` + ) -join '' ) + } + } + else + { + Write-Verbose -Message ( @( + "$($MyInvocation.MyCommand): " + $($script:localizedData.EnsureRouteDoesNotExistMessage) ` + -f $AddressFamily, $InterfaceAlias, $DestinationPrefix, $NextHop ` + ) -join '' ) + + if ($route) + { + <# + The Route exists - remove it + Use the parameters passed to Set-TargetResource to delete the appropriate route. + Clear the Publish and PreferredLifetime parameters so they aren't passed to the + Remove-NetRoute cmdlet. + #> + + $null = $PSBoundParameters.Remove('Publish') + $null = $PSBoundParameters.Remove('PreferredLifetime') + + Remove-NetRoute @PSBoundParameters ` + -Confirm:$false ` + -ErrorAction Stop + + Write-Verbose -Message ( @( + "$($MyInvocation.MyCommand): " + $($script:localizedData.RouteRemovedMessage) ` + -f $AddressFamily, $InterfaceAlias, $DestinationPrefix, $NextHop ` + ) -join '' ) + } # if + } # if +} # Set-TargetResource + +<# + .SYNOPSIS + Tests the state of a Route on an interface. + + .PARAMETER InterfaceAlias + Specifies the alias of a network interface. + + .PARAMETER AddressFamily + Specify the IP address family. + + .PARAMETER DestinationPrefix + Specifies a destination prefix of an IP route. + A destination prefix consists of an IP address prefix + and a prefix length, separated by a slash (/). + + .PARAMETER NextHop + Specifies the next hop for the IP route. + + .PARAMETER Ensure + Specifies whether the route should exist. + Defaults to 'Present'. + + .PARAMETER RouteMetric + Specifies an integer route metric for an IP route. + Defaults to 256. + + .PARAMETER Publish + Specifies the publish setting of an IP route. + Defaults to 'No'. + + .PARAMETER PreferredLifetime + Specifies a preferred lifetime in seconds of an IP route. +#> +function Test-TargetResource +{ + [CmdletBinding()] + [OutputType([System.Boolean])] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $InterfaceAlias, + + [Parameter(Mandatory = $true)] + [ValidateSet('IPv4', 'IPv6')] + [System.String] + $AddressFamily, + + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $DestinationPrefix, + + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $NextHop, + + [Parameter()] + [ValidateSet('Present', 'Absent')] + [System.String] + $Ensure = 'Present', + + [Parameter()] + [System.Uint16] + $RouteMetric = 256, + + [Parameter()] + [ValidateSet('No', 'Yes', 'Age')] + [System.String] + $Publish = 'No', + + [Parameter()] + [System.Double] + $PreferredLifetime + ) + + Write-Verbose -Message ( @( + "$($MyInvocation.MyCommand): " + $($script:localizedData.TestingRouteMessage) ` + -f $AddressFamily, $InterfaceAlias, $DestinationPrefix, $NextHop ` + ) -join '' ) + + # Flag to signal whether settings are correct + [System.Boolean] $desiredConfigurationMatch = $true + + # Remove any parameters that can't be splatted. + $null = $PSBoundParameters.Remove('Ensure') + + # Check the parameters + Assert-ResourceProperty @PSBoundParameters + + # Lookup the existing Route + $route = Get-Route @PSBoundParameters + + if ($Ensure -eq 'Present') + { + # The route should exist + if ($route) + { + # The route exists and does - but check the parameters + if (($PSBoundParameters.ContainsKey('RouteMetric')) ` + -and ($route.RouteMetric -ne $RouteMetric)) + { + Write-Verbose -Message ( @( + "$($MyInvocation.MyCommand): " + $($script:localizedData.RoutePropertyNeedsUpdateMessage) ` + -f $AddressFamily, $InterfaceAlias, $DestinationPrefix, $NextHop, 'RouteMetric' ` + ) -join '' ) + + $desiredConfigurationMatch = $false + } + + if (($PSBoundParameters.ContainsKey('Publish')) ` + -and ($route.Publish -ne $Publish)) + { + Write-Verbose -Message ( @( + "$($MyInvocation.MyCommand): " + $($script:localizedData.RoutePropertyNeedsUpdateMessage) ` + -f $AddressFamily, $InterfaceAlias, $DestinationPrefix, $NextHop, 'Publish' ` + ) -join '' ) + + $desiredConfigurationMatch = $false + } + + if (($PSBoundParameters.ContainsKey('PreferredLifetime')) ` + -and ($route.PreferredLifetime.TotalSeconds -ne $PreferredLifetime)) + { + Write-Verbose -Message ( @( + "$($MyInvocation.MyCommand): " + $($script:localizedData.RoutePropertyNeedsUpdateMessage) ` + -f $AddressFamily, $InterfaceAlias, $DestinationPrefix, $NextHop, 'PreferredLifetime' ` + ) -join '' ) + + $desiredConfigurationMatch = $false + } + } + else + { + # The route doesn't exist but should + Write-Verbose -Message ( @( + "$($MyInvocation.MyCommand): " + $($script:localizedData.RouteDoesNotExistButShouldMessage) ` + -f $AddressFamily, $InterfaceAlias, $DestinationPrefix, $NextHop ` + ) -join '' ) + + $desiredConfigurationMatch = $false + } + } + else + { + # The route should not exist + if ($route) + { + # The route exists but should not + Write-Verbose -Message ( @( + "$($MyInvocation.MyCommand): " + $($script:localizedData.RouteExistsButShouldNotMessage) ` + -f $AddressFamily, $InterfaceAlias, $DestinationPrefix, $NextHop ` + ) -join '' ) + + $desiredConfigurationMatch = $false + } + else + { + # The route does not exist and should not + Write-Verbose -Message ( @( + "$($MyInvocation.MyCommand): " + $($script:localizedData.RouteDoesNotExistAndShouldNotMessage) ` + -f $AddressFamily, $InterfaceAlias, $DestinationPrefix, $NextHop ` + ) -join '' ) + } + } # if + + return $desiredConfigurationMatch +} # Test-TargetResource + +<# + .SYNOPSIS + This function looks up the route using the parameters and returns + it. If the route is not found $null is returned. + + .PARAMETER InterfaceAlias + Specifies the alias of a network interface. + + .PARAMETER AddressFamily + Specify the IP address family. + + .PARAMETER DestinationPrefix + Specifies a destination prefix of an IP route. + A destination prefix consists of an IP address prefix + and a prefix length, separated by a slash (/). + + .PARAMETER NextHop + Specifies the next hop for the IP route. + + .PARAMETER Ensure + Specifies whether the route should exist. + Defaults to 'Present'. + + .PARAMETER RouteMetric + Specifies an integer route metric for an IP route. + Defaults to 256. + + .PARAMETER Publish + Specifies the publish setting of an IP route. + Defaults to 'No'. + + .PARAMETER PreferredLifetime + Specifies a preferred lifetime in seconds of an IP route. +#> +function Get-Route +{ + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $InterfaceAlias, + + [Parameter(Mandatory = $true)] + [ValidateSet('IPv4', 'IPv6')] + [System.String] + $AddressFamily, + + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $DestinationPrefix, + + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $NextHop, + + [Parameter()] + [ValidateSet('Present', 'Absent')] + [System.String] + $Ensure = 'Present', + + [Parameter()] + [System.Uint16] + $RouteMetric = 256, + + [Parameter()] + [ValidateSet('No', 'Yes', 'Age')] + [System.String] + $Publish = 'No', + + [Parameter()] + [System.Double] + $PreferredLifetime + ) + + try + { + $route = Get-NetRoute ` + -InterfaceAlias $InterfaceAlias ` + -AddressFamily $AddressFamily ` + -DestinationPrefix $DestinationPrefix ` + -NextHop $NextHop ` + -ErrorAction Stop + } + catch [Microsoft.PowerShell.Cmdletization.Cim.CimJobException] + { + $route = $null + } + catch + { + throw $_ + } + + return $route +} + +<# + .SYNOPSIS + This function validates the parameters passed. Called by Test-Resource. + Will throw an error if any parameters are invalid. + + .PARAMETER InterfaceAlias + Specifies the alias of a network interface. + + .PARAMETER AddressFamily + Specify the IP address family. + + .PARAMETER DestinationPrefix + Specifies a destination prefix of an IP route. + A destination prefix consists of an IP address prefix + and a prefix length, separated by a slash (/). + + .PARAMETER NextHop + Specifies the next hop for the IP route. + + .PARAMETER Ensure + Specifies whether the route should exist. + Defaults to 'Present'. + + .PARAMETER RouteMetric + Specifies an integer route metric for an IP route. + Defaults to 256. + + .PARAMETER Publish + Specifies the publish setting of an IP route. + Defaults to 'No'. + + .PARAMETER PreferredLifetime + Specifies a preferred lifetime in seconds of an IP route. +#> +function Assert-ResourceProperty +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $InterfaceAlias, + + [Parameter(Mandatory = $true)] + [ValidateSet('IPv4', 'IPv6')] + [System.String] + $AddressFamily, + + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $DestinationPrefix, + + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $NextHop, + + [Parameter()] + [ValidateSet('Present', 'Absent')] + [System.String] + $Ensure = 'Present', + + [Parameter()] + [System.Uint16] + $RouteMetric = 256, + + [Parameter()] + [ValidateSet('No', 'Yes', 'Age')] + [System.String] + $Publish = 'No', + + [Parameter()] + [System.Double] + $PreferredLifetime + ) + + # Validate the Adapter exists + if (-not (Get-NetAdapter | Where-Object -Property Name -EQ $InterfaceAlias )) + { + New-InvalidArgumentException ` + -Message $($($script:localizedData.InterfaceNotAvailableError) -f $InterfaceAlias) ` + -ArgumentName 'InterfaceAlias' + } + + # Validate the DestinationPrefix Parameter + $components = $DestinationPrefix -split '/' + $prefix = $components[0] + + Assert-IPAddress -Address $prefix -AddressFamily $AddressFamily + + # Validate the NextHop Parameter + Assert-IPAddress -Address $NextHop -AddressFamily $AddressFamily +} + +Export-ModuleMember -Function *-TargetResource diff --git a/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_Route/DSC_Route.schema.mof b/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_Route/DSC_Route.schema.mof new file mode 100644 index 0000000..9739a3e --- /dev/null +++ b/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_Route/DSC_Route.schema.mof @@ -0,0 +1,12 @@ +[ClassVersion("1.0.0.0"), FriendlyName("Route")] +class DSC_Route : OMI_BaseResource +{ + [Key, Description("Specifies the alias of a network interface.")] string InterfaceAlias; + [Key, Description("Specifies the IP address family."), ValueMap{"IPv4", "IPv6"},Values{"IPv4", "IPv6"}] string AddressFamily; + [Key, Description("Specifies a destination prefix of an IP route. A destination prefix consists of an IP address prefix and a prefix length, separated by a slash (/).")] String DestinationPrefix; + [Key, Description("Specifies the next hop for the IP route.")] String NextHop; + [Write, Description("Specifies whether the route should exist. Defaults to 'Present'."), ValueMap{"Present","Absent"}, Values{"Present","Absent"}] String Ensure; + [Write, Description("Specifies an integer route metric for an IP route. Defaults to 256.")] Uint16 RouteMetric; + [Write, Description("Specifies the publish setting of an IP route. Defaults to 'No'."), ValueMap{"No","Yes","Age"}, Values{"No","Yes","Age"}] String Publish; + [Write, Description("Specifies a preferred lifetime in seconds of an IP route.")] Real64 PreferredLifetime; +}; diff --git a/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_Route/README.MD b/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_Route/README.MD new file mode 100644 index 0000000..5ae942d --- /dev/null +++ b/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_Route/README.MD @@ -0,0 +1,3 @@ +# Description + +This resource is used to control static routes on an interface for a node. diff --git a/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_Route/en-US/DSC_Route.strings.psd1 b/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_Route/en-US/DSC_Route.strings.psd1 new file mode 100644 index 0000000..256a7fa --- /dev/null +++ b/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_Route/en-US/DSC_Route.strings.psd1 @@ -0,0 +1,19 @@ +# Localized resources for DSC_Route + +ConvertFrom-StringData @' + GettingRouteMessage = Getting '{0}' route on '{1}' destination '{2}' nexthop '{3}'. + RouteExistsMessage = '{0}' route on '{1}' destination '{2}' nexthop '{3}' exists. + RouteDoesNotExistMessage = '{0}' route on '{1}' destination '{2}' nexthop '{3}' does not exist. + SettingRouteMessage = Setting '{0}' route on '{1}' destination '{2}' nexthop '{3}'. + EnsureRouteExistsMessage = Ensuring '{0}' route on '{1}' destination '{2}' nexthop '{3}' exists. + EnsureRouteDoesNotExistMessage = Ensuring '{0}' route on '{1}' destination '{2}' nexthop '{3}' does not exist. + RouteCreatedMessage = '{0}' route on '{1}' destination '{2}' nexthop '{3}' has been created. + RouteUpdatedMessage = '{0}' route on '{1}' destination '{2}' nexthop '{3}' has been updated. + RouteRemovedMessage = '{0}' route on '{1}' destination '{2}' nexthop '{3}' has been removed. + TestingRouteMessage = Testing '{0}' route on '{1}' destination '{2}' nexthop '{3}'. + RoutePropertyNeedsUpdateMessage = '{4}' property on '{0}' route on '{1}' destination '{2}' nexthop '{3}' is different. Change required. + RouteDoesNotExistButShouldMessage = '{0}' route on '{1}' destination '{2}' nexthop '{3}' does not exist but should. Change required. + RouteExistsButShouldNotMessage = '{0}' route on '{1}' destination '{2}' nexthop '{3}' exists but should not. Change required. + RouteDoesNotExistAndShouldNotMessage = '{0}' route on '{1}' destination '{2}' nexthop '{3}' does not exist and should not. Change not required. + InterfaceNotAvailableError = Interface '{0}' is not available. Please select a valid interface and try again. +'@ diff --git a/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_Route/en-US/about_Route.help.txt b/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_Route/en-US/about_Route.help.txt new file mode 100644 index 0000000..3e1c96e --- /dev/null +++ b/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_Route/en-US/about_Route.help.txt @@ -0,0 +1,64 @@ +.NAME + Route + +.DESCRIPTION + This resource is used to control static routes on an interface for a node. + +.PARAMETER InterfaceAlias + Key - String + Specifies the alias of a network interface. + +.PARAMETER AddressFamily + Key - String + Allowed values: IPv4, IPv6 + Specifies the IP address family. + +.PARAMETER DestinationPrefix + Key - String + Specifies a destination prefix of an IP route. A destination prefix consists of an IP address prefix and a prefix length, separated by a slash (/). + +.PARAMETER NextHop + Key - String + Specifies the next hop for the IP route. + +.PARAMETER Ensure + Write - String + Allowed values: Present, Absent + Specifies whether the route should exist. Defaults to 'Present'. + +.PARAMETER RouteMetric + Write - UInt16 + Specifies an integer route metric for an IP route. Defaults to 256. + +.PARAMETER Publish + Write - String + Allowed values: No, Yes, Age + Specifies the publish setting of an IP route. Defaults to 'No'. + +.PARAMETER PreferredLifetime + Write - Real64 + Specifies a preferred lifetime in seconds of an IP route. + +.EXAMPLE 1 + +Add a net route to the Ethernet interface. + +Configuration Route_AddRoute_Config +{ + Import-DSCResource -ModuleName NetworkingDsc + + Node localhost + { + Route NetRoute1 + { + Ensure = 'Present' + InterfaceAlias = 'Ethernet' + AddressFamily = 'IPv4' + DestinationPrefix = '192.168.0.0/16' + NextHop = '192.168.120.0' + RouteMetric = 200 + } + } +} + + diff --git a/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_WaitForNetworkTeam/DSC_WaitForNetworkTeam.psm1 b/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_WaitForNetworkTeam/DSC_WaitForNetworkTeam.psm1 new file mode 100644 index 0000000..7e2ce40 --- /dev/null +++ b/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_WaitForNetworkTeam/DSC_WaitForNetworkTeam.psm1 @@ -0,0 +1,223 @@ +# Suppressed as per PSSA Rule Severity guidelines for unit/integration tests: +# https://github.com/PowerShell/DscResources/blob/master/PSSARuleSeverities.md +[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '')] +param () + +$modulePath = Join-Path -Path (Split-Path -Path (Split-Path -Path $PSScriptRoot -Parent) -Parent) -ChildPath 'Modules' + +# Import the Networking Common Modules +Import-Module -Name (Join-Path -Path $modulePath ` + -ChildPath (Join-Path -Path 'NetworkingDsc.Common' ` + -ChildPath 'NetworkingDsc.Common.psm1')) + +Import-Module -Name (Join-Path -Path $modulePath -ChildPath 'DscResource.Common') + +# Import Localization Strings +$script:localizedData = Get-LocalizedData -DefaultUICulture 'en-US' + +<# + .SYNOPSIS + Returns the current state of the wait for network team resource. + + .PARAMETER Name + Specifies the name of the network team to wait for. +#> +function Get-TargetResource +{ + [CmdletBinding()] + [OutputType([System.Collections.Hashtable])] + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $Name + ) + + Write-Verbose -Message ( @( + "$($MyInvocation.MyCommand): " + $($script:localizedData.GettingWaitForNetworkTeamStatusMessage -f $Name) + ) -join '' ) + + + $null = Get-NetLbfoTeamStatus -Name $Name + + $returnValue = @{ + Name = $Name + RetryIntervalSec = $null + RetryCount = $null + } + + return $returnValue +} # function Get-TargetResource + +<# + .SYNOPSIS + Sets the current state of the wait for network team resource. + + .PARAMETER Name + Specifies the name of the network team to wait for. + + .PARAMETER RetryIntervalSec + Specifies the number of seconds to wait for the network team to become available. + + .PARAMETER RetryCount + The number of times to loop the retry interval while waiting for the network team. +#> +function Set-TargetResource +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $Name, + + [Parameter()] + [System.UInt32] + $RetryIntervalSec = 10, + + [Parameter()] + [System.UInt32] + $RetryCount = 60 + ) + + Write-Verbose -Message ( @( + "$($MyInvocation.MyCommand): " + $($script:localizedData.SettingWaitForNetworkTeamStatusMessage -f $Name) + ) -join '' ) + + $lbfoTeamUp = $false + + for ($count = 0; $count -lt $RetryCount; $count++) + { + $lbfoTeamStatus = Get-NetLbfoTeamStatus -Name $Name + + if ($lbfoTeamStatus -eq 'Up') + { + Write-Verbose -Message ( @( + "$($MyInvocation.MyCommand): " + $($script:localizedData.NetworkTeamUpMessage -f $Name) + ) -join '' ) + + $lbfoTeamUp = $true + break + } + else + { + Write-Verbose -Message ( @( + "$($MyInvocation.MyCommand): " + $($script:localizedData.NetworkTeamNotUpRetryingMessage -f $Name, $RetryIntervalSec) + ) -join '' ) + + Start-Sleep -Seconds $RetryIntervalSec + } # if + } # for + + if ($lbfoTeamUp -eq $false) + { + New-InvalidOperationException ` + -Message ($script:localizedData.NetworkTeamNotUpAfterError -f $Name, $RetryCount) + } # if +} # function Set-TargetResource + +<# + .SYNOPSIS + Tests the current state of the wait for network team resource. + + .PARAMETER Name + Specifies the name of the network team to wait for. + + .PARAMETER RetryIntervalSec + Specifies the number of seconds to wait for the network team to become available. + + .PARAMETER RetryCount + The number of times to loop the retry interval while waiting for the network team. +#> +function Test-TargetResource +{ + [CmdletBinding()] + [OutputType([System.Boolean])] + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $Name, + + [Parameter()] + [System.UInt32] + $RetryIntervalSec = 10, + + [Parameter()] + [System.UInt32] + $RetryCount = 60 + ) + + Write-Verbose -Message ( @( + "$($MyInvocation.MyCommand): " + $($script:localizedData.TestingWaitForNetworkTeamStatusMessage -f $Name) + ) -join '' ) + + $lbfoTeamStatus = Get-NetLbfoTeamStatus -Name $Name + + if ($lbfoTeamStatus -eq 'Up') + { + Write-Verbose -Message ( @( + "$($MyInvocation.MyCommand): " + $($script:localizedData.NetworkTeamUpMessage -f $Name) + ) -join '' ) + + return $true + } + + Write-Verbose -Message ( @( + "$($MyInvocation.MyCommand): " + $($script:localizedData.NetworkTeamNotUpMessage -f $Name) + ) -join '' ) + + return $false +} # function Test-TargetResource + +<# + .SYNOPSIS + Returns the current status of a network team. + 'Up' indicates that the network team is acive. + 'Degraded' indicates that the network team is not yet + available. + If the network team does not exist an exception will be + thrown. + + .PARAMETER Name + Specifies the name of the network team to get the status of. +#> + +function Get-NetLbfoTeamStatus +{ + [CmdletBinding()] + [OutputType([System.String])] + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $Name + ) + + try + { + $lbfoTeam = Get-NetLbfoTeam -Name $Name + + Write-Verbose -Message ( @( + "$($MyInvocation.MyCommand): " + $($script:localizedData.NetworkTeamFoundMessage -f $Name) + ) -join '' ) + + } + catch [Microsoft.PowerShell.Cmdletization.Cim.CimJobException] + { + New-InvalidOperationException ` + -Message ($script:localizedData.NetworkTeamNotFoundMessage -f $Name) + } + + return $lbfoTeam.Status +} + +Export-ModuleMember -Function *-TargetResource diff --git a/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_WaitForNetworkTeam/DSC_WaitForNetworkTeam.schema.mof b/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_WaitForNetworkTeam/DSC_WaitForNetworkTeam.schema.mof new file mode 100644 index 0000000..3034cf9 --- /dev/null +++ b/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_WaitForNetworkTeam/DSC_WaitForNetworkTeam.schema.mof @@ -0,0 +1,8 @@ + +[ClassVersion("1.0.0.0"), FriendlyName("WaitForNetworkTeam")] +class DSC_WaitForNetworkTeam : OMI_BaseResource +{ + [Key, Description("Specifies the name of the network team to wait for.")] String Name; + [Write, Description("Specifies the number of seconds to wait for the network team to become available.")] Uint32 RetryIntervalSec; + [Write, Description("The number of times to loop the retry interval while waiting for the network team.")] Uint32 RetryCount; +}; diff --git a/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_WaitForNetworkTeam/README.md b/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_WaitForNetworkTeam/README.md new file mode 100644 index 0000000..64ef036 --- /dev/null +++ b/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_WaitForNetworkTeam/README.md @@ -0,0 +1,3 @@ +# Description + +The resource is used to wait for a network team to achieve the 'Up' status. diff --git a/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_WaitForNetworkTeam/en-US/DSC_WaitForNetworkTeam.strings.psd1 b/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_WaitForNetworkTeam/en-US/DSC_WaitForNetworkTeam.strings.psd1 new file mode 100644 index 0000000..ab57234 --- /dev/null +++ b/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_WaitForNetworkTeam/en-US/DSC_WaitForNetworkTeam.strings.psd1 @@ -0,0 +1,11 @@ +ConvertFrom-StringData @' + GettingWaitForNetworkTeamStatusMessage = Getting Network Team status '{0}'. + SettingWaitForNetworkTeamStatusMessage = Waiting for Network Team '{0}' to become 'Up'. + NetworkTeamFoundMessage = Found Network Team '{0}'. + NetworkTeamNotFoundMessage = Network Team '{0}' not found. + NetworkTeamUpMessage = Network Team '{0}' is 'Up'. + NetworkTeamNotUpMessage = Network Team '{0}' not 'Up'. + NetworkTeamNotUpRetryingMessage = Network Team '{0}' not 'Up', retrying in {1} seconds. + NetworkTeamNotUpAfterError = Network Team '{0}' not 'Up' after {1} counts. + TestingWaitForNetworkTeamStatusMessage = Testing Network Team status '{0}'. +'@ diff --git a/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_WaitForNetworkTeam/en-US/about_WaitForNetworkTeam.help.txt b/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_WaitForNetworkTeam/en-US/about_WaitForNetworkTeam.help.txt new file mode 100644 index 0000000..c88d4c4 --- /dev/null +++ b/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_WaitForNetworkTeam/en-US/about_WaitForNetworkTeam.help.txt @@ -0,0 +1,48 @@ +.NAME + WaitForNetworkTeam + +.DESCRIPTION + The resource is used to wait for a network team to achieve the 'Up' status. + +.PARAMETER Name + Key - String + Specifies the name of the network team to wait for. + +.PARAMETER RetryIntervalSec + Write - UInt32 + Specifies the number of seconds to wait for the network team to become available. + +.PARAMETER RetryCount + Write - UInt32 + The number of times to loop the retry interval while waiting for the network team. + +.EXAMPLE 1 + +Creates the switch independent Network Team 'HostTeam' using the NIC1 +and NIC2 interfaces. It sets the load balacing algorithm to 'HyperVPort'. +The config will then wait for the 'HostTeam' to achieve the 'Up' status. + +Configuration WaitForNetworkTeam_AddNetworkTeam_Config +{ + Import-DSCResource -ModuleName NetworkingDsc + + Node localhost + { + NetworkTeam AddNetworkTeam + { + Name = 'HostTeam' + TeamingMode = 'SwitchIndependent' + LoadBalancingAlgorithm = 'HyperVPort' + TeamMembers = 'NIC1', 'NIC2' + Ensure = 'Present' + } + + WaitForNetworkTeam WaitForHostTeam + { + Name = 'HostTeam' + DependsOn = '[NetworkTeam]AddNetworkTeam' + } + } +} + + diff --git a/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_WinsServerAddress/DSC_WinsServerAddress.psm1 b/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_WinsServerAddress/DSC_WinsServerAddress.psm1 new file mode 100644 index 0000000..e8598e3 --- /dev/null +++ b/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_WinsServerAddress/DSC_WinsServerAddress.psm1 @@ -0,0 +1,168 @@ +$modulePath = Join-Path -Path (Split-Path -Path (Split-Path -Path $PSScriptRoot -Parent) -Parent) -ChildPath 'Modules' + +# Import the Networking Common Modules +Import-Module -Name (Join-Path -Path $modulePath ` + -ChildPath (Join-Path -Path 'NetworkingDsc.Common' ` + -ChildPath 'NetworkingDsc.Common.psm1')) + +Import-Module -Name (Join-Path -Path $modulePath -ChildPath 'DscResource.Common') + +# Import Localization Strings +$script:localizedData = Get-LocalizedData -DefaultUICulture 'en-US' + +<# + .SYNOPSIS + Returns the current WINS Server Addresses for an interface. + + .PARAMETER InterfaceAlias + Alias of the network interface for which the WINS server address is set. + + .PARAMETER Address + The desired WINS Server address(es). Exclude to remove existing servers. +#> +function Get-TargetResource +{ + [CmdletBinding()] + [OutputType([System.Collections.Hashtable])] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [String] + $InterfaceAlias + ) + + Assert-ResourceProperty @PSBoundParameters + + Write-Verbose -Message "$($MyInvocation.MyCommand): $($script:localizedData.GettingWinsServerAddressesMessage)" + + # Get the current WINS Server Addresses based on the parameters given. + $currentAddress = [string[]]@(Get-WinsClientServerStaticAddress -InterfaceAlias $InterfaceAlias -ErrorAction Stop) + + $returnValue = @{ + InterfaceAlias = $InterfaceAlias + Address = $currentAddress + } + + return $returnValue +} + +<# + .SYNOPSIS + Sets the WINS Server Address(es) for an interface. + + .PARAMETER InterfaceAlias + Alias of the network interface for which the WINS server address is set. + + .PARAMETER Address + The desired WINS Server address(es). Exclude to remove existing servers. +#> +function Set-TargetResource +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $InterfaceAlias, + + [Parameter()] + [AllowEmptyCollection()] + [System.String[]] + $Address + ) + + Assert-ResourceProperty @PSBoundParameters + + Write-Verbose -Message "$($MyInvocation.MyCommand): $($script:localizedData.ApplyingWinsServerAddressesMessage)" + + Set-WinsClientServerStaticAddress -InterfaceAlias $InterfaceAlias -Address $Address -ErrorAction Stop + +} + +<# + .SYNOPSIS + Tests the current state of a WINS Server Address for an interface. + + .PARAMETER InterfaceAlias + Alias of the network interface for which the WINS server address is set. + + .PARAMETER Address + The desired WINS Server address(es). Exclude to remove existing servers. +#> +function Test-TargetResource +{ + [CmdletBinding()] + [OutputType([System.Boolean])] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $InterfaceAlias, + + [Parameter()] + [AllowEmptyCollection()] + [System.String[]] + $Address + ) + + Write-Verbose -Message "$($MyInvocation.MyCommand): $($script:localizedData.CheckingWinsServerAddressesMessage)" + + Assert-ResourceProperty @PSBoundParameters + + $currentState = Get-TargetResource -InterfaceAlias $InterfaceAlias + $desiredState = $PSBoundParameters + + $result = Test-DscParameterState -CurrentValues $currentState -DesiredValues $desiredState + + if ($result) + { + Write-Verbose -Message "$($MyInvocation.MyCommand): $($script:localizedData.WinsServersSetCorrectlyMessage)" + } + else + { + $message = "$($MyInvocation.MyCommand): $($script:localizedData.WinsServersNotCorrectMessage -f + ($currentState.Address -join ', '), ($desiredState.Address -join ', '))" + Write-Verbose -Message $message + } + + return $result +} + +function Assert-ResourceProperty +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $InterfaceAlias, + + [Parameter()] + [AllowEmptyCollection()] + [System.String[]] + $Address + ) + + if (-not (Get-NetAdapter | Where-Object Name -EQ $InterfaceAlias)) + { + New-InvalidArgumentException ` + -Message ($script:localizedData.InterfaceNotAvailableError -f $InterfaceAlias) ` + -ArgumentName 'InterfaceAlias' + } + + foreach ($ip in $Address) + { + if (-not ([System.Net.IPAddress]::TryParse($ip, [ref]0))) + { + New-InvalidArgumentException ` + -Message ($script:localizedData.AddressFormatError -f $ip) + -ArgumentName 'Address' + } + } +} + +Export-ModuleMember -Function *-TargetResource diff --git a/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_WinsServerAddress/DSC_WinsServerAddress.schema.mof b/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_WinsServerAddress/DSC_WinsServerAddress.schema.mof new file mode 100644 index 0000000..1d92e0d --- /dev/null +++ b/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_WinsServerAddress/DSC_WinsServerAddress.schema.mof @@ -0,0 +1,6 @@ +[ClassVersion("1.0.0"), FriendlyName("WinsServerAddress")] +class DSC_WinsServerAddress : OMI_BaseResource +{ + [Key, Description("Alias of the network interface for which the WINS server address is set.")] string InterfaceAlias; + [Write, Description("The desired WINS Server address(es). Exclude to remove all WINS servers.")] string Address[]; +}; diff --git a/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_WinsServerAddress/README.MD b/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_WinsServerAddress/README.MD new file mode 100644 index 0000000..e5bcd34 --- /dev/null +++ b/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_WinsServerAddress/README.MD @@ -0,0 +1,3 @@ +# Description + +This resource is used to control a node's WINS Server address(s) for the given network interface. diff --git a/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_WinsServerAddress/en-US/DSC_WinsServerAddress.strings.psd1 b/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_WinsServerAddress/en-US/DSC_WinsServerAddress.strings.psd1 new file mode 100644 index 0000000..933f2a0 --- /dev/null +++ b/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_WinsServerAddress/en-US/DSC_WinsServerAddress.strings.psd1 @@ -0,0 +1,11 @@ +# Localized resources for DSC_DnsServerAddress + +ConvertFrom-StringData @' + GettingWinsServerAddressesMessage = Getting the WINS server addresses. + ApplyingWinsServerAddressesMessage = Applying the WINS server addresses. + WinsServersSetCorrectlyMessage = WINS server addresses are set correctly. + WinsServersNotCorrectMessage = WINS server addresses are not correct. Expected "{0}", actual "{1}". + InterfaceNotAvailableError = Interface "{0}" is not available. Please select a valid interface and try again. + AddressFormatError = Address "{0}" is not in the correct format. Please correct the Address parameter in the configuration and try again. + CheckingWinsServerAddressesMessage = Checking the WINS server addresses. +'@ diff --git a/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_WinsServerAddress/en-US/about_WinsServerAddress.help.txt b/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_WinsServerAddress/en-US/about_WinsServerAddress.help.txt new file mode 100644 index 0000000..3d8e72a --- /dev/null +++ b/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_WinsServerAddress/en-US/about_WinsServerAddress.help.txt @@ -0,0 +1,51 @@ +.NAME + WinsServerAddress + +.DESCRIPTION + This resource is used to control a node's WINS Server address(s) for the given network interface. + +.PARAMETER InterfaceAlias + Key - String + Alias of the network interface for which the WINS server address is set. + +.PARAMETER Address + Write - StringArray + The desired WINS Server address(es). Exclude to remove all WINS servers. + +.EXAMPLE 1 + +Configure WINS Server for the Ethernet adapter. + +Configuration WinsServerAddress_Config +{ + Import-DscResource -Module NetworkingDsc + + Node localhost + { + WinsServerAddress WinsServerAddress + { + Address = '192.168.0.1' + InterfaceAlias = 'Ethernet' + } + } +} + +.EXAMPLE 2 + +Configure primary and secondary WINS Server addresses on the Ethernet adapter. + +Configuration WinsServerAddress_PrimaryAndSecondary_Config +{ + Import-DscResource -Module NetworkingDsc + + Node localhost + { + WinsServerAddress PrimaryAndSecondary + { + Address = '192.168.0.1', '192.168.0.2' + InterfaceAlias = 'Ethernet' + } + } +} + + diff --git a/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_WinsSetting/DSC_WinsSetting.psm1 b/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_WinsSetting/DSC_WinsSetting.psm1 new file mode 100644 index 0000000..2dd213f --- /dev/null +++ b/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_WinsSetting/DSC_WinsSetting.psm1 @@ -0,0 +1,186 @@ +$modulePath = Join-Path -Path (Split-Path -Path (Split-Path -Path $PSScriptRoot -Parent) -Parent) -ChildPath 'Modules' + +# Import the Networking Common Modules +Import-Module -Name (Join-Path -Path $modulePath ` + -ChildPath (Join-Path -Path 'NetworkingDsc.Common' ` + -ChildPath 'NetworkingDsc.Common.psm1')) + +Import-Module -Name (Join-Path -Path $modulePath -ChildPath 'DscResource.Common') + +# Import Localization Strings +$script:localizedData = Get-LocalizedData -DefaultUICulture 'en-US' + +<# + .SYNOPSIS + Returns the current WINS settings. + + .PARAMETER IsSingleInstance + Specifies the resource is a single instance, the value must be 'Yes'. +#> +function Get-TargetResource +{ + [CmdletBinding()] + [OutputType([System.Collections.Hashtable])] + param + ( + [Parameter(Mandatory = $true)] + [ValidateSet('Yes')] + [System.String] + $IsSingleInstance + ) + + Write-Verbose -Message ( @( + "$($MyInvocation.MyCommand): " + $($script:localizedData.GettingWinsSettingMessage) + ) -join '' ) + + # 0 equals off, 1 equals on + $enableLmHostsRegistryKey = Get-ItemProperty ` + -Path 'HKLM:\SYSTEM\CurrentControlSet\Services\NetBT\Parameters' ` + -Name EnableLMHOSTS ` + -ErrorAction SilentlyContinue + + $enableLmHosts = ($enableLmHostsRegistryKey.EnableLMHOSTS -eq 1) + + # 0 equals off, 1 equals on + $enableDnsRegistryKey = Get-ItemProperty ` + -Path 'HKLM:\SYSTEM\CurrentControlSet\Services\NetBT\Parameters' ` + -Name EnableDNS ` + -ErrorAction SilentlyContinue + + if ($enableDnsRegistryKey) + { + $enableDns = ($enableDnsRegistryKey.EnableDNS -eq 1) + } + else + { + # if the key does not exist, then set the default which is enabled. + $enableDns = $true + } + + return @{ + IsSingleInstance = 'Yes' + EnableLmHosts = $enableLmHosts + EnableDns = $enableDns + } +} # Get-TargetResource + +<# + .SYNOPSIS + Sets the current configuration for the LMHOSTS Lookup setting. + + .PARAMETER IsSingleInstance + Specifies the resource is a single instance, the value must be 'Yes'. + + .PARAMETER EnableLmHosts + Specifies if LMHOSTS lookup should be enabled for all network + adapters with TCP/IP enabled. + + .PARAMETER EnableDns + Specifies if DNS is enabled for name resolution over WINS for + all network adapters with TCP/IP enabled. +#> +function Set-TargetResource +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [ValidateSet('Yes')] + [System.String] + $IsSingleInstance, + + [Parameter()] + [System.Boolean] + $EnableLmHosts, + + [Parameter()] + [System.Boolean] + $EnableDns + ) + + Write-Verbose -Message ( @( + "$($MyInvocation.MyCommand): " + $($script:localizedData.SettingWinsSettingMessage) + ) -join '' ) + + # Get the current values of the WINS settings + $currentState = Get-TargetResource -IsSingleInstance 'Yes' + + if (-not $PSBoundParameters.ContainsKey('EnableLmHosts')) + { + $EnableLmHosts = $currentState.EnableLmHosts + } + + if (-not $PSBoundParameters.ContainsKey('EnableDns')) + { + $EnableDns = $currentState.EnableDNS + } + + $result = Invoke-CimMethod ` + -ClassName Win32_NetworkAdapterConfiguration ` + -MethodName EnableWins ` + -Arguments @{ + DNSEnabledForWINSResolution = $EnableDns + WINSEnableLMHostsLookup = $EnableLmHosts + } + + if ($result.ReturnValue -ne 0) + { + New-InvalidOperationException ` + -Message ($script:localizedData.FailedUpdatingWinsSettingError -f $result.ReturnValue) + } + + Write-Verbose -Message ( @( + "$($MyInvocation.MyCommand): " + $($script:localizedData.WinsSettingUpdatedMessage) + ) -join '' ) +} # Set-TargetResource + +<# + .SYNOPSIS + Tests the current configuration for the LMHOSTS Lookup setting. + + .PARAMETER IsSingleInstance + Specifies the resource is a single instance, the value must be 'Yes'. + + .PARAMETER EnableLmHosts + Specifies if LMHOSTS lookup should be enabled for all network + adapters with TCP/IP enabled. + + .PARAMETER EnableDns + Specifies if DNS is enabled for name resolution over WINS for + all network adapters with TCP/IP enabled. +#> +function Test-TargetResource +{ + [CmdletBinding()] + [OutputType([System.Boolean])] + param + ( + [Parameter(Mandatory = $true)] + [ValidateSet('Yes')] + [System.String] + $IsSingleInstance, + + [Parameter()] + [System.Boolean] + $EnableLmHosts, + + [Parameter()] + [System.Boolean] + $EnableDns + ) + + Write-Verbose -Message ( @( + "$($MyInvocation.MyCommand): " + $($script:localizedData.TestingWinsSettingMessage) + ) -join '' ) + + # Get the current values of the WINS settings + $currentState = Get-TargetResource -IsSingleInstance 'Yes' + + return Test-DscParameterState -CurrentValues $currentState -DesiredValues $PSBoundParameters +} # Test-TargetResource + +Export-ModuleMember -Function *-TargetResource diff --git a/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_WinsSetting/DSC_WinsSetting.schema.mof b/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_WinsSetting/DSC_WinsSetting.schema.mof new file mode 100644 index 0000000..3e6f015 --- /dev/null +++ b/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_WinsSetting/DSC_WinsSetting.schema.mof @@ -0,0 +1,7 @@ +[ClassVersion("1.0.0.0"), FriendlyName("WinsSetting")] +class DSC_WinsSetting : OMI_BaseResource +{ + [Key, Description("Specifies the resource is a single instance, the value must be 'Yes'."), ValueMap{"Yes"}, Values{"Yes"}] String IsSingleInstance; + [Write, Description("Specifies if LMHOSTS lookup should be enabled for all network adapters with TCP/IP enabled.")] Boolean EnableLmHosts; + [Write, Description("Specifies if DNS is enabled for name resolution over WINS for all network adapters with TCP/IP enabled.")] Boolean EnableDns; +}; diff --git a/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_WinsSetting/README.MD b/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_WinsSetting/README.MD new file mode 100644 index 0000000..1cd6db5 --- /dev/null +++ b/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_WinsSetting/README.MD @@ -0,0 +1,4 @@ +# Description + +This resource is used to configure the WINS settings that enable or disable +LMHOSTS lookups and enable or disable DNS for name resolution over WINS. diff --git a/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_WinsSetting/en-US/DSC_WinsSetting.strings.psd1 b/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_WinsSetting/en-US/DSC_WinsSetting.strings.psd1 new file mode 100644 index 0000000..91ac9da --- /dev/null +++ b/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_WinsSetting/en-US/DSC_WinsSetting.strings.psd1 @@ -0,0 +1,9 @@ +# Localized resources for DSC_WinsSetting + +ConvertFrom-StringData @' + GettingWinsSettingMessage = Getting WINS settings. + SettingWinsSettingMessage = Setting WINS settings. + WinsSettingUpdatedMessage = WINS settings updated. + TestingWinsSettingMessage = Testing WINS settings. + FailedUpdatingWinsSettingError = An error code of '{0}' was returned when attemting to update WINS settings. +'@ diff --git a/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_WinsSetting/en-US/about_WinsSetting.help.txt b/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_WinsSetting/en-US/about_WinsSetting.help.txt new file mode 100644 index 0000000..f697512 --- /dev/null +++ b/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/DSCResources/DSC_WinsSetting/en-US/about_WinsSetting.help.txt @@ -0,0 +1,40 @@ +.NAME + WinsSetting + +.DESCRIPTION + This resource is used to configure the WINS settings that enable or disable + LMHOSTS lookups and enable or disable DNS for name resolution over WINS. + +.PARAMETER IsSingleInstance + Key - String + Allowed values: Yes + Specifies the resource is a single instance, the value must be 'Yes'. + +.PARAMETER EnableLmHosts + Write - Boolean + Specifies if LMHOSTS lookup should be enabled for all network adapters with TCP/IP enabled. + +.PARAMETER EnableDns + Write - Boolean + Specifies if DNS is enabled for name resolution over WINS for all network adapters with TCP/IP enabled. + +.EXAMPLE 1 + +Disable LMHOSTS lookup and disable using DNS for WINS name resolution. + +Configuration WinSetting_ConfigureWinsSetting_Config +{ + Import-DscResource -Module NetworkingDsc + + Node localhost + { + WinsSetting ConfigureWinsSettings + { + IsSingleInstance = 'Yes' + EnableLMHOSTS = $false + EnableDNS = $false + } + } +} + + diff --git a/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/Modules/DscResource.Common/0.9.3/DscResource.Common.psd1 b/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/Modules/DscResource.Common/0.9.3/DscResource.Common.psd1 new file mode 100644 index 0000000..0c18ec0 --- /dev/null +++ b/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/Modules/DscResource.Common/0.9.3/DscResource.Common.psd1 @@ -0,0 +1,73 @@ +@{ + # Script module or binary module file associated with this manifest. + RootModule = 'DscResource.Common.psm1' + + # Version number of this module. + ModuleVersion = '0.9.3' + + # ID used to uniquely identify this module + GUID = '9c9daa5b-5c00-472d-a588-c96e8e498450' + + # Author of this module + Author = 'DSC Community' + + # Company or vendor of this module + CompanyName = 'DSC Community' + + # Copyright statement for this module + Copyright = 'Copyright the DSC Community contributors. All rights reserved.' + + # Description of the functionality provided by this module + Description = 'Common functions used in DSC Resources' + + # Minimum version of the PowerShell engine required by this module + PowerShellVersion = '4.0' + + # Functions to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no functions to export. + FunctionsToExport = @('Assert-BoundParameter','Assert-IPAddress','Assert-Module','Compare-ResourcePropertyState','ConvertTo-CimInstance','ConvertTo-HashTable','Get-LocalizedData','Get-TemporaryFolder','New-InvalidArgumentException','New-InvalidDataException','New-InvalidOperationException','New-InvalidResultException','New-NotImplementedException','New-ObjectNotFoundException','Remove-CommonParameter','Set-DscMachineRebootRequired','Set-PSModulePath','Test-DscParameterState','Test-IsNanoServer') + + # Cmdlets to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no cmdlets to export. + CmdletsToExport = @() + + # Variables to export from this module + VariablesToExport = @() + + # Aliases to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no aliases to export. + AliasesToExport = @() + + # Private data to pass to the module specified in RootModule/ModuleToProcess. This may also contain a PSData hashtable with additional module metadata used by PowerShell. + PrivateData = @{ + + PSData = @{ + # Tags applied to this module. These help with module discovery in online galleries. + Tags = @('DSC', 'Localization') + + # A URL to the license for this module. + LicenseUri = 'https://github.com/dsccommunity/DscResource.Common/blob/master/LICENSE' + + # A URL to the main website for this project. + ProjectUri = 'https://github.com/dsccommunity/DscResource.Common' + + # A URL to an icon representing this module. + IconUri = 'https://dsccommunity.org/images/DSC_Logo_300p.png' + + # ReleaseNotes of this module + ReleaseNotes = '## [0.9.3] - 2020-07-25 + +## Fixed + +- Correction to `Test-DscParameterState` returning false positive when parameter + with an empty array is passed in `DesriedValues` or `CurrentValues` - fixes + [issue #53](https://github.com/dsccommunity/DscResource.Common/issues/53). + +' + + Prerelease = '' + } # End of PSData hashtable + + } # End of PrivateData hashtable +} + + + + diff --git a/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/Modules/DscResource.Common/0.9.3/DscResource.Common.psm1 b/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/Modules/DscResource.Common/0.9.3/DscResource.Common.psm1 new file mode 100644 index 0000000..17d1159 --- /dev/null +++ b/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/Modules/DscResource.Common/0.9.3/DscResource.Common.psm1 @@ -0,0 +1,2179 @@ +#Region './prefix.ps1' 0 +$script:modulesFolderPath = Split-Path -Path $PSScriptRoot -Parent +#EndRegion './prefix.ps1' 1 +#Region './Private/Test-DscObjectHasProperty.ps1' 0 +<# + .SYNOPSIS + Tests if an object has a property. + + .DESCRIPTION + Tests if the specified object has the specified property and return + $true or $false. + + .PARAMETER Object + Specifies the object to test for the specified property. + + .PARAMETER PropertyName + Specifies the property name to test for. + + .EXAMPLE + Test-DscObjectHasProperty -Object 'AnyString' -PropertyName 'Length' +#> +function Test-DscObjectHasProperty +{ + [CmdletBinding()] + [OutputType([System.Boolean])] + param + ( + [Parameter(Mandatory = $true)] + [System.Object] + $Object, + + [Parameter(Mandatory = $true)] + [System.String] + $PropertyName + ) + + if ($Object.PSObject.Properties.Name -contains $PropertyName) + { + return [System.Boolean] $Object.$PropertyName + } + + return $false +} +#EndRegion './Private/Test-DscObjectHasProperty.ps1' 39 +#Region './Private/Test-DscPropertyState.ps1' 0 +<# + .SYNOPSIS + Compares the current and the desired value of a property. + + .DESCRIPTION + This function is used to compare the current and the desired value of a + property. + + .PARAMETER Values + This is set to a hash table with the current value (the CurrentValue key) + and desired value (the DesiredValue key). + + .EXAMPLE + Test-DscPropertyState -Values @{ + CurrentValue = 'John' + DesiredValue = 'Alice' + } + + .EXAMPLE + Test-DscPropertyState -Values @{ + CurrentValue = 1 + DesiredValue = 2 + } + + .NOTES + This function is used by the cmdlet Compare-ResourcePropertyState. +#> +function Test-DscPropertyState +{ + [CmdletBinding()] + [OutputType([System.Boolean])] + param + ( + [Parameter(Mandatory = $true)] + [System.Collections.Hashtable] + $Values + ) + + if ($null -eq $Values.CurrentValue -and $null -eq $Values.DesiredValue) + { + # Both values are $null so return $true + $returnValue = $true + } + elseif ($null -eq $Values.CurrentValue -or $null -eq $Values.DesiredValue) + { + # Either CurrentValue or DesiredValue are $null so return $false + $returnValue = $false + } + elseif ( + $Values.DesiredValue -is [Microsoft.Management.Infrastructure.CimInstance[]] ` + -or $Values.DesiredValue -is [System.Array] -and $Values.DesiredValue[0] -is [Microsoft.Management.Infrastructure.CimInstance] + ) + { + if (-not $Values.ContainsKey('KeyProperties')) + { + $errorMessage = $script:localizedData.KeyPropertiesMissing + + New-InvalidOperationException -Message $errorMessage + } + + $propertyState = @() + + <# + It is a collection of CIM instances, then recursively call + Test-DscPropertyState for each CIM instance in the collection. + #> + foreach ($desiredCimInstance in $Values.DesiredValue) + { + $currentCimInstance = $Values.CurrentValue + + <# + Use the CIM instance Key properties to filter out the current + values if the exist. + #> + foreach ($keyProperty in $Values.KeyProperties) + { + $currentCimInstance = $currentCimInstance | + Where-Object -Property $keyProperty -EQ -Value $desiredCimInstance.$keyProperty + } + + if ($currentCimInstance.Count -gt 1) + { + $errorMessage = $script:localizedData.TooManyCimInstances + + New-InvalidOperationException -Message $errorMessage + } + + if ($currentCimInstance) + { + $keyCimInstanceProperties = $currentCimInstance.CimInstanceProperties | + Where-Object -FilterScript { + $_.Name -in $Values.KeyProperties + } + + <# + For each key property build a string representation of the + property name and its value. + #> + $keyPropertyValues = $keyCimInstanceProperties.ForEach({'{0}="{1}"' -f $_.Name, ($_.Value -join ',')}) + + Write-Debug -Message ( + $script:localizedData.TestingCimInstance -f @( + $currentCimInstance.CimClass.CimClassName, + ($keyPropertyValues -join ';') + ) + ) + } + else + { + $keyCimInstanceProperties = $desiredCimInstance.CimInstanceProperties | + Where-Object -FilterScript { + $_.Name -in $Values.KeyProperties + } + + <# + For each key property build a string representation of the + property name and its value. + #> + $keyPropertyValues = $keyCimInstanceProperties.ForEach({'{0}="{1}"' -f $_.Name, ($_.Value -join ',')}) + + Write-Debug -Message ( + $script:localizedData.MissingCimInstance -f @( + $desiredCimInstance.CimClass.CimClassName, + ($keyPropertyValues -join ';') + ) + ) + } + + # Recursively call Test-DscPropertyState with the CimInstance to evaluate. + $propertyState += Test-DscPropertyState -Values @{ + CurrentValue = $currentCimInstance + DesiredValue = $desiredCimInstance + } + } + + # Return $false if one property is found to not be in desired state. + $returnValue = -not ($false -in $propertyState) + } + elseif ($Values.DesiredValue -is [Microsoft.Management.Infrastructure.CimInstance]) + { + $propertyState = @() + + <# + It is a CIM instance, recursively call Test-DscPropertyState for each + CIM instance property. + #> + $desiredCimInstanceProperties = $Values.DesiredValue.CimInstanceProperties | + Select-Object -Property @('Name', 'Value') + + if ($desiredCimInstanceProperties) + { + foreach ($desiredCimInstanceProperty in $desiredCimInstanceProperties) + { + <# + Recursively call Test-DscPropertyState to evaluate each property + in the CimInstance. + #> + $propertyState += Test-DscPropertyState -Values @{ + CurrentValue = $Values.CurrentValue.($desiredCimInstanceProperty.Name) + DesiredValue = $desiredCimInstanceProperty.Value + } + } + } + else + { + if ($Values.CurrentValue.CimInstanceProperties.Count -gt 0) + { + # Current value did not have any CIM properties, but desired state has. + $propertyState += $false + } + } + + # Return $false if one property is found to not be in desired state. + $returnValue = -not ($false -in $propertyState) + } + elseif ($Values.DesiredValue -is [System.Array] -or $Values.CurrentValue -is [System.Array]) + { + $compareObjectParameters = @{ + ReferenceObject = $Values.CurrentValue + DifferenceObject = $Values.DesiredValue + } + + $arrayCompare = Compare-Object @compareObjectParameters + + if ($null -ne $arrayCompare) + { + Write-Debug -Message $script:localizedData.ArrayDoesNotMatch + + $arrayCompare | + ForEach-Object -Process { + if ($_.SideIndicator -eq '=>') + { + Write-Debug -Message ( + $script:localizedData.ArrayValueIsAbsent -f $_.InputObject + ) + } + else + { + Write-Debug -Message ( + $script:localizedData.ArrayValueIsPresent -f $_.InputObject + ) + } + } + + $returnValue = $false + } + else + { + $returnValue = $true + } + } + elseif ($Values.CurrentValue -ne $Values.DesiredValue) + { + $desiredType = $Values.DesiredValue.GetType() + + $returnValue = $false + + $supportedTypes = @( + 'String' + 'Int32' + 'UInt32' + 'Int16' + 'UInt16' + 'Single' + 'Boolean' + ) + + if ($desiredType.Name -notin $supportedTypes) + { + Write-Warning -Message ($script:localizedData.UnableToCompareType -f $desiredType.Name) + } + else + { + Write-Debug -Message ( + $script:localizedData.PropertyValueOfTypeDoesNotMatch ` + -f $desiredType.Name, $Values.CurrentValue, $Values.DesiredValue + ) + } + } + else + { + $returnValue = $true + } + + return $returnValue +} +#EndRegion './Private/Test-DscPropertyState.ps1' 246 +#Region './Public/Assert-BoundParameter.ps1' 0 +<# + .SYNOPSIS + Throws an error if there is a bound parameter that exists in both the + mutually exclusive lists. + + .DESCRIPTION + Throws an error if there is a bound parameter that exists in both the + mutually exclusive lists. + + .PARAMETER BoundParameterList + The parameters that should be evaluated against the mutually exclusive + lists MutuallyExclusiveList1 and MutuallyExclusiveList2. This parameter is + normally set to the $PSBoundParameters variable. + + .PARAMETER MutuallyExclusiveList1 + An array of parameter names that are not allowed to be bound at the + same time as those in MutuallyExclusiveList2. + + .PARAMETER MutuallyExclusiveList2 + An array of parameter names that are not allowed to be bound at the + same time as those in MutuallyExclusiveList1. + + .EXAMPLE + $assertBoundParameterParameters = @{ + BoundParameterList = $PSBoundParameters + MutuallyExclusiveList1 = @( + 'Parameter1' + ) + MutuallyExclusiveList2 = @( + 'Parameter2' + ) + } + + Assert-BoundParameter @assertBoundParameterParameters + + This example throws an exception if `$PSBoundParameters` contains both + the parameters `Parameter1` and `Parameter2`. +#> +function Assert-BoundParameter +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [AllowEmptyCollection()] + [System.Collections.Hashtable] + $BoundParameterList, + + [Parameter(Mandatory = $true)] + [System.String[]] + $MutuallyExclusiveList1, + + [Parameter(Mandatory = $true)] + [System.String[]] + $MutuallyExclusiveList2 + ) + + $itemFoundFromList1 = $BoundParameterList.Keys.Where({ $_ -in $MutuallyExclusiveList1 }) + $itemFoundFromList2 = $BoundParameterList.Keys.Where({ $_ -in $MutuallyExclusiveList2 }) + + if ($itemFoundFromList1.Count -gt 0 -and $itemFoundFromList2.Count -gt 0) + { + $errorMessage = ` + $script:localizedData.ParameterUsageWrong ` + -f ($MutuallyExclusiveList1 -join "','"), ($MutuallyExclusiveList2 -join "','") + + New-InvalidArgumentException -ArgumentName 'Parameters' -Message $errorMessage + } +} +#EndRegion './Public/Assert-BoundParameter.ps1' 69 +#Region './Public/Assert-IPAddress.ps1' 0 +<# + .SYNOPSIS + Asserts that the specified IP address is valid. + + .DESCRIPTION + Checks the IP address so that it is valid and do not conflict with address + family. If any problems are detected an exception will be thrown. + + .PARAMETER AddressFamily + IP address family that the supplied Address should be in. Valid values are + 'IPv4' or 'IPv6'. + + .PARAMETER Address + Specifies an IPv4 or IPv6 address. + + .EXAMPLE + Assert-IPAddress -Address '127.0.0.1' + + This will assert that the supplied address is a valid IPv4 address. + If it is not an exception will be thrown. + + .EXAMPLE + Assert-IPAddress -Address 'fe80:ab04:30F5:002b::1' + + This will assert that the supplied address is a valid IPv6 address. + If it is not an exception will be thrown. + + .EXAMPLE + Assert-IPAddress -Address 'fe80:ab04:30F5:002b::1' -AddressFamily 'IPv6' + + This will assert that address is valid and that it matches the + supplied address family. If the supplied address family does not match + the address an exception will be thrown. +#> +function Assert-IPAddress +{ + [CmdletBinding()] + param + ( + [Parameter()] + [ValidateSet('IPv4', 'IPv6')] + [System.String] + $AddressFamily, + + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $Address + ) + + [System.Net.IPAddress] $ipAddress = $null + + if (-not ([System.Net.IPAddress]::TryParse($Address, [ref] $ipAddress))) + { + New-InvalidArgumentException ` + -Message ($script:localizedData.AddressFormatError -f $Address) ` + -ArgumentName 'Address' + } + + if ($AddressFamily) + { + switch ($AddressFamily) + { + 'IPv4' + { + if ($ipAddress.AddressFamily -ne [System.Net.Sockets.AddressFamily]::InterNetwork.ToString()) + { + New-InvalidArgumentException ` + -Message ($script:localizedData.AddressIPv6MismatchError -f $Address, $AddressFamily) ` + -ArgumentName 'AddressFamily' + } + } + + 'IPv6' + { + if ($ipAddress.AddressFamily -ne [System.Net.Sockets.AddressFamily]::InterNetworkV6.ToString()) + { + New-InvalidArgumentException ` + -Message ($script:localizedData.AddressIPv4MismatchError -f $Address, $AddressFamily) ` + -ArgumentName 'AddressFamily' + } + } + } + } +} +#EndRegion './Public/Assert-IPAddress.ps1' 85 +#Region './Public/Assert-Module.ps1' 0 +<# + .SYNOPSIS + Assert if the specific module is available to be imported. + + .DESCRIPTION + Assert if the specific module is available to be imported. + + .PARAMETER ModuleName + Specifies the name of the module to assert. + + .PARAMETER ImportModule + Specfiies to import the module if it is asserted. + + .EXAMPLE + Assert-Module -ModuleName 'DhcpServer' + + This asserts that the module DhcpServer is available on the system. +#> +function Assert-Module +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $ModuleName, + + [Parameter()] + [System.Management.Automation.SwitchParameter] + $ImportModule + ) + + if (-not (Get-Module -Name $ModuleName -ListAvailable)) + { + $errorMessage = $script:localizedData.ModuleNotFound -f $ModuleName + New-ObjectNotFoundException -Message $errorMessage + } + + if ($ImportModule) + { + Import-Module -Name $ModuleName + } +} +#EndRegion './Public/Assert-Module.ps1' 43 +#Region './Public/Compare-ResourcePropertyState.ps1' 0 +<# + .SYNOPSIS + Compare current and desired property values for any DSC resource. + + .DESCRIPTION + This function is used to compare current and desired property values for any + DSC resource, and return a hashtable with the metadata from the comparison. + + .PARAMETER CurrentValues + The current values that should be compared to to desired values. Normally + the values returned from Get-TargetResource. + + .PARAMETER DesiredValues + The values set in the configuration and is provided in the call to the + functions *-TargetResource, and that will be compared against current + values. Normally set to $PSBoundParameters. + + .PARAMETER Properties + An array of property names, from the keys provided in DesiredValues, that + will be compared. If this parameter is left out, all the keys in the + DesiredValues will be compared. + + .PARAMETER IgnoreProperties + An array of property names, from the keys provided in DesiredValues, that + will be ignored in the comparison. If this parameter is left out, all the + keys in the DesiredValues will be compared. + + .PARAMETER CimInstanceKeyProperties + A hashtable containing a key for each property that contain a collection + of CimInstances and the value is an array of strings of the CimInstance + key properties. + @{ + Permission = @('State') + } + + .EXAMPLE + $compareTargetResourceStateParameters = @{ + CurrentValues = (Get-TargetResource $PSBoundParameters) + DesiredValues = $PSBoundParameters + } + + $propertyState = Compare-ResourcePropertyState @compareTargetResourceStateParameters + + This examples call Compare-ResourcePropertyState with the current state + and the desired state and returns a hashtable array of all the properties + that was evaluated based on the properties pass in the parameter DesiredValues. +#> +function Compare-ResourcePropertyState +{ + [CmdletBinding()] + [OutputType([System.Collections.Hashtable[]])] + param + ( + [Parameter(Mandatory = $true)] + [System.Collections.Hashtable] + $CurrentValues, + + [Parameter(Mandatory = $true)] + [System.Collections.Hashtable] + $DesiredValues, + + [Parameter()] + [ValidateNotNullOrEmpty()] + [System.String[]] + $Properties, + + [Parameter()] + [ValidateNotNullOrEmpty()] + [System.String[]] + $IgnoreProperties, + + [Parameter()] + [ValidateNotNull()] + [System.Collections.Hashtable] + $CimInstanceKeyProperties = @{} + ) + + if ($PSBoundParameters.ContainsKey('Properties')) + { + # Filter out the parameters (keys) not specified in Properties + $desiredValuesToRemove = $DesiredValues.Keys | + Where-Object -FilterScript { + $_ -notin $Properties + } + + $desiredValuesToRemove | + ForEach-Object -Process { + $DesiredValues.Remove($_) + } + } + else + { + <# + Remove any common parameters that might be part of DesiredValues, + if it $PSBoundParameters was used to pass the desired values. + #> + $commonParametersToRemove = $DesiredValues.Keys | + Where-Object -FilterScript { + $_ -in [System.Management.Automation.PSCmdlet]::CommonParameters ` + -or $_ -in [System.Management.Automation.PSCmdlet]::OptionalCommonParameters + } + + $commonParametersToRemove | + ForEach-Object -Process { + $DesiredValues.Remove($_) + } + } + + # Remove any properties that should be ignored. + if ($PSBoundParameters.ContainsKey('IgnoreProperties')) + { + $IgnoreProperties | + ForEach-Object -Process { + if ($DesiredValues.ContainsKey($_)) + { + $DesiredValues.Remove($_) + } + } + } + + $compareTargetResourceStateReturnValue = @() + + foreach ($parameterName in $DesiredValues.Keys) + { + Write-Debug -Message ($script:localizedData.EvaluatePropertyState -f $parameterName) + + $parameterState = @{ + ParameterName = $parameterName + Expected = $DesiredValues.$parameterName + Actual = $CurrentValues.$parameterName + } + + # Check if the parameter is in compliance. + $isPropertyInDesiredState = Test-DscPropertyState -Values @{ + CurrentValue = $CurrentValues.$parameterName + DesiredValue = $DesiredValues.$parameterName + KeyProperties = $CimInstanceKeyProperties.$parameterName + } + + if ($isPropertyInDesiredState) + { + Write-Verbose -Message ($script:localizedData.PropertyInDesiredState -f $parameterName) + + $parameterState['InDesiredState'] = $true + } + else + { + Write-Verbose -Message ($script:localizedData.PropertyNotInDesiredState -f $parameterName) + + $parameterState['InDesiredState'] = $false + } + + $compareTargetResourceStateReturnValue += $parameterState + } + + return $compareTargetResourceStateReturnValue +} +#EndRegion './Public/Compare-ResourcePropertyState.ps1' 157 +#Region './Public/ConvertTo-CimInstance.ps1' 0 +<# + .SYNOPSIS + Converts a hashtable into a CimInstance array. + + .DESCRIPTION + This function is used to convert a hashtable into MSFT_KeyValuePair objects. + These are stored as an CimInstance array. DSC cannot handle hashtables but + CimInstances arrays storing MSFT_KeyValuePair. + + .PARAMETER Hashtable + A hashtable with the values to convert. + + .OUTPUTS + An object array with CimInstance objects. + + .EXAMPLE + ConvertTo-CimInstance -Hashtable @{ + String = 'a string' + Bool = $true + Int = 99 + Array = 'a, b, c' + } + + This example returns an CimInstance with the provided hashtable values. +#> +function ConvertTo-CimInstance +{ + [CmdletBinding()] + [OutputType([System.Object[]])] + param + ( + [Parameter(Mandatory = $true, ValueFromPipeline = $true, ParameterSetName = 'Hashtable')] + [System.Collections.Hashtable] + $Hashtable + ) + + process + { + foreach ($item in $Hashtable.GetEnumerator()) + { + New-CimInstance -ClassName 'MSFT_KeyValuePair' -Namespace 'root/microsoft/Windows/DesiredStateConfiguration' -Property @{ + Key = $item.Key + Value = if ($item.Value -is [array]) + { + $item.Value -join ',' + } + else + { + $item.Value + } + } -ClientOnly + } + } +} +#EndRegion './Public/ConvertTo-CimInstance.ps1' 54 +#Region './Public/ConvertTo-HashTable.ps1' 0 +<# + .SYNOPSIS + Converts CimInstances into a hashtable. + + .DESCRIPTION + This function is used to convert a CimInstance array containing + MSFT_KeyValuePair objects into a hashtable. + + .PARAMETER CimInstance + An array of CimInstances or a single CimInstance object to convert. + + .OUTPUTS + Hashtable + + .EXAMPLE + $newInstanceParameters = @{ + ClassName = 'MSFT_KeyValuePair' + Namespace = 'root/microsoft/Windows/DesiredStateConfiguration' + ClientOnly = $true + } + + $cimInstance = [Microsoft.Management.Infrastructure.CimInstance[]] ( + (New-CimInstance @newInstanceParameters -Property @{ + Key = 'FirstName' + Value = 'John' + }), + + (New-CimInstance @newInstanceParameters -Property @{ + Key = 'LastName' + Value = 'Smith' + }) + ) + + ConvertTo-HashTable -CimInstance $cimInstance + + This creates a array om CimInstances of the class name MSFT_KeyValuePair + and passes it to ConvertTo-HashTable which returns a hashtable. +#> +function ConvertTo-HashTable +{ + [CmdletBinding()] + [OutputType([System.Collections.Hashtable])] + param + ( + [Parameter(Mandatory = $true, ValueFromPipeline = $true, ParameterSetName = 'CimInstance')] + [AllowEmptyCollection()] + [Microsoft.Management.Infrastructure.CimInstance[]] + $CimInstance + ) + + begin + { + $result = @{ } + } + + process + { + foreach ($ci in $CimInstance) + { + $result.Add($ci.Key, $ci.Value) + } + } + + end + { + $result + } +} +#EndRegion './Public/ConvertTo-HashTable.ps1' 68 +#Region './Public/Get-LocalizedData.ps1' 0 +<# + .SYNOPSIS + Gets language-specific data into scripts and functions based on the UI culture + that is selected for the operating system. + Similar to Import-LocalizedData, with extra parameter 'DefaultUICulture'. + + .DESCRIPTION + The Get-LocalizedData cmdlet dynamically retrieves strings from a subdirectory + whose name matches the UI language set for the current user of the operating system. + It is designed to enable scripts to display user messages in the UI language selected + by the current user. + + Get-LocalizedData imports data from .psd1 files in language-specific subdirectories + of the script directory and saves them in a local variable that is specified in the + command. The cmdlet selects the subdirectory and file based on the value of the + $PSUICulture automatic variable. When you use the local variable in the script to + display a user message, the message appears in the user's UI language. + + You can use the parameters of G-LocalizedData to specify an alternate UI culture, + path, and file name, to add supported commands, and to suppress the error message that + appears if the .psd1 files are not found. + + The G-LocalizedData cmdlet supports the script internationalization + initiative that was introduced in Windows PowerShell 2.0. This initiative + aims to better serve users worldwide by making it easy for scripts to display + user messages in the UI language of the current user. For more information + about this and about the format of the .psd1 files, see about_Script_Internationalization. + + .PARAMETER BindingVariable + Specifies the variable into which the text strings are imported. Enter a variable + name without a dollar sign ($). + + In Windows PowerShell 2.0, this parameter is required. In Windows PowerShell 3.0, + this parameter is optional. If you omit this parameter, Import-LocalizedData + returns a hash table of the text strings. The hash table is passed down the pipeline + or displayed at the command line. + + When using Import-LocalizedData to replace default text strings specified in the + DATA section of a script, assign the DATA section to a variable and enter the name + of the DATA section variable in the value of the BindingVariable parameter. Then, + when Import-LocalizedData saves the imported content in the BindingVariable, the + imported data will replace the default text strings. If you are not specifying + default text strings, you can select any variable name. + + .PARAMETER UICulture + Specifies an alternate UI culture. The default is the value of the $PsUICulture + automatic variable. Enter a UI culture in - format, such as + en-US, de-DE, or ar-SA. + + The value of the UICulture parameter determines the language-specific subdirectory + (within the base directory) from which Import-LocalizedData gets the .psd1 file + for the script. + + The cmdlet searches for a subdirectory with the same name as the value of the + UICulture parameter or the $PsUICulture automatic variable, such as de-DE or + ar-SA. If it cannot find the directory, or the directory does not contain a .psd1 + file for the script, it searches for a subdirectory with the name of the language + code, such as de or ar. If it cannot find the subdirectory or .psd1 file, the + command fails and the data is displayed in the default language specified in the + script. + + .PARAMETER BaseDirectory + Specifies the base directory where the .psd1 files are located. The default is + the directory where the script is located. Import-LocalizedData searches for + the .psd1 file for the script in a language-specific subdirectory of the base + directory. + + .PARAMETER FileName + Specifies the name of the data file (.psd1) to be imported. Enter a file name. + You can specify a file name that does not include its .psd1 file name extension, + or you can specify the file name including the .psd1 file name extension. + + The FileName parameter is required when Import-LocalizedData is not used in a + script. Otherwise, the parameter is optional and the default value is the base + name of the script. You can use this parameter to direct Import-LocalizedData + to search for a different .psd1 file. + + For example, if the FileName is omitted and the script name is FindFiles.ps1, + Import-LocalizedData searches for the FindFiles.psd1 data file. + + .PARAMETER SupportedCommand + Specifies cmdlets and functions that generate only data. + + Use this parameter to include cmdlets and functions that you have written or + tested. For more information, see about_Script_Internationalization. + + .PARAMETER DefaultUICulture + Specifies which UICulture to default to if current UI culture or its parents + culture don't have matching data file. + + For example, if you have a data file in 'en-US' but not in 'en' or 'en-GB' and + your current culture is 'en-GB', you can default back to 'en-US'. + + .NOTES + Before using Import-LocalizedData, localize your user messages. Format the messages + for each locale (UI culture) in a hash table of key/value pairs, and save the + hash table in a file with the same name as the script and a .psd1 file name extension. + Create a directory under the script directory for each supported UI culture, and + then save the .psd1 file for each UI culture in the directory with the UI + culture name. + + For example, localize your user messages for the de-DE locale and format them in + a hash table. Save the hash table in a .psd1 file. Then create a de-DE + subdirectory under the script directory, and save the de-DE .psd1 + file in the de-DE subdirectory. Repeat this method for each locale that you support. + + Import-LocalizedData performs a structured search for the localized user + messages for a script. + + Import-LocalizedData begins the search in the directory where the script file + is located (or the value of the BaseDirectory parameter). It then searches within + the base directory for a subdirectory with the same name as the value of the + $PsUICulture variable (or the value of the UICulture parameter), such as de-DE or + ar-SA. Then, it searches in that subdirectory for a .psd1 file with the same name + as the script (or the value of the FileName parameter). + + If Import-LocalizedData cannot find a subdirectory with the name of the UI culture, + or the subdirectory does not contain a .psd1 file for the script, it searches for + a .psd1 file for the script in a subdirectory with the name of the language code, + such as de or ar. If it cannot find the subdirectory or .psd1 file, the command + fails, the data is displayed in the default language in the script, and an error + message is displayed explaining that the data could not be imported. To suppress + the message and fail gracefully, use the ErrorAction common parameter with a value + of SilentlyContinue. + + If Import-LocalizedData finds the subdirectory and the .psd1 file, it imports the + hash table of user messages into the value of the BindingVariable parameter in the + command. Then, when you display a message from the hash table in the variable, the + localized message is displayed. + + For more information, see about_Script_Internationalization. + + .EXAMPLE + $script:localizedData = Get-LocalizedData -DefaultUICulture 'en-US' + + This is an example that can be used in DSC resources to import the + localized strings and if the current UI culture localized folder does + not exist the UI culture 'en-US' is returned. +#> +function Get-LocalizedData +{ + [CmdletBinding(DefaultParameterSetName = 'DefaultUICulture')] + param + ( + [Parameter(Position = 0)] + [Alias('Variable')] + [ValidateNotNullOrEmpty()] + [System.String] + $BindingVariable, + + [Parameter(Position = 1, ParameterSetName = 'TargetedUICulture')] + [System.String] + $UICulture, + + [Parameter()] + [System.String] + $BaseDirectory, + + [Parameter()] + [System.String] + $FileName, + + [Parameter()] + [System.String[]] + $SupportedCommand, + + [Parameter(Position = 1, ParameterSetName = 'DefaultUICulture')] + [System.String] + $DefaultUICulture = 'en-US' + ) + + begin + { + <# + Because Proxy Command changes the Invocation origin, we need to be explicit + when handing the pipeline back to original command. + #> + if (!$PSBoundParameters.ContainsKey('FileName')) + { + if ($myInvocation.ScriptName) + { + $file = [System.IO.FileInfo] $myInvocation.ScriptName + } + else + { + $file = [System.IO.FileInfo] $myInvocation.MyCommand.Module.Path + } + + $FileName = $file.BaseName + + $PSBoundParameters.Add('FileName', $file.Name) + } + + if ($PSBoundParameters.ContainsKey('BaseDirectory')) + { + $callingScriptRoot = $BaseDirectory + } + else + { + $callingScriptRoot = $MyInvocation.PSScriptRoot + + $PSBoundParameters.Add('BaseDirectory', $callingScriptRoot) + } + + if ($PSBoundParameters.ContainsKey('DefaultUICulture') -and !$PSBoundParameters.ContainsKey('UICulture')) + { + <# + We don't want the resolution to eventually return the ModuleManifest + so we run the same GetFilePath() logic than here: + https://github.com/PowerShell/PowerShell/blob/master/src/Microsoft.PowerShell.Commands.Utility/commands/utility/Import-LocalizedData.cs#L302-L333 + and if we see it will return the wrong thing, set the UICulture to DefaultUI culture, and return the logic to Import-LocalizedData + #> + $currentCulture = Get-UICulture + + $evaluateDefaultCulture = $true + + <# + If the LCID is 127 then use default UI culture instead. + + See more information in issue https://github.com/dsccommunity/DscResource.Common/issues/11. + #> + if ($currentCulture.LCID -eq 127) + { + $currentCulture = New-Object -TypeName 'System.Globalization.CultureInfo' -ArgumentList @($DefaultUICulture) + $PSBoundParameters['UICulture'] = $DefaultUICulture + + $evaluateDefaultCulture = $false + } + + $languageFile = $null + + $localizedFileNames = @( + $FileName + '.psd1' + $FileName + '.strings.psd1' + ) + + while ($null -ne $currentCulture -and $currentCulture.Name -and -not $languageFile) + { + foreach ($fullFileName in $localizedFileNames) + { + $filePath = [System.IO.Path]::Combine($callingScriptRoot, $CurrentCulture.Name, $fullFileName) + + if (Test-Path -Path $filePath) + { + Write-Debug -Message "Found $filePath" + + $languageFile = $filePath + + # Set the filename to the file we found. + $PSBoundParameters['FileName'] = $fullFileName + + # Exit loop if we find the first filename. + break + } + else + { + Write-Debug -Message "File $filePath not found" + } + } + + if (-not $languageFile) + { + <# + Evaluate the parent culture if there is one. + + If the parent culture is LCID 127 then move to the default culture. + See more information in issue https://github.com/dsccommunity/DscResource.Common/issues/11. + #> + if ($currentCulture.Parent -and $currentCulture.Parent.LCID -ne 127) + { + $currentCulture = $currentCulture.Parent + } + else + { + if ($evaluateDefaultCulture) + { + $evaluateDefaultCulture = $false + + <# + Could not find localized strings file for the the operating + system UI culture. Evaluating the default UI culture (which + defaults to 'en-US' if not specifically set). + #> + $currentCulture = New-Object -TypeName 'System.Globalization.CultureInfo' -ArgumentList @($DefaultUICulture) + $PSBoundParameters['UICulture'] = $DefaultUICulture + } + else + { + <# + Already evaluated everything we could, exit and let + Import-LocalizedData throw an exception. + #> + break + } + } + } + } + + <# + Removes the parameter DefaultUICulture so that isn't used when + calling Import-LocalizedData. + #> + $null = $PSBoundParameters.Remove('DefaultUICulture') + } + + try + { + $outBuffer = $null + + if ($PSBoundParameters.TryGetValue('OutBuffer', [ref] $outBuffer)) + { + $PSBoundParameters['OutBuffer'] = 1 + } + + $wrappedCmd = $ExecutionContext.InvokeCommand.GetCommand('Microsoft.PowerShell.Utility\Import-LocalizedData', [System.Management.Automation.CommandTypes]::Cmdlet) + $scriptCmd = { & $wrappedCmd @PSBoundParameters } + + $steppablePipeline = $scriptCmd.GetSteppablePipeline($myInvocation.CommandOrigin) + $steppablePipeline.Begin($PSCmdlet) + } + catch + { + throw + } + } + + process + { + try + { + $steppablePipeline.Process($_) + } + catch + { + throw + } + } + + end + { + if ($BindingVariable -and ($valueToBind = Get-Variable -Name $BindingVariable -ValueOnly -ErrorAction 'Ignore')) + { + # Bringing the variable to the parent scope + Set-Variable -Scope 1 -Name $BindingVariable -Force -ErrorAction 'SilentlyContinue' -Value $valueToBind + } + + try + { + $steppablePipeline.End() + } + catch + { + throw + } + } +} +#EndRegion './Public/Get-LocalizedData.ps1' 356 +#Region './Public/Get-TemporaryFolder.ps1' 0 +<# + .SYNOPSIS + Returns the path of the current user's temporary folder. + + .DESCRIPTION + Returns the path of the current user's temporary folder. + + .NOTES + This is the same as doing the following + - Windows: $env:TEMP + - macOS: $env:TMPDIR + - Linux: /tmp/ + + .EXAMPLE + Get-TemporaryFolder + + Returns the current user temporary folder on the current operating system. +#> +function Get-TemporaryFolder +{ + [CmdletBinding()] + [OutputType([System.String])] + param () + + return [IO.Path]::GetTempPath() +} +#EndRegion './Public/Get-TemporaryFolder.ps1' 26 +#Region './Public/New-InvalidArgumentException.ps1' 0 +<# + .SYNOPSIS + Creates and throws an invalid argument exception. + + .DESCRIPTION + Creates and throws an invalid argument exception. + + .PARAMETER Message + The message explaining why this error is being thrown. + + .PARAMETER ArgumentName + The name of the invalid argument that is causing this error to be thrown. + + .EXAMPLE + $errorMessage = $script:localizedData.ActionCannotBeUsedInThisContextMessage ` + -f $Action, $Parameter + + New-InvalidArgumentException -ArgumentName 'Action' -Message $errorMessage +#> +function New-InvalidArgumentException +{ + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '')] + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $Message, + + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $ArgumentName + ) + + $argumentException = New-Object -TypeName 'ArgumentException' ` + -ArgumentList @($Message, $ArgumentName) + + $newObjectParameters = @{ + TypeName = 'System.Management.Automation.ErrorRecord' + ArgumentList = @($argumentException, $ArgumentName, 'InvalidArgument', $null) + } + + $errorRecord = New-Object @newObjectParameters + + throw $errorRecord +} +#EndRegion './Public/New-InvalidArgumentException.ps1' 48 +#Region './Public/New-InvalidDataException.ps1' 0 +<# + .SYNOPSIS + Creates and throws an invalid data exception. + + .DESCRIPTION + Creates and throws an invalid data exception. + + .PARAMETER ErrorId + The error Id to assign to the exception. + + .PARAMETER ErrorMessage + The error message to assign to the exception. + + .EXAMPLE + if ( -not $resultOfEvaluation ) + { + $errorMessage = $script:localizedData.InvalidData -f $Action + + New-InvalidDataException -ErrorId 'InvalidDataError' -ErrorMessage $errorMessage + } +#> +function New-InvalidDataException +{ + [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '')] + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $ErrorId, + + [Parameter(Mandatory = $true)] + [System.String] + $ErrorMessage + ) + + $errorCategory = [System.Management.Automation.ErrorCategory]::InvalidData + $exception = New-Object ` + -TypeName System.InvalidOperationException ` + -ArgumentList $ErrorMessage + $errorRecord = New-Object ` + -TypeName System.Management.Automation.ErrorRecord ` + -ArgumentList $exception, $ErrorId, $errorCategory, $null + + throw $errorRecord +} +#EndRegion './Public/New-InvalidDataException.ps1' 46 +#Region './Public/New-InvalidOperationException.ps1' 0 +<# + .SYNOPSIS + Creates and throws an invalid operation exception. + + .DESCRIPTION + Creates and throws an invalid operation exception. + + .PARAMETER Message + The message explaining why this error is being thrown. + + .PARAMETER ErrorRecord + The error record containing the exception that is causing this terminating error. + + .EXAMPLE + try + { + Start-Process @startProcessArguments + } + catch + { + $errorMessage = $script:localizedData.InstallationFailedMessage -f $Path, $processId + New-InvalidOperationException -Message $errorMessage -ErrorRecord $_ + } +#> +function New-InvalidOperationException +{ + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '')] + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $Message, + + [Parameter()] + [ValidateNotNull()] + [System.Management.Automation.ErrorRecord] + $ErrorRecord + ) + + if ($null -eq $ErrorRecord) + { + $invalidOperationException = New-Object -TypeName 'InvalidOperationException' ` + -ArgumentList @($Message) + } + else + { + $invalidOperationException = New-Object -TypeName 'InvalidOperationException' ` + -ArgumentList @($Message, $ErrorRecord.Exception) + } + + $newObjectParameters = @{ + TypeName = 'System.Management.Automation.ErrorRecord' + ArgumentList = @( + $invalidOperationException.ToString(), + 'MachineStateIncorrect', + 'InvalidOperation', + $null + ) + } + + $errorRecordToThrow = New-Object @newObjectParameters + + throw $errorRecordToThrow +} +#EndRegion './Public/New-InvalidOperationException.ps1' 66 +#Region './Public/New-InvalidResultException.ps1' 0 +<# + .SYNOPSIS + Creates and throws an invalid result exception. + + .DESCRIPTION + Creates and throws an invalid result exception. + + .PARAMETER Message + The message explaining why this error is being thrown. + + .PARAMETER ErrorRecord + The error record containing the exception that is causing this terminating error. + + .EXAMPLE + try + { + $numberOfObjects = Get-ChildItem -Path $path + if ($numberOfObjects -eq 0) + { + throw 'To few files.' + } + } + catch + { + $errorMessage = $script:localizedData.TooFewFilesMessage -f $path + New-InvalidResultException -Message $errorMessage -ErrorRecord $_ + } +#> +function New-InvalidResultException +{ + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '')] + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $Message, + + [Parameter()] + [ValidateNotNull()] + [System.Management.Automation.ErrorRecord] + $ErrorRecord + ) + + if ($null -eq $ErrorRecord) + { + $exception = New-Object -TypeName 'System.Exception' ` + -ArgumentList @($Message) + } + else + { + $exception = New-Object -TypeName 'System.Exception' ` + -ArgumentList @($Message, $ErrorRecord.Exception) + } + + $newObjectParameters = @{ + TypeName = 'System.Management.Automation.ErrorRecord' + ArgumentList = @( + $exception.ToString(), + 'MachineStateIncorrect', + 'InvalidResult', + $null + ) + } + + $errorRecordToThrow = New-Object @newObjectParameters + + throw $errorRecordToThrow +} +#EndRegion './Public/New-InvalidResultException.ps1' 70 +#Region './Public/New-NotImplementedException.ps1' 0 +<# + .SYNOPSIS + Creates and throws an not implemented exception. + + .DESCRIPTION + Creates and throws an not implemented exception. + + .PARAMETER Message + The message explaining why this error is being thrown. + + .PARAMETER ErrorRecord + The error record containing the exception that is causing this terminating error. + + .EXAMPLE + if ($runFeature) + { + $errorMessage = $script:localizedData.FeatureMissing -f $path + New-NotImplementedException -Message $errorMessage -ErrorRecord $_ + } + + Throws an not implemented exception if the variable $runFeature contains + a value. +#> +function New-NotImplementedException +{ + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '')] + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $Message, + + [Parameter()] + [ValidateNotNull()] + [System.Management.Automation.ErrorRecord] + $ErrorRecord + ) + + if ($null -eq $ErrorRecord) + { + $invalidOperationException = New-Object -TypeName 'NotImplementedException' ` + -ArgumentList @($Message) + } + else + { + $invalidOperationException = New-Object -TypeName 'NotImplementedException' ` + -ArgumentList @($Message, $ErrorRecord.Exception) + } + + $newObjectParameters = @{ + TypeName = 'System.Management.Automation.ErrorRecord' + ArgumentList = @( + $invalidOperationException.ToString(), + 'MachineStateIncorrect', + 'NotImplemented', + $null + ) + } + + $errorRecordToThrow = New-Object @newObjectParameters + + throw $errorRecordToThrow +} +#EndRegion './Public/New-NotImplementedException.ps1' 65 +#Region './Public/New-ObjectNotFoundException.ps1' 0 + +<# + .SYNOPSIS + Creates and throws an object not found exception. + + .DESCRIPTION + Creates and throws an object not found exception. + + .PARAMETER Message + The message explaining why this error is being thrown. + + .PARAMETER ErrorRecord + The error record containing the exception that is causing this terminating error. + + .EXAMPLE + try + { + Get-ChildItem -Path $path + } + catch + { + $errorMessage = $script:localizedData.PathNotFoundMessage -f $path + New-ObjectNotFoundException -Message $errorMessage -ErrorRecord $_ + } +#> +function New-ObjectNotFoundException +{ + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '')] + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $Message, + + [Parameter()] + [ValidateNotNull()] + [System.Management.Automation.ErrorRecord] + $ErrorRecord + ) + + if ($null -eq $ErrorRecord) + { + $exception = New-Object -TypeName 'System.Exception' ` + -ArgumentList @($Message) + } + else + { + $exception = New-Object -TypeName 'System.Exception' ` + -ArgumentList @($Message, $ErrorRecord.Exception) + } + + $newObjectParameters = @{ + TypeName = 'System.Management.Automation.ErrorRecord' + ArgumentList = @( + $exception.ToString(), + 'MachineStateIncorrect', + 'ObjectNotFound', + $null + ) + } + + $errorRecordToThrow = New-Object @newObjectParameters + + throw $errorRecordToThrow +} +#EndRegion './Public/New-ObjectNotFoundException.ps1' 67 +#Region './Public/Remove-CommonParameter.ps1' 0 +<# + .SYNOPSIS + Removes common parameters from a hashtable. + + .DESCRIPTION + This function serves the purpose of removing common parameters and option + common parameters from a parameter hashtable. + + .PARAMETER Hashtable + The parameter hashtable that should be pruned. + + .EXAMPLE + Remove-CommonParameter -Hashtable $PSBoundParameters + + Returns a new hashtable without the common and optional common parameters. +#> +function Remove-CommonParameter +{ + [Diagnostics.CodeAnalysis.SuppressMessageAttribute( + 'PSUseShouldProcessForStateChangingFunctions', + '', + Justification = 'ShouldProcess is not supported in DSC resources.' + )] + [OutputType([System.Collections.Hashtable])] + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [System.Collections.Hashtable] + $Hashtable + ) + + $inputClone = $Hashtable.Clone() + + $commonParameters = [System.Management.Automation.PSCmdlet]::CommonParameters + $commonParameters += [System.Management.Automation.PSCmdlet]::OptionalCommonParameters + + $Hashtable.Keys | Where-Object -FilterScript { + $_ -in $commonParameters + } | ForEach-Object -Process { + $inputClone.Remove($_) + } + + return $inputClone +} +#EndRegion './Public/Remove-CommonParameter.ps1' 45 +#Region './Public/Set-DscMachineRebootRequired.ps1' 0 +<# + .SYNOPSIS + Set the DSC reboot required status variable. + + .DESCRIPTION + Sets the global DSCMachineStatus variable to a value of 1. + This function is used to set the global variable that indicates + to the LCM that a reboot of the node is required. + + .EXAMPLE + PS C:\> Set-DscMachineRebootRequired + + Sets the $global:DSCMachineStatus variable to 1. + + .NOTES + This function is implemented so that individual resource modules + do not need to use and therefore suppress Global variables + directly. It also enables mocking to increase testability of + consumers. +#> +function Set-DscMachineRebootRequired +{ + [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '')] + # Suppressing this rule because $global:DSCMachineStatus is used to trigger a reboot. + [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidGlobalVars', '')] + <# + Suppressing this rule because $global:DSCMachineStatus is only set, + never used (by design of Desired State Configuration). + #> + [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', '')] + [CmdletBinding()] + param + ( + ) + + $global:DSCMachineStatus = 1 +} +#EndRegion './Public/Set-DscMachineRebootRequired.ps1' 37 +#Region './Public/Set-PSModulePath.ps1' 0 + +<# + .SYNOPSIS + Set environment variable PSModulePath in the current session or machine + wide. + + .DESCRIPTION + This is a wrapper to set environment variable PSModulePath in current + session or machine wide. + + .PARAMETER Path + A string with all the paths separated by semi-colons. + + .PARAMETER Machine + If set the PSModulePath will be changed machine wide. If not set, only + the current session will be changed. + + .EXAMPLE + Set-PSModulePath -Path ';' + + .EXAMPLE + Set-PSModulePath -Path ';' -Machine +#> +function Set-PSModulePath +{ + [Diagnostics.CodeAnalysis.SuppressMessageAttribute( + 'PSUseShouldProcessForStateChangingFunctions', + '', + Justification = 'ShouldProcess is not supported in DSC resources.' + )] + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $Path, + + [Parameter()] + [System.Management.Automation.SwitchParameter] + $Machine + ) + + if ($Machine.IsPresent) + { + [System.Environment]::SetEnvironmentVariable('PSModulePath', $Path, [System.EnvironmentVariableTarget]::Machine) + } + else + { + $env:PSModulePath = $Path + } +} +#EndRegion './Public/Set-PSModulePath.ps1' 52 +#Region './Public/Test-DscParameterState.ps1' 0 +<# + .SYNOPSIS + This method is used to compare current and desired values for any DSC resource. + + .DESCRIPTION + This function tests the parameter status of DSC resource parameters against + the current values present on the system. + + .PARAMETER CurrentValues + A hashtable with the current values on the system, obtained by e.g. + Get-TargetResource. + + .PARAMETER DesiredValues + The hashtable of desired values. For example $PSBoundParameters with the + desired values. + + .PARAMETER Properties + This is a list of properties in the desired values list should be checked. + If this is empty then all values in DesiredValues are checked. + + .PARAMETER ExcludeProperties + This is a list of which properties in the desired values list should be checked. + If this is empty then all values in DesiredValues are checked. + + .PARAMETER TurnOffTypeChecking + Indicates that the type of the parameter should not be checked. + + .PARAMETER ReverseCheck + Indicates that a reverse check should be done. The current and desired state + are swapped for another test. + + .PARAMETER SortArrayValues + If the sorting of array values does not matter, values are sorted internally + before doing the comparison. + + .EXAMPLE + $currentState = Get-TargetResource @PSBoundParameters + + $returnValue = Test-DscParameterState -CurrentValues $currentState -DesiredValues $PSBoundParameters + + The function Get-TargetResource is called first using all bound parameters + to get the values in the current state. The result is then compared to the + desired state by calling `Test-DscParameterState`. + + .EXAMPLE + $getTargetResourceParameters = @{ + ServerName = $ServerName + InstanceName = $InstanceName + Name = $Name + } + + $returnValue = Test-DscParameterState ` + -CurrentValues (Get-TargetResource @getTargetResourceParameters) ` + -DesiredValues $PSBoundParameters ` + -ExcludeProperties @( + 'FailsafeOperator' + 'NotificationMethod' + ) + + This compares the values in the current state against the desires state. + The function Get-TargetResource is called using just the required parameters + to get the values in the current state. The parameter 'ExcludeProperties' + is used to exclude the properties 'FailsafeOperator' and + 'NotificationMethod' from the comparison. + + .EXAMPLE + $getTargetResourceParameters = @{ + ServerName = $ServerName + InstanceName = $InstanceName + Name = $Name + } + + $returnValue = Test-DscParameterState ` + -CurrentValues (Get-TargetResource @getTargetResourceParameters) ` + -DesiredValues $PSBoundParameters ` + -Properties ServerName, Name + + This compares the values in the current state against the desires state. + The function Get-TargetResource is called using just the required parameters + to get the values in the current state. The 'Properties' parameter is used + to to only compare the properties 'ServerName' and 'Name'. +#> +function Test-DscParameterState +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [System.Object] + $CurrentValues, + + [Parameter(Mandatory = $true)] + [System.Object] + $DesiredValues, + + [Parameter()] + [System.String[]] + [Alias('ValuesToCheck')] + $Properties, + + [Parameter()] + [System.String[]] + $ExcludeProperties, + + [Parameter()] + [System.Management.Automation.SwitchParameter] + $TurnOffTypeChecking, + + [Parameter()] + [System.Management.Automation.SwitchParameter] + $ReverseCheck, + + [Parameter()] + [System.Management.Automation.SwitchParameter] + $SortArrayValues + ) + + $returnValue = $true + + if ($CurrentValues -is [Microsoft.Management.Infrastructure.CimInstance] -or + $CurrentValues -is [Microsoft.Management.Infrastructure.CimInstance[]]) + { + $CurrentValues = ConvertTo-HashTable -CimInstance $CurrentValues + } + + if ($DesiredValues -is [Microsoft.Management.Infrastructure.CimInstance] -or + $DesiredValues -is [Microsoft.Management.Infrastructure.CimInstance[]]) + { + $DesiredValues = ConvertTo-HashTable -CimInstance $DesiredValues + } + + $types = 'System.Management.Automation.PSBoundParametersDictionary', 'System.Collections.Hashtable', 'Microsoft.Management.Infrastructure.CimInstance' + + if ($DesiredValues.GetType().FullName -notin $types) + { + New-InvalidArgumentException ` + -Message ($script:localizedData.InvalidDesiredValuesError -f $DesiredValues.GetType().FullName) ` + -ArgumentName 'DesiredValues' + } + + if ($CurrentValues.GetType().FullName -notin $types) + { + New-InvalidArgumentException ` + -Message ($script:localizedData.InvalidCurrentValuesError -f $CurrentValues.GetType().FullName) ` + -ArgumentName 'CurrentValues' + } + + if ($DesiredValues -is [Microsoft.Management.Infrastructure.CimInstance] -and -not $Properties) + { + New-InvalidArgumentException ` + -Message $script:localizedData.InvalidPropertiesError ` + -ArgumentName Properties + } + + $desiredValuesClean = Remove-CommonParameter -Hashtable $DesiredValues + + if (-not $Properties) + { + $keyList = $desiredValuesClean.Keys + } + else + { + $keyList = $Properties + } + if ($ExcludeProperties) + { + $keyList = $keyList | Where-Object -FilterScript { $_ -notin $ExcludeProperties } + } + + foreach ($key in $keyList) + { + $desiredValue = $desiredValuesClean.$key + $currentValue = $CurrentValues.$key + + if ($desiredValue -is [Microsoft.Management.Infrastructure.CimInstance] -or + $desiredValue -is [Microsoft.Management.Infrastructure.CimInstance[]]) + { + $desiredValue = ConvertTo-HashTable -CimInstance $desiredValue + } + if ($currentValue -is [Microsoft.Management.Infrastructure.CimInstance] -or + $currentValue -is [Microsoft.Management.Infrastructure.CimInstance[]]) + { + $currentValue = ConvertTo-HashTable -CimInstance $currentValue + } + + if ($null -ne $desiredValue) + { + $desiredType = $desiredValue.GetType() + } + else + { + $desiredType = @{ + Name = 'Unknown' + } + } + + if ($null -ne $currentValue) + { + $currentType = $currentValue.GetType() + } + else + { + $currentType = @{ + Name = 'Unknown' + } + } + + if ($currentType.Name -ne 'Unknown' -and $desiredType.Name -eq 'PSCredential') + { + # This is a credential object. Compare only the user name + if ($currentType.Name -eq 'PSCredential' -and $currentValue.UserName -eq $desiredValue.UserName) + { + Write-Verbose -Message ($script:localizedData.MatchPsCredentialUsernameMessage -f $currentValue.UserName, $desiredValue.UserName) + continue + } + else + { + Write-Verbose -Message ($script:localizedData.NoMatchPsCredentialUsernameMessage -f $currentValue.UserName, $desiredValue.UserName) + $returnValue = $false + } + + # Assume the string is our username when the matching desired value is actually a credential + if ($currentType.Name -eq 'string' -and $currentValue -eq $desiredValue.UserName) + { + Write-Verbose -Message ($script:localizedData.MatchPsCredentialUsernameMessage -f $currentValue, $desiredValue.UserName) + continue + } + else + { + Write-Verbose -Message ($script:localizedData.NoMatchPsCredentialUsernameMessage -f $currentValue, $desiredValue.UserName) + $returnValue = $false + } + } + + if (-not $TurnOffTypeChecking) + { + if (($desiredType.Name -ne 'Unknown' -and $currentType.Name -ne 'Unknown') -and + $desiredType.FullName -ne $currentType.FullName) + { + Write-Verbose -Message ($script:localizedData.NoMatchTypeMismatchMessage -f $key, $currentType.FullName, $desiredType.FullName) + $returnValue = $false + continue + } + } + + if ($currentValue -eq $desiredValue -and -not $desiredType.IsArray) + { + Write-Verbose -Message ($script:localizedData.MatchValueMessage -f $desiredType.FullName, $key, $currentValue, $desiredValue) + continue + } + + if ($desiredValuesClean.GetType().Name -in 'HashTable', 'PSBoundParametersDictionary') + { + $checkDesiredValue = $desiredValuesClean.ContainsKey($key) + } + else + { + $checkDesiredValue = Test-DscObjectHasProperty -Object $desiredValuesClean -PropertyName $key + } + + if (-not $checkDesiredValue) + { + Write-Verbose -Message ($script:localizedData.MatchValueMessage -f $desiredType.FullName, $key, $currentValue, $desiredValue) + continue + } + + if ($desiredType.IsArray) + { + Write-Verbose -Message ($script:localizedData.TestDscParameterCompareMessage -f $key, $desiredType.FullName) + + if (-not $currentValue -and -not $desiredValue) + { + Write-Verbose -Message ($script:localizedData.MatchValueMessage -f $desiredType.FullName, $key, 'empty array', 'empty array') + continue + } + elseif (-not $currentValue) + { + Write-Verbose -Message ($script:localizedData.NoMatchValueMessage -f $desiredType.FullName, $key, $currentValue, $desiredValue) + $returnValue = $false + continue + } + elseif ($currentValue.Count -ne $desiredValue.Count) + { + Write-Verbose -Message ($script:localizedData.NoMatchValueDifferentCountMessage -f $desiredType.FullName, $key, $currentValue.Count, $desiredValue.Count) + $returnValue = $false + continue + } + else + { + $desiredArrayValues = $desiredValue + $currentArrayValues = $currentValue + + if ($SortArrayValues) + { + $desiredArrayValues = @($desiredArrayValues | Sort-Object) + $currentArrayValues = @($currentArrayValues | Sort-Object) + } + + for ($i = 0; $i -lt $desiredArrayValues.Count; $i++) + { + if ($desiredArrayValues[$i]) + { + $desiredType = $desiredArrayValues[$i].GetType() + } + else + { + $desiredType = @{ + Name = 'Unknown' + } + } + + if ($currentArrayValues[$i]) + { + $currentType = $currentArrayValues[$i].GetType() + } + else + { + $currentType = @{ + Name = 'Unknown' + } + } + + if (-not $TurnOffTypeChecking) + { + if (($desiredType.Name -ne 'Unknown' -and $currentType.Name -ne 'Unknown') -and + $desiredType.FullName -ne $currentType.FullName) + { + Write-Verbose -Message ($script:localizedData.NoMatchElementTypeMismatchMessage -f $key, $i, $currentType.FullName, $desiredType.FullName) + $returnValue = $false + continue + } + } + + #Convert a scriptblock into a string as scriptblocks are not comparable + $wasCurrentArrayValuesConverted = $false + if ($currentArrayValues[$i] -is [scriptblock]) + { + $currentArrayValues[$i] = if ($desiredArrayValues[$i] -is [string]) + { + $currentArrayValues[$i] = $currentArrayValues[$i].Invoke() + } + else + { + $currentArrayValues[$i].ToString() + } + $wasCurrentArrayValuesConverted = $true + } + if ($desiredArrayValues[$i] -is [scriptblock]) + { + $desiredArrayValues[$i] = if ($currentArrayValues[$i] -is [string] -and -not $wasCurrentArrayValuesConverted) + { + $desiredArrayValues[$i].Invoke() + } + else + { + $desiredArrayValues[$i].ToString() + } + } + + if ($desiredType -eq [System.Collections.Hashtable] -and $currentType -eq [System.Collections.Hashtable]) + { + $param = $PSBoundParameters + $param.CurrentValues = $currentArrayValues[$i] + $param.DesiredValues = $desiredArrayValues[$i] + + if ($returnValue) + { + $returnValue = Test-DscParameterState @param + } + else + { + Test-DscParameterState @param | Out-Null + } + continue + } + + if ($desiredArrayValues[$i] -ne $currentArrayValues[$i]) + { + Write-Verbose -Message ($script:localizedData.NoMatchElementValueMismatchMessage -f $i, $desiredType.FullName, $key, $currentArrayValues[$i], $desiredArrayValues[$i]) + $returnValue = $false + continue + } + else + { + Write-Verbose -Message ($script:localizedData.MatchElementValueMessage -f $i, $desiredType.FullName, $key, $currentArrayValues[$i], $desiredArrayValues[$i]) + continue + } + } + + } + } + elseif ($desiredType -eq [System.Collections.Hashtable] -and $currentType -eq [System.Collections.Hashtable]) + { + $param = $PSBoundParameters + $param.CurrentValues = $currentValue + $param.DesiredValues = $desiredValue + + if ($returnValue) + { + $returnValue = Test-DscParameterState @param + } + else + { + Test-DscParameterState @param | Out-Null + } + continue + } + else + { + #Convert a scriptblock into a string as scriptblocks are not comparable + $wasCurrentValue = $false + if ($currentValue -is [scriptblock]) + { + $currentValue = if ($desiredValue -is [string]) + { + $currentValue = $currentValue.Invoke() + } + else + { + $currentValue.ToString() + } + $wasCurrentValue = $true + } + if ($desiredValue -is [scriptblock]) + { + $desiredValue = if ($currentValue -is [string] -and -not $wasCurrentValue) + { + $desiredValue.Invoke() + } + else + { + $desiredValue.ToString() + } + } + + if ($desiredValue -ne $currentValue) + { + Write-Verbose -Message ($script:localizedData.NoMatchValueMessage -f $desiredType.FullName, $key, $currentValue, $desiredValue) + $returnValue = $false + } + } + } + + if ($ReverseCheck) + { + Write-Verbose -Message $script:localizedData.StartingReverseCheck + $reverseCheckParameters = $PSBoundParameters + $reverseCheckParameters.CurrentValues = $DesiredValues + $reverseCheckParameters.DesiredValues = $CurrentValues + $null = $reverseCheckParameters.Remove('ReverseCheck') + + if ($returnValue) + { + $returnValue = Test-DscParameterState @reverseCheckParameters + } + else + { + $null = Test-DscParameterState @reverseCheckParameters + } + } + + Write-Verbose -Message ($script:localizedData.TestDscParameterResultMessage -f $returnValue) + return $returnValue +} +#EndRegion './Public/Test-DscParameterState.ps1' 464 +#Region './Public/Test-IsNanoServer.ps1' 0 +<# + .SYNOPSIS + Tests if the current OS is a Nano server. + + .DESCRIPTION + Tests if the current OS is a Nano server. + + .EXAMPLE + Test-IsNanoServer + + Returns $true if the current operating system is Nano Server, if not $false + is returned. +#> +function Test-IsNanoServer +{ + [OutputType([System.Boolean])] + [CmdletBinding()] + param () + + $productDatacenterNanoServer = 143 + $productStandardNanoServer = 144 + + $operatingSystemSKU = (Get-CimInstance -ClassName Win32_OperatingSystem).OperatingSystemSKU + + Write-Verbose -Message ($script:localizedData.TestIsNanoServerOperatingSystemSku -f $operatingSystemSKU) + + return ($operatingSystemSKU -in ($productDatacenterNanoServer, $productStandardNanoServer)) +} +#EndRegion './Public/Test-IsNanoServer.ps1' 28 +#Region './suffix.ps1' 0 +$script:localizedData = Get-LocalizedData -DefaultUICulture 'en-US' +#EndRegion './suffix.ps1' 1 diff --git a/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/Modules/DscResource.Common/0.9.3/en-US/DscResource.Common.strings.psd1 b/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/Modules/DscResource.Common/0.9.3/en-US/DscResource.Common.strings.psd1 new file mode 100644 index 0000000..fd8440b --- /dev/null +++ b/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/Modules/DscResource.Common/0.9.3/en-US/DscResource.Common.strings.psd1 @@ -0,0 +1,37 @@ +# Localized English (en-US) strings. + +ConvertFrom-StringData @' + TestIsNanoServerOperatingSystemSku = OperatingSystemSKU {0} was returned by Win32_OperatingSystem when detecting if operating system is Nano Server. (DRC0008) + ModuleNotFound = Please ensure that the PowerShell module '{0}' is installed. (DRC0009) + ParameterUsageWrong = None of the parameter(s) '{0}' may be used at the same time as any of the parameter(s) '{1}'. (DRC0010) + AddressFormatError = Address '{0}' is not in the correct format. Please correct the Address parameter in the configuration and try again. (DRC0011) + AddressIPv4MismatchError = Address '{0}' is in IPv4 format, which does not match server address family {1}. Please correct either of them in the configuration and try again. (DRC0012) + AddressIPv6MismatchError = Address '{0}' is in IPv6 format, which does not match server address family {1}. Please correct either of them in the configuration and try again. (DRC0013) + InvalidDesiredValuesError = Property 'DesiredValues' in Test-DscParameterState must be either a Hashtable or CimInstance. Type detected was '{0}'. (DRC0014) + InvalidCurrentValuesError = Property 'CurrentValues' in Test-DscParameterState must be either a Hashtable, CimInstance, or CimIntance[]. Type detected was '{0}'. (DRC0015) + InvalidPropertiesError = If 'DesiredValues' is a CimInstance then property 'Properties' must contain a value. (DRC0016) + MatchPsCredentialUsernameMessage = MATCH: PSCredential username match. Current state is '{0}' and desired state is '{1}'. (DRC0017) + NoMatchPsCredentialUsernameMessage = NOTMATCH: PSCredential username mismatch. Current state is '{0}' and desired state is '{1}'. (DRC0018) + NoMatchTypeMismatchMessage = NOTMATCH: Type mismatch for property '{0}' Current state type is '{1}' and desired type is '{2}'. (DRC0019) + MatchValueMessage = MATCH: Value (type '{0}') for property '{1}' does match. Current state is '{2}' and desired state is '{3}'. (DRC0020) + NoMatchValueMessage = NOTMATCH: Value (type '{0}') for property '{1}' does not match. Current state is '{2}' and desired state is '{3}'. (DRC0021) + NoMatchValueDifferentCountMessage = NOTMATCH: Value (type '{0}') for property '{1}' does have a different count. Current state count is '{2}' and desired state count is '{3}'. (DRC0022) + NoMatchElementTypeMismatchMessage = NOTMATCH: Type mismatch for property '{0}' Current state type of element [{1}] is '{2}' and desired type is '{3}'. (DRC0023) + NoMatchElementValueMismatchMessage = NOTMATCH: Value [{0}] (type '{1}') for property '{2}' does match. Current state is '{3}' and desired state is '{4}'. (DRC0024) + MatchElementValueMessage = MATCH: Value [{0}] (type '{1}') for property '{2}' does match. Current state is '{3}' and desired state is '{4}'. (DRC0025) + TestDscParameterResultMessage = Test-DscParameter result is '{0}'. (DRC0026) + StartingReverseCheck = Starting with a reverse check. (DRC0027) + TestDscParameterCompareMessage = Comparing values in property '{0}'. (DRC0028) + TooManyCimInstances = More than one CIM instance was returned from the current state. (DRC0029) + TestingCimInstance = Testing CIM instance '{0}' with the key properties '{1}'. (DRC0030) + MissingCimInstance = The CIM instance '{0}' with the key properties '{1}' is missing. (DRC0031) + ArrayValueIsAbsent = The array value '{0}' is absent. (DRC0032) + ArrayValueIsPresent = The array value '{0}' is present. (DRC0033) + KeyPropertiesMissing = The hashtable passed to function Test-DscPropertyState is missing the key 'KeyProperties'. This must be set to the property names that makes each instance in the CIM instance collection unique. (DRC0034) + ArrayDoesNotMatch = One or more values in an array does not match the desired state. Details of the changes are below. (DRC0035) + PropertyValueOfTypeDoesNotMatch = {0} value does not match. Current value is '{1}', but expected the value '{2}'. (DRC0036) + UnableToCompareType = Unable to compare the type {0} as it is not handled by the Test-DscPropertyState cmdlet. (DRC0037) + EvaluatePropertyState = Evaluating the state of the property '{0}'. (DRC0038) + PropertyInDesiredState = The parameter '{0}' is in desired state. (DRC0039) + PropertyNotInDesiredState = The parameter '{0}' is not in desired state. (DRC0040) +'@ diff --git a/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/Modules/DscResource.Common/0.9.3/en-US/about_DscResource.Common.help.txt b/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/Modules/DscResource.Common/0.9.3/en-US/about_DscResource.Common.help.txt new file mode 100644 index 0000000..2a99677 --- /dev/null +++ b/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/Modules/DscResource.Common/0.9.3/en-US/about_DscResource.Common.help.txt @@ -0,0 +1,26 @@ +TOPIC + about_DscResource.Common + +SHORT DESCRIPTION + Common functions used in DSC tesources. + +LONG DESCRIPTION + This module contains common functions that are used in DSC resources. + +EXAMPLES + PS C:\> Get-Command -Module DscResource.Common + +NOTE: + Thank you to the DSC Community contributors who contributed to this module by + writing code, sharing opinions, and provided feedback. + +TROUBLESHOOTING NOTE: + Go to the Github repository for read about issues, submit a new issue, and read + about new releases. https://github.com/dsccommunity/DscResource.Common + +SEE ALSO + - https://github.com/dsccommunity/DscResource.Common + +KEYWORDS + DSC, Localization + diff --git a/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/Modules/NetworkingDsc.Common/NetworkingDsc.Common.psm1 b/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/Modules/NetworkingDsc.Common/NetworkingDsc.Common.psm1 new file mode 100644 index 0000000..d9347fa --- /dev/null +++ b/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/Modules/NetworkingDsc.Common/NetworkingDsc.Common.psm1 @@ -0,0 +1,582 @@ +$modulePath = Join-Path -Path (Split-Path -Path (Split-Path -Path $PSScriptRoot -Parent) -Parent) -ChildPath 'Modules' + +Import-Module -Name (Join-Path -Path $modulePath -ChildPath 'DscResource.Common') + +# Import Localization Strings +$script:localizedData = Get-LocalizedData -DefaultUICulture 'en-US' + +<# + .SYNOPSIS + Converts any IP Addresses containing CIDR notation filters in an array to use Subnet Mask + notation. + + .PARAMETER Address + The array of addresses to that need to be converted. +#> +function Convert-CIDRToSubhetMask +{ + [CmdletBinding()] + [OutputType([ Microsoft.Management.Infrastructure.CimInstance])] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String[]] + $Address + ) + + $results = @() + + foreach ($entry in $Address) + { + if (-not $entry.Contains(':') -and -not $entry.Contains('-')) + { + $entrySplit = $entry -split '/' + + if (-not [String]::IsNullOrEmpty($entrySplit[1])) + { + # There was a / so this contains a Subnet Mask or CIDR + $prefix = $entrySplit[0] + $postfix = $entrySplit[1] + + if ($postfix -match '^[0-9]*$') + { + # The postfix contains CIDR notation so convert this to Subnet Mask + $cidr = [System.Int32] $postfix + $subnetMaskInt64 = ([convert]::ToInt64(('1' * $cidr + '0' * (32 - $cidr)), 2)) + $subnetMask = @( + ([math]::Truncate($subnetMaskInt64 / 16777216)) + ([math]::Truncate(($subnetMaskInt64 % 16777216) / 65536)) + ([math]::Truncate(($subnetMaskInt64 % 65536) / 256)) + ([math]::Truncate($subnetMaskInt64 % 256)) + ) + } + else + { + $subnetMask = $postfix -split '\.' + } + + <# + Apply the Subnet Mast to the IP Address so that we end up with a correctly + masked IP Address that will match what the Firewall rule returns. + #> + $maskedIp = $prefix -split '\.' + + for ([System.Int32] $Octet = 0; $octet -lt 4; $octet++) + { + $maskedIp[$Octet] = $maskedIp[$octet] -band $SubnetMask[$octet] + } + + $entry = '{0}/{1}' -f ($maskedIp -join '.'), ($subnetMask -join '.') + } + } + + $results += $entry + } + + return $results +} # Convert-CIDRToSubhetMask + +<# + .SYNOPSIS + This function will find a network adapter based on the provided + search parameters. + + .PARAMETER Name + This is the name of network adapter to find. + + .PARAMETER PhysicalMediaType + This is the media type of the network adapter to find. + + .PARAMETER Status + This is the status of the network adapter to find. + + .PARAMETER MacAddress + This is the MAC address of the network adapter to find. + + .PARAMETER InterfaceDescription + This is the interface description of the network adapter to find. + + .PARAMETER InterfaceIndex + This is the interface index of the network adapter to find. + + .PARAMETER InterfaceGuid + This is the interface GUID of the network adapter to find. + + .PARAMETER DriverDescription + This is the driver description of the network adapter. + + .PARAMETER InterfaceNumber + This is the interface number of the network adapter if more than one + are returned by the parameters. + + .PARAMETER IgnoreMultipleMatchingAdapters + This switch will suppress an error occurring if more than one matching + adapter matches the parameters passed. +#> +function Find-NetworkAdapter +{ + [CmdletBinding()] + [OutputType([System.Collections.Hashtable])] + param + ( + [Parameter()] + [System.String] + $Name, + + [Parameter()] + [System.String] + $PhysicalMediaType, + + [Parameter()] + [ValidateNotNullOrEmpty()] + [ValidateSet('Up', 'Disconnected', 'Disabled')] + [System.String] + $Status = 'Up', + + [Parameter()] + [System.String] + $MacAddress, + + [Parameter()] + [System.String] + $InterfaceDescription, + + [Parameter()] + [System.UInt32] + $InterfaceIndex, + + [Parameter()] + [System.String] + $InterfaceGuid, + + [Parameter()] + [System.String] + $DriverDescription, + + [Parameter()] + [System.UInt32] + $InterfaceNumber = 1, + + [Parameter()] + [System.Boolean] + $IgnoreMultipleMatchingAdapters = $false + ) + + Write-Verbose -Message ( @("$($MyInvocation.MyCommand): " + $($script:localizedData.FindingNetAdapterMessage) + ) -join '') + + $adapterFilters = @() + + if ($PSBoundParameters.ContainsKey('Name')) + { + $adapterFilters += @('($_.Name -eq $Name)') + } # if + + if ($PSBoundParameters.ContainsKey('PhysicalMediaType')) + { + $adapterFilters += @('($_.PhysicalMediaType -eq $PhysicalMediaType)') + } # if + + if ($PSBoundParameters.ContainsKey('Status')) + { + $adapterFilters += @('($_.Status -eq $Status)') + } # if + + if ($PSBoundParameters.ContainsKey('MacAddress')) + { + $adapterFilters += @('($_.MacAddress -eq $MacAddress)') + } # if + + if ($PSBoundParameters.ContainsKey('InterfaceDescription')) + { + $adapterFilters += @('($_.InterfaceDescription -eq $InterfaceDescription)') + } # if + + if ($PSBoundParameters.ContainsKey('InterfaceIndex')) + { + $adapterFilters += @('($_.InterfaceIndex -eq $InterfaceIndex)') + } # if + + if ($PSBoundParameters.ContainsKey('InterfaceGuid')) + { + $adapterFilters += @('($_.InterfaceGuid -eq $InterfaceGuid)') + } # if + + if ($PSBoundParameters.ContainsKey('DriverDescription')) + { + $adapterFilters += @('($_.DriverDescription -eq $DriverDescription)') + } # if + + if ($adapterFilters.Count -eq 0) + { + Write-Verbose -Message ( @("$($MyInvocation.MyCommand): " + $($script:localizedData.AllNetAdaptersFoundMessage) + ) -join '') + + $matchingAdapters = @(Get-NetAdapter) + } + else + { + # Join all the filters together + $adapterFilterScript = '(' + ($adapterFilters -join ' -and ') + ')' + $matchingAdapters = @(Get-NetAdapter | + Where-Object -FilterScript ([ScriptBlock]::Create($adapterFilterScript))) + } + + # Were any adapters found matching the criteria? + if ($matchingAdapters.Count -eq 0) + { + New-InvalidOperationException ` + -Message ($script:localizedData.NetAdapterNotFoundError) + + # Return a null so that ErrorAction SilentlyContinue works correctly + return $null + } + else + { + Write-Verbose -Message ( @("$($MyInvocation.MyCommand): " + $($script:localizedData.NetAdapterFoundMessage -f $matchingAdapters.Count) + ) -join '') + + if ($matchingAdapters.Count -gt 1) + { + if ($IgnoreMultipleMatchingAdapters) + { + # Was the number of matching adapters found matching the adapter number? + if (($InterfaceNumber -gt 1) -and ($InterfaceNumber -gt $matchingAdapters.Count)) + { + New-InvalidOperationException ` + -Message ($script:localizedData.InvalidNetAdapterNumberError ` + -f $matchingAdapters.Count, $InterfaceNumber) + + # Return a null so that ErrorAction SilentlyContinue works correctly + return $null + } # if + } + else + { + New-InvalidOperationException ` + -Message ($script:localizedData.MultipleMatchingNetAdapterFound ` + -f $matchingAdapters.Count) + + # Return a null so that ErrorAction SilentlyContinue works correctly + return $null + } # if + } # if + } # if + + # Identify the exact adapter from the adapters that match + $exactAdapter = $matchingAdapters[$InterfaceNumber - 1] + + $returnValue = [PSCustomObject] @{ + Name = $exactAdapter.Name + PhysicalMediaType = $exactAdapter.PhysicalMediaType + Status = $exactAdapter.Status + MacAddress = $exactAdapter.MacAddress + InterfaceDescription = $exactAdapter.InterfaceDescription + InterfaceIndex = $exactAdapter.InterfaceIndex + InterfaceGuid = $exactAdapter.InterfaceGuid + MatchingAdapterCount = $matchingAdapters.Count + } + + return $returnValue +} # Find-NetworkAdapter + +<# + .SYNOPSIS + Returns the DNS Client Server static address that are assigned to a network + adapter. This is required because Get-DnsClientServerAddress always returns + the currently assigned server addresses whether regardless if they were + assigned as static or by DHCP. + + The only way that could be found to do this is to query the registry. + + .PARAMETER InterfaceAlias + Alias of the network interface to get the static DNS Server addresses from. + + .PARAMETER AddressFamily + IP address family. +#> +function Get-DnsClientServerStaticAddress +{ + [CmdletBinding()] + [OutputType([System.String[]])] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $InterfaceAlias, + + [Parameter(Mandatory = $true)] + [ValidateSet('IPv4', 'IPv6')] + [System.String] + $AddressFamily + ) + + Write-Verbose -Message ( @("$($MyInvocation.MyCommand): " + $($script:localizedData.GettingDNSServerStaticAddressMessage) -f $AddressFamily, $InterfaceAlias + ) -join '') + + # Look up the interface Guid + $adapter = Get-NetAdapter ` + -InterfaceAlias $InterfaceAlias ` + -ErrorAction SilentlyContinue + + if (-not $adapter) + { + New-InvalidOperationException ` + -Message ($script:localizedData.InterfaceAliasNotFoundError ` + -f $InterfaceAlias) + + # Return null to support ErrorAction Silently Continue + return $null + } # if + + $interfaceGuid = $adapter.InterfaceGuid.ToLower() + + if ($AddressFamily -eq 'IPv4') + { + $interfaceRegKeyPath = "HKLM:\SYSTEM\CurrentControlSet\Services\Tcpip\Parameters\Interfaces\$interfaceGuid\" + } + else + { + $interfaceRegKeyPath = "HKLM:\SYSTEM\CurrentControlSet\Services\Tcpip6\Parameters\Interfaces\$interfaceGuid\" + } # if + + $interfaceInformation = Get-ItemProperty ` + -Path $interfaceRegKeyPath ` + -ErrorAction SilentlyContinue + $nameServerAddressString = $interfaceInformation.NameServer + + # Are any statically assigned addresses for this adapter? + if ([System.String]::IsNullOrWhiteSpace($nameServerAddressString)) + { + # Static DNS Server addresses not found so return empty array + Write-Verbose -Message ( @("$($MyInvocation.MyCommand): " + $($script:localizedData.DNSServerStaticAddressNotSetMessage) -f $AddressFamily, $InterfaceAlias + ) -join '') + + return $null + } + else + { + # Static DNS Server addresses found so split them into an array using comma + Write-Verbose -Message ( @("$($MyInvocation.MyCommand): " + $($script:localizedData.DNSServerStaticAddressFoundMessage) -f $AddressFamily, $InterfaceAlias, $nameServerAddressString + ) -join '') + + return @($nameServerAddressString -split ',') + } # if +} # Get-DnsClientServerStaticAddress + +<# + .SYNOPSIS + Returns the WINS Client Server static address that are assigned to a network + adapter. The CIM class Win32_NetworkAdapterConfiguration unfortunately only supports + the primary and secondary WINS server. The registry gives more flexibility. + + .PARAMETER InterfaceAlias + Alias of the network interface to get the static WINS Server addresses from. +#> +function Get-WinsClientServerStaticAddress +{ + [CmdletBinding()] + [OutputType([System.String[]])] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $InterfaceAlias + ) + + Write-Verbose -Message ("$($MyInvocation.MyCommand): $($script:localizedData.GettingWinsServerStaticAddressMessage -f $InterfaceAlias)") + + # Look up the interface Guid + $adapter = Get-NetAdapter -InterfaceAlias $InterfaceAlias -ErrorAction SilentlyContinue + + if (-not $adapter) + { + New-InvalidOperationException -Message ($script:localizedData.InterfaceAliasNotFoundError -f $InterfaceAlias) + + # Return null to support ErrorAction Silently Continue + return $null + } + + $interfaceGuid = $adapter.InterfaceGuid.ToLower() + + $interfaceRegKeyPath = "HKLM:\SYSTEM\CurrentControlSet\Services\NetBT\Parameters\Interfaces\Tcpip_$interfaceGuid\" + + $interfaceInformation = Get-ItemProperty -Path $interfaceRegKeyPath -ErrorAction SilentlyContinue + $nameServerAddressString = $interfaceInformation.NameServerList + + # Are any statically assigned addresses for this adapter? + if (-not $nameServerAddressString) + { + # Static DNS Server addresses not found so return empty array + Write-Verbose -Message ("$($MyInvocation.MyCommand): $($script:localizedData.WinsServerStaticAddressNotSetMessage -f $InterfaceAlias)") + return $null + } + else + { + # Static DNS Server addresses found so split them into an array using comma + Write-Verbose -Message ("$($MyInvocation.MyCommand): $($script:localizedData.WinsServerStaticAddressFoundMessage -f + $InterfaceAlias, ($nameServerAddressString -join ','))") + + return $nameServerAddressString + } +} # Get-WinsClientServerStaticAddress + +<# + .SYNOPSIS + Sets the WINS Client Server static address on a network adapter. The CIM class + Win32_NetworkAdapterConfiguration unfortunately only supports the primary and + secondary WINS server. The registry gives more flexibility. + + .PARAMETER InterfaceAlias + Alias of the network interface to set the static WINS Server addresses on. +#> +function Set-WinsClientServerStaticAddress +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $InterfaceAlias, + + [Parameter(Mandatory = $true)] + [AllowEmptyCollection()] + [System.String[]] + $Address + ) + + Write-Verbose -Message ("$($MyInvocation.MyCommand): $($script:localizedData.SettingWinsServerStaticAddressMessage -f $InterfaceAlias, ($Address -join ', '))") + + # Look up the interface Guid + $adapter = Get-NetAdapter -InterfaceAlias $InterfaceAlias -ErrorAction SilentlyContinue + + if (-not $adapter) + { + New-InvalidOperationException -Message ($script:localizedData.InterfaceAliasNotFoundError -f $InterfaceAlias) + } + + $interfaceGuid = $adapter.InterfaceGuid.ToLower() + + $interfaceRegKeyPath = "HKLM:\SYSTEM\CurrentControlSet\Services\NetBT\Parameters\Interfaces\Tcpip_$interfaceGuid\" + + Set-ItemProperty -Path $interfaceRegKeyPath -Name NameServerList -Value $Address + +} # Set-WinsClientServerStaticAddress + +<# + .SYNOPSIS + Gets the IP Address prefix from a provided IP Address in CIDR notation. + + .PARAMETER IPAddress + IP Address to get prefix for, can be in CIDR notation. + + .PARAMETER AddressFamily + Address family for provided IP Address, defaults to IPv4. + +#> +function Get-IPAddressPrefix +{ + [cmdletbinding()] + param + ( + [Parameter(Mandatory = $true, + ValueFromPipeline)] + [System.String[]] + $IPAddress, + + [Parameter()] + [ValidateSet('IPv4', 'IPv6')] + [System.String] + $AddressFamily = 'IPv4' + ) + + process + { + foreach ($singleIP in $IPAddress) + { + $prefixLength = ($singleIP -split '/')[1] + + if (-not ($prefixLength) -and $AddressFamily -eq 'IPv4') + { + if ($singleIP.split('.')[0] -in (0..127)) + { + $prefixLength = 8 + } + elseif ($singleIP.split('.')[0] -in (128..191)) + { + $prefixLength = 16 + } + elseif ($singleIP.split('.')[0] -in (192..223)) + { + $prefixLength = 24 + } + } + elseif (-not ($prefixLength) -and $AddressFamily -eq 'IPv6') + { + $prefixLength = 64 + } + + [PSCustomObject]@{ + IPAddress = $singleIP.split('/')[0] + prefixLength = $prefixLength + } + } + } +} + +<# +.SYNOPSIS + Returns a filter string for the net adapter CIM instances. Wildcards supported. + +.PARAMETER InterfaceAlias + Specifies the alias of a network interface. Supports the use of '*' or '%'. +#> +function Format-Win32NetworkAdapterFilterByNetConnectionId +{ + [CmdletBinding()] + [OutputType([System.String])] + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $InterfaceAlias + ) + + if ($InterfaceAlias.Contains('*')) + { + $InterfaceAlias = $InterfaceAlias.Replace('*','%') + } + + if ($InterfaceAlias.Contains('%')) + { + $operator = ' LIKE ' + } + else + { + $operator = '=' + } + + $returnNetAdapaterFilter = 'NetConnectionID{0}"{1}"' -f $operator, $InterfaceAlias + + return $returnNetAdapaterFilter +} + +Export-ModuleMember -Function @( + 'Convert-CIDRToSubhetMask' + 'Find-NetworkAdapter' + 'Get-DnsClientServerStaticAddress' + 'Get-WinsClientServerStaticAddress' + 'Set-WinsClientServerStaticAddress' + 'Get-IPAddressPrefix' + 'Format-Win32NetworkAdapterFilterByNetConnectionId' +) diff --git a/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/Modules/NetworkingDsc.Common/en-US/NetworkingDsc.Common.strings.psd1 b/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/Modules/NetworkingDsc.Common/en-US/NetworkingDsc.Common.strings.psd1 new file mode 100644 index 0000000..3c8b51d --- /dev/null +++ b/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/Modules/NetworkingDsc.Common/en-US/NetworkingDsc.Common.strings.psd1 @@ -0,0 +1,16 @@ +ConvertFrom-StringData @' + FindingNetAdapterMessage = Finding network adapters matching the parameters. + AllNetAdaptersFoundMessage = Found all network adapters because no filter parameters provided. + NetAdapterFoundMessage = {0} network adapters were found matching the parameters. + NetAdapterNotFoundError = A network adapter matching the parameters was not found. Please correct the properties and try again. + InterfaceAliasNotFoundError = A network adapter with the alias '{0}' could not be found. + MultipleMatchingNetAdapterFound = Please adjust the parameters or specify IgnoreMultipleMatchingAdapters to only use the first and try again. + InvalidNetAdapterNumberError = network adapter interface number {0} was specified but only {1} was found. Please correct the interface number and try again. + GettingDNSServerStaticAddressMessage = Getting staticly assigned DNS server {0} address for interface alias '{1}'. + GettingWinsServerStaticAddressMessage = Getting staticly assigned WINS server address for interface alias '{0}'. + SettingWinsServerStaticAddressMessage = Setting staticly assigned WINS server address for interface alias '{0}' to '{1}'. + DNSServerStaticAddressNotSetMessage = Statically assigned DNS server {0} address for interface alias '{1}' is not set. + WinsServerStaticAddressNotSetMessage = Statically assigned WINS server address for interface alias '{0}' is not set. + DNSServerStaticAddressFoundMessage = Statically assigned DNS server {0} address for interface alias '{1}' is '{2}'. + WinsServerStaticAddressFoundMessage = Statically assigned WINS server address for interface alias '{0}' is '{1}'. +'@ diff --git a/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/NetworkingDsc.psd1 b/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/NetworkingDsc.psd1 new file mode 100644 index 0000000..9928ca0 --- /dev/null +++ b/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/NetworkingDsc.psd1 @@ -0,0 +1,114 @@ +@{ + # Version number of this module. + moduleVersion = '8.2.0' + + # ID used to uniquely identify this module + GUID = 'e6647cc3-ce9c-4c86-9eb8-2ee8919bf358' + + # Author of this module + Author = 'DSC Community' + + # Company or vendor of this module + CompanyName = 'DSC Community' + + # Copyright statement for this module + Copyright = 'Copyright the DSC Community contributors. All rights reserved.' + + # Description of the functionality provided by this module + Description = 'DSC resources for configuring settings related to networking.' + + # Minimum version of the Windows PowerShell engine required by this module + PowerShellVersion = '4.0' + + # Minimum version of the common language runtime (CLR) required by this module + CLRVersion = '4.0' + + # Functions to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no functions to export. + FunctionsToExport = @() + + # Cmdlets to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no cmdlets to export. + CmdletsToExport = @() + + # Variables to export from this module + VariablesToExport = @() + + # Aliases to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no aliases to export. + AliasesToExport = @() + + # DSC resources to export from this module + DscResourcesToExport = @( + 'DefaultGatewayAddress', + 'DnsClientGlobalSetting', + 'DnsConnectionSuffix', + 'DNSServerAddress', + 'Firewall', + 'FirewallProfile', + 'HostsFile', + 'IPAddress', + 'IPAddressOption', + 'NetAdapterAdvancedProperty', + 'NetAdapterBinding', + 'NetAdapterLso', + 'NetAdapterName', + 'NetAdapterRDMA', + 'NetAdapterRsc', + 'NetAdapterRss', + 'NetAdapterState', + 'NetBIOS', + 'NetConnectionProfile', + 'NetIPInterface', + 'NetworkTeam', + 'NetworkTeamInterface', + 'ProxySettings', + 'Route', + 'WINSSetting' + ) + + # Private data to pass to the module specified in RootModule/ModuleToProcess. This may also contain a PSData hashtable with additional module metadata used by PowerShell. + PrivateData = @{ + + PSData = @{ + # Set to a prerelease string value if the release should be a prerelease. + Prerelease = '' + + # Tags applied to this module. These help with module discovery in online galleries. + Tags = @('DesiredStateConfiguration', 'DSC', 'DSCResourceKit', 'DSCResource') + + # A URL to the license for this module. + LicenseUri = 'https://github.com/dsccommunity/NetworkingDsc/blob/master/LICENSE' + + # A URL to the main website for this project. + ProjectUri = 'https://github.com/dsccommunity/NetworkingDsc' + + # A URL to an icon representing this module. + IconUri = 'https://dsccommunity.org/images/DSC_Logo_300p.png' + + # ReleaseNotes of this module + ReleaseNotes = '## [8.2.0] - 2020-10-16 + +### Changed + +- IPAddress + - Improved integration test structure. + +### Added + +- NetIPInterface + - Added `InterfaceMetric` parameter- fixes [Issue #473](https://github.com/PowerShell/xNetworking/issues/473). + +### Fixed + +- NetIPInterface + - Fix ''type mismatch for property'' issue when setting ''AdvertiseDefaultRoute'', + ''Advertising'', ''AutomaticMetric'', ''Dhcp'', ''DirectedMacWolPattern'', ''EcnMarking'', + ''ForceArpNdWolPattern'', ''Forwarding'', ''IgnoreDefaultRoutes'', ''ManagedAddressConfiguration'', + ''NeighborUnreachabilityDetection'', ''OtherStatefulConfiguration'', ''RouterDiscovery'', + ''WeakHostReceive'' or ''WeakHostSend'' - Fixes [Issue #470](https://github.com/dsccommunity/NetworkingDsc/issues/470). + +' + } # End of PSData hashtable + } # End of PrivateData hashtable +} + + + diff --git a/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/en-US/about_NetworkingDsc.help.txt b/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/en-US/about_NetworkingDsc.help.txt new file mode 100644 index 0000000..d847656 --- /dev/null +++ b/deployment/dsc/azshcihost/NetworkingDsc/8.2.0/en-US/about_NetworkingDsc.help.txt @@ -0,0 +1,29 @@ +TOPIC + about_NetworkingDsc + +SHORT DESCRIPTION + DSC resources for configuring common operating systems features, files and + settings. + +LONG DESCRIPTION + This module contains DSC resources for configuring common operating systems + features, files and settings. + +EXAMPLES + PS C:\> Get-DscResource -Module NetworkingDsc + +NOTE: + Thank you to the DSC Community contributors who contributed to this module by + writing code, sharing opinions, and provided feedback. + +TROUBLESHOOTING NOTE: + Go to the Github repository for read about issues, submit a new issue, and read + about new releases. https://github.com/dsccommunity/NetworkingDsc + +SEE ALSO + - https://github.com/dsccommunity/NetworkingDsc + +KEYWORDS + DSC, DscResource, Archive, Environment, Group, MSI, Package, File, + RemoteFile, Registry, Script, Service, User, + WindowsFeature, WindowsOptionalFeature, WindowsPackageCab, WindowsProcess diff --git a/deployment/dsc/azshcihost/StorageDsc/5.0.1/DSCResources/DSC_Disk/DSC_Disk.psm1 b/deployment/dsc/azshcihost/StorageDsc/5.0.1/DSCResources/DSC_Disk/DSC_Disk.psm1 new file mode 100644 index 0000000..fc1ec50 --- /dev/null +++ b/deployment/dsc/azshcihost/StorageDsc/5.0.1/DSCResources/DSC_Disk/DSC_Disk.psm1 @@ -0,0 +1,923 @@ +$modulePath = Join-Path -Path (Split-Path -Path (Split-Path -Path $PSScriptRoot -Parent) -Parent) -ChildPath 'Modules' + +# Import the Storage Common Module. +Import-Module -Name (Join-Path -Path $modulePath ` + -ChildPath (Join-Path -Path 'StorageDsc.Common' ` + -ChildPath 'StorageDsc.Common.psm1')) + +Import-Module -Name (Join-Path -Path $modulePath -ChildPath 'DscResource.Common') + +# Import Localization Strings. +$script:localizedData = Get-LocalizedData -DefaultUICulture 'en-US' + +<# + .SYNOPSIS + Returns the current state of the Disk and Partition. + + .PARAMETER DriveLetter + Specifies the preferred letter to assign to the disk volume. + + .PARAMETER DiskId + Specifies the disk identifier for the disk to modify. + + .PARAMETER DiskIdType + Specifies the identifier type the DiskId contains. Defaults to Number. + + .PARAMETER PartitionStyle + Specifies the partition style of the disk. Defaults to GPT. + This parameter is not used in Get-TargetResource. + + .PARAMETER Size + Specifies the size of new volume (use all available space on disk if not provided). + This parameter is not used in Get-TargetResource. + + .PARAMETER FSLabel + Specifies the volume label to assign to the volume. + This parameter is not used in Get-TargetResource. + + .PARAMETER AllocationUnitSize + Specifies the allocation unit size to use when formatting the volume. + This parameter is not used in Get-TargetResource. + + .PARAMETER FSFormat + Specifies the file system format of the new volume. + This parameter is not used in Get-TargetResource. + + .PARAMETER AllowDestructive + Specifies if potentially destructive operations may occur. + This parameter is not used in Get-TargetResource. + + .PARAMETER ClearDisk + Specifies if the disks partition schema should be removed entirely, even if data and OEM + partitions are present. Only possible with AllowDestructive enabled. + This parameter is not used in Get-TargetResource. +#> +function Get-TargetResource +{ + [CmdletBinding()] + [OutputType([System.Collections.Hashtable])] + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $DriveLetter, + + [Parameter(Mandatory = $true)] + [System.String] + $DiskId, + + [Parameter()] + [ValidateSet('Number', 'UniqueId', 'Guid', 'Location')] + [System.String] + $DiskIdType = 'Number', + + [Parameter()] + [ValidateSet('GPT', 'MBR')] + [System.String] + $PartitionStyle = 'GPT', + + [Parameter()] + [System.UInt64] + $Size, + + [Parameter()] + [System.String] + $FSLabel, + + [Parameter()] + [System.UInt32] + $AllocationUnitSize, + + [Parameter()] + [ValidateSet('NTFS', 'ReFS')] + [System.String] + $FSFormat = 'NTFS', + + [Parameter()] + [System.Boolean] + $AllowDestructive, + + [Parameter()] + [System.Boolean] + $ClearDisk + ) + + Write-Verbose -Message ( @( + "$($MyInvocation.MyCommand): " + $($script:localizedData.GettingDiskMessage -f $DiskIdType, $DiskId, $DriveLetter) + ) -join '' ) + + # Validate the DriveLetter parameter + $DriveLetter = Assert-DriveLetterValid -DriveLetter $DriveLetter + + # Get the Disk using the identifiers supplied + $disk = Get-DiskByIdentifier ` + -DiskId $DiskId ` + -DiskIdType $DiskIdType + + $partition = Get-Partition ` + -DriveLetter $DriveLetter ` + -ErrorAction SilentlyContinue | Select-Object -First 1 + + $volume = Get-Volume ` + -DriveLetter $DriveLetter ` + -ErrorAction SilentlyContinue + + $blockSize = (Get-CimInstance ` + -Query "SELECT BlockSize from Win32_Volume WHERE DriveLetter = '$($DriveLetter):'" ` + -ErrorAction SilentlyContinue).BlockSize + + return @{ + DiskId = $DiskId + DiskIdType = $DiskIdType + DriveLetter = $partition.DriveLetter + PartitionStyle = $disk.PartitionStyle + Size = $partition.Size + FSLabel = $volume.FileSystemLabel + AllocationUnitSize = $blockSize + FSFormat = $volume.FileSystem + } +} # Get-TargetResource + +<# + .SYNOPSIS + Initializes the Disk and Partition and assigns the drive letter. + + .PARAMETER DriveLetter + Specifies the preferred letter to assign to the disk volume. + + .PARAMETER DiskId + Specifies the disk identifier for the disk to modify. + + .PARAMETER DiskIdType + Specifies the identifier type the DiskId contains. Defaults to Number. + + .PARAMETER PartitionStyle + Specifies the partition style of the disk. Defaults to GPT. + + .PARAMETER Size + Specifies the size of new volume. Leave empty to use the remaining free space. + + .PARAMETER FSLabel + Specifies the volume label to assign to the volume. + + .PARAMETER AllocationUnitSize + Specifies the allocation unit size to use when formatting the volume. + + .PARAMETER FSFormat + Specifies the file system format of the new volume. + + .PARAMETER AllowDestructive + Specifies if potentially destructive operations may occur. + + .PARAMETER ClearDisk + Specifies if the disks partition schema should be removed entirely, even if data and OEM + partitions are present. Only possible with AllowDestructive enabled. +#> +function Set-TargetResource +{ + # Should process is called in a helper functions but not directly in Set-TargetResource + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSShouldProcess', '')] + [CmdletBinding(SupportsShouldProcess = $true)] + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $DriveLetter, + + [Parameter(Mandatory = $true)] + [System.String] + $DiskId, + + [Parameter()] + [ValidateSet('Number', 'UniqueId', 'Guid', 'Location')] + [System.String] + $DiskIdType = 'Number', + + [Parameter()] + [ValidateSet('GPT', 'MBR')] + [System.String] + $PartitionStyle = 'GPT', + + [Parameter()] + [System.UInt64] + $Size, + + [Parameter()] + [System.String] + $FSLabel, + + [Parameter()] + [System.UInt32] + $AllocationUnitSize, + + [Parameter()] + [ValidateSet('NTFS', 'ReFS')] + [System.String] + $FSFormat = 'NTFS', + + [Parameter()] + [System.Boolean] + $AllowDestructive, + + [Parameter()] + [System.Boolean] + $ClearDisk + ) + + Write-Verbose -Message ( @( + "$($MyInvocation.MyCommand): " + $($script:localizedData.SettingDiskMessage -f $DiskIdType, $DiskId, $DriveLetter) + ) -join '' ) + + # Validate the DriveLetter parameter + $DriveLetter = Assert-DriveLetterValid -DriveLetter $DriveLetter + + # Get the Disk using the identifiers supplied + $disk = Get-DiskByIdentifier ` + -DiskId $DiskId ` + -DiskIdType $DiskIdType + + if ($disk.IsOffline) + { + # Disk is offline, so bring it online + Write-Verbose -Message ( @( + "$($MyInvocation.MyCommand): " + $($script:localizedData.SetDiskOnlineMessage -f $DiskIdType, $DiskId) + ) -join '' ) + + $disk | Set-Disk -IsOffline $false + } # if + + if ($disk.IsReadOnly) + { + # Disk is read-only, so make it read/write + Write-Verbose -Message ( @( + "$($MyInvocation.MyCommand): " + $($script:localizedData.SetDiskReadWriteMessage -f $DiskIdType, $DiskId) + ) -join '' ) + + $disk | Set-Disk -IsReadOnly $false + } # if + + Write-Verbose -Message ( @( + "$($MyInvocation.MyCommand): " + $($script:localizedData.CheckingDiskPartitionStyleMessage -f $DiskIdType, $DiskId) + ) -join '' ) + + if ($AllowDestructive -and $ClearDisk -and $disk.PartitionStyle -ne 'RAW') + { + Write-Verbose -Message ( @( + "$($MyInvocation.MyCommand): " + $($script:localizedData.ClearingDiskMessage -f $DiskIdType, $DiskId) + ) -join '' ) + + $disk | Clear-Disk -RemoveData -RemoveOEM -Confirm:$true + + # Requery the disk + $disk = Get-DiskByIdentifier ` + -DiskId $DiskId ` + -DiskIdType $DiskIdType + } + + if ($disk.PartitionStyle -eq 'RAW') + { + Write-Verbose -Message ( @( + "$($MyInvocation.MyCommand): " + $($script:localizedData.InitializingDiskMessage -f $DiskIdType, $DiskId, $PartitionStyle) + ) -join '' ) + + $disk | Initialize-Disk -PartitionStyle $PartitionStyle + } + else + { + if ($disk.PartitionStyle -eq $PartitionStyle) + { + # The disk partition is already initialized with the correct partition style + Write-Verbose -Message ( @( + "$($MyInvocation.MyCommand): " + $($script:localizedData.DiskAlreadyInitializedMessage ` + -f $DiskIdType, $DiskId, $disk.PartitionStyle) + ) -join '' ) + + } + else + { + # This disk is initialized but with the incorrect partition style + New-InvalidOperationException ` + -Message ($script:localizedData.DiskInitializedWithWrongPartitionStyleError ` + -f $DiskIdType, $DiskId, $disk.PartitionStyle, $PartitionStyle) + } + } + + # Get the partitions on the disk + $partition = $disk | Get-Partition -ErrorAction SilentlyContinue + + # Check if the disk has an existing partition assigned to the drive letter + $assignedPartition = $partition | + Where-Object -Property DriveLetter -eq $DriveLetter + + # Check if existing partition already has file system on it + if ($null -eq $assignedPartition) + { + # There is no partiton with this drive letter + Write-Verbose -Message ( @( + "$($MyInvocation.MyCommand): " + $($script:localizedData.DriveNotFoundOnPartitionMessage ` + -f $DiskIdType, $DiskId, $DriveLetter) + ) -join '' ) + + # Are there any partitions defined on this disk? + if ($partition) + { + # There are partitions defined - identify if one matches the size required + if ($Size) + { + # Find the first basic partition matching the size + $partition = $partition | + Where-Object -FilterScript { $_.Type -eq 'Basic' -and $_.Size -eq $Size } | + Select-Object -First 1 + + if ($partition) + { + # A partition matching the required size was found + Write-Verbose -Message ($script:localizedData.MatchingPartitionFoundMessage ` + -f $DiskIdType, $DiskId, $partition.PartitionNumber) + } + else + { + # A partition matching the required size was not found + Write-Verbose -Message ($script:localizedData.MatchingPartitionNotFoundMessage ` + -f $DiskIdType, $DiskId) + } # if + } + else + { + <# + No size specified, so see if there is a partition that has a volume + matching the file system type that is not assigned to a drive letter. + #> + Write-Verbose -Message ($script:localizedData.MatchingPartitionNoSizeMessage ` + -f $DiskIdType, $DiskId) + + $searchPartitions = $partition | Where-Object -FilterScript { + $_.Type -eq 'Basic' -and -not [System.Char]::IsLetter($_.DriveLetter) + } + + $partition = $null + + foreach ($searchPartition in $searchPartitions) + { + # Look for the volume in the partition. + Write-Verbose -Message ($script:localizedData.SearchForVolumeMessage ` + -f $DiskIdType, $DiskId, $searchPartition.PartitionNumber, $FSFormat) + + $searchVolumes = $searchPartition | Get-Volume + + $volumeMatch = $searchVolumes | Where-Object -FilterScript { + $_.FileSystem -eq $FSFormat + } + + if ($volumeMatch) + { + <# + Found a partition with a volume that matches file system + type and not assigned a drive letter. + #> + $partition = $searchPartition + + Write-Verbose -Message ($script:localizedData.VolumeFoundMessage ` + -f $DiskIdType, $DiskId, $searchPartition.PartitionNumber, $FSFormat) + + break + } # if + } # foreach + } # if + } # if + + # Do we need to create a new partition? + if (-not $partition) + { + # Attempt to create a new partition + $partitionParams = @{ + DriveLetter = $DriveLetter + } + + if ($Size) + { + # Use only a specific size + Write-Verbose -Message ( @( + "$($MyInvocation.MyCommand): " + $($script:localizedData.CreatingPartitionMessage ` + -f $DiskIdType, $DiskId, $DriveLetter, "$($Size/1KB) KB") + ) -join '' ) + + $partitionParams['Size'] = $Size + } + else + { + # Use the entire disk + Write-Verbose -Message ( @( + "$($MyInvocation.MyCommand): " + $($script:localizedData.CreatingPartitionMessage ` + -f $DiskIdType, $DiskId, $DriveLetter, 'all free space') + ) -join '' ) + + $partitionParams['UseMaximumSize'] = $true + } # if + + # Create the partition. + $partition = $disk | New-Partition @partitionParams + + <# + After creating the partition it can take a few seconds for it to become writeable + Wait for up to 30 seconds for the parition to become writeable + #> + $timeAtStart = Get-Date + $minimumTimeToWait = $timeAtStart + (New-Timespan -Second 3) + $maximumTimeToWait = $timeAtStart + (New-Timespan -Second 30) + + while (($partitionstate.IsReadOnly -and (Get-Date) -lt $maximumTimeToWait) ` + -or ((Get-Date) -lt $minimumTimeToWait)) + { + Write-Verbose -Message ( @( + "$($MyInvocation.MyCommand): " + ($script:localizedData.NewPartitionIsReadOnlyMessage ` + -f $DiskIdType, $DiskId, $partition.PartitionNumber) + ) -join '' ) + + Start-Sleep -Seconds 1 + + # Pull the partition details again to check if it is readonly + $partitionstate = $partition | Get-Partition + } # while + } # if + + if ($partition.IsReadOnly) + { + # The partition is still readonly - throw an exception + New-InvalidOperationException ` + -Message ($script:localizedData.NewParitionIsReadOnlyError ` + -f $DiskIdType, $DiskId, $partition.PartitionNumber) + } # if + + $assignDriveLetter = $true + } + else + { + # The disk already has a partition on it that is assigned to the Drive Letter + Write-Verbose -Message ( @( + "$($MyInvocation.MyCommand): " + $($script:localizedData.PartitionAlreadyAssignedMessage ` + -f $DriveLetter, $assignedPartition.PartitionNumber) + ) -join '' ) + + $assignDriveLetter = $false + + $supportedSize = $assignedPartition | Get-PartitionSupportedSize + + <# + If the parition size was not specified then try and make the partition + use all possible space on the disk. + #> + if (-not ($PSBoundParameters.ContainsKey('Size'))) + { + $Size = $supportedSize.SizeMax + } + + if ($assignedPartition.Size -ne $Size) + { + # A patition resize is required + if ($AllowDestructive) + { + if ($FSFormat -eq 'ReFS') + { + Write-Warning -Message ( @( + "$($MyInvocation.MyCommand): " + $($script:localizedData.ResizeRefsNotPossibleMessage ` + -f $DriveLetter, $assignedPartition.Size, $Size) + ) -join '' ) + + } + else + { + Write-Verbose -Message ( @( + "$($MyInvocation.MyCommand): " + $($script:localizedData.SizeMismatchCorrectionMessage ` + -f $DriveLetter, $assignedPartition.Size, $Size) + ) -join '' ) + + if ($Size -gt $supportedSize.SizeMax) + { + New-InvalidArgumentException -Message ( @( + "$($MyInvocation.MyCommand): " + $($script:localizedData.FreeSpaceViolationError ` + -f $DriveLetter, $assignedPartition.Size, $Size, $supportedSize.SizeMax) + ) -join '' ) -ArgumentName 'Size' -ErrorAction Stop + } + + $assignedPartition | Resize-Partition -Size $Size + } + } + else + { + # A partition resize was required but is not allowed + Write-Warning -Message ( @( + "$($MyInvocation.MyCommand): " + $($script:localizedData.ResizeNotAllowedMessage ` + -f $DriveLetter, $assignedPartition.Size, $Size) + ) -join '' ) + } + } + } + + # Get the Volume on the partition + $volume = $partition | Get-Volume + + # Is the volume already formatted? + if ($volume.FileSystem -eq '') + { + # The volume is not formatted + $formatVolumeParameters = @{ + FileSystem = $FSFormat + Confirm = $false + } + + if ($FSLabel) + { + # Set the File System label on the new volume + $formatVolumeParameters['NewFileSystemLabel'] = $FSLabel + } # if + + if ($AllocationUnitSize) + { + # Set the Allocation Unit Size on the new volume + $formatVolumeParameters['AllocationUnitSize'] = $AllocationUnitSize + } # if + + Write-Verbose -Message ( @( + "$($MyInvocation.MyCommand): " + $($script:localizedData.FormattingVolumeMessage -f $formatVolumeParameters.FileSystem) + ) -join '' ) + + # Format the volume + $volume = $partition | Format-Volume @formatVolumeParameters + } + else + { + # The volume is already formatted + if ($PSBoundParameters.ContainsKey('FSFormat')) + { + # Check the filesystem format + $fileSystem = $volume.FileSystem + if ($fileSystem -ne $FSFormat) + { + # The file system format does not match + Write-Verbose -Message ( @( + "$($MyInvocation.MyCommand): " + $($script:localizedData.FileSystemFormatMismatch ` + -f $DriveLetter, $fileSystem, $FSFormat) + ) -join '' ) + + if ($AllowDestructive) + { + Write-Verbose -Message ( @( + "$($MyInvocation.MyCommand): " + $($script:localizedData.VolumeFormatInProgressMessage ` + -f $DriveLetter, $fileSystem, $FSFormat) + ) -join '' ) + + $formatParam = @{ + FileSystem = $FSFormat + Force = $true + } + + if ($PSBoundParameters.ContainsKey('AllocationUnitSize')) + { + $formatParam.Add('AllocationUnitSize', $AllocationUnitSize) + } + + $Volume | Format-Volume @formatParam + } + } # if + } # if + + # Check the volume label + if ($PSBoundParameters.ContainsKey('FSLabel')) + { + # The volume should have a label assigned + if ($volume.FileSystemLabel -ne $FSLabel) + { + # The volume lable needs to be changed because it is different. + Write-Verbose -Message ( @( + "$($MyInvocation.MyCommand): " + $($script:localizedData.ChangingVolumeLabelMessage ` + -f $DriveLetter, $FSLabel) + ) -join '' ) + + $volume | Set-Volume -NewFileSystemLabel $FSLabel + } # if + } # if + } # if + + # Assign the Drive Letter if it isn't assigned + if ($assignDriveLetter -and ($partition.DriveLetter -ne $DriveLetter)) + { + Write-Verbose -Message ( @( + "$($MyInvocation.MyCommand): " + $($script:localizedData.AssigningDriveLetterMessage -f $DriveLetter) + ) -join '' ) + + $null = $partition | Set-Partition -NewDriveLetter $DriveLetter + + Write-Verbose -Message ( @( + "$($MyInvocation.MyCommand): " + $($script:localizedData.SuccessfullyInitializedMessage -f $DriveLetter) + ) -join '' ) + } # if +} # Set-TargetResource + +<# + .SYNOPSIS + Tests if the disk is initialized, the partion exists and the drive letter is assigned. + + .PARAMETER DriveLetter + Specifies the preferred letter to assign to the disk volume. + + .PARAMETER DiskId + Specifies the disk identifier for the disk to modify. + + .PARAMETER DiskIdType + Specifies the identifier type the DiskId contains. Defaults to Number. + + .PARAMETER PartitionStyle + Specifies the partition style of the disk. Defaults to GPT. + + .PARAMETER Size + Specifies the size of new volume. Leave empty to use the remaining free space. + + .PARAMETER FSLabel + Specifies the volume label to assign to the volume. + + .PARAMETER AllocationUnitSize + Specifies the allocation unit size to use when formatting the volume. + + .PARAMETER FSFormat + Specifies the file system format of the new volume. + + .PARAMETER AllowDestructive + Specifies if potentially destructive operations may occur. + + .PARAMETER ClearDisk + Specifies if the disks partition schema should be removed entirely, even if data and OEM + partitions are present. Only possible with AllowDestructive enabled. +#> +function Test-TargetResource +{ + [CmdletBinding()] + [OutputType([System.Boolean])] + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $DriveLetter, + + [Parameter(Mandatory = $true)] + [System.String] + $DiskId, + + [Parameter()] + [ValidateSet('Number', 'UniqueId', 'Guid', 'Location')] + [System.String] + $DiskIdType = 'Number', + + [Parameter()] + [ValidateSet('GPT', 'MBR')] + [System.String] + $PartitionStyle = 'GPT', + + [Parameter()] + [System.UInt64] + $Size, + + [Parameter()] + [System.String] + $FSLabel, + + [Parameter()] + [System.UInt32] + $AllocationUnitSize, + + [Parameter()] + [ValidateSet('NTFS', 'ReFS')] + [System.String] + $FSFormat = 'NTFS', + + [Parameter()] + [System.Boolean] + $AllowDestructive, + + [Parameter()] + [System.Boolean] + $ClearDisk + ) + + Write-Verbose -Message ( @( + "$($MyInvocation.MyCommand): " + $($script:localizedData.TestingDiskMessage -f $DiskIdType, $DiskId, $DriveLetter) + ) -join '' ) + + # Validate the DriveLetter parameter + $DriveLetter = Assert-DriveLetterValid -DriveLetter $DriveLetter + + Write-Verbose -Message ( @( + "$($MyInvocation.MyCommand): " + $($script:localizedData.CheckDiskInitializedMessage -f $DiskIdType, $DiskId) + ) -join '' ) + + # Get the Disk using the identifiers supplied + $disk = Get-DiskByIdentifier ` + -DiskId $DiskId ` + -DiskIdType $DiskIdType + + if (-not $disk) + { + Write-Verbose -Message ( @( + "$($MyInvocation.MyCommand): " + $($script:localizedData.DiskNotFoundMessage -f $DiskIdType, $DiskId) + ) -join '' ) + + return $false + } # if + + if ($disk.IsOffline) + { + Write-Verbose -Message ( @( + "$($MyInvocation.MyCommand): " + $($script:localizedData.DiskNotOnlineMessage -f $DiskIdType, $DiskId) + ) -join '' ) + + return $false + } # if + + if ($disk.IsReadOnly) + { + Write-Verbose -Message ( @( + "$($MyInvocation.MyCommand): " + $($script:localizedData.DiskReadOnlyMessage ` + -f $DiskIdType, $DiskId) + ) -join '' ) + + return $false + } # if + + if ($disk.PartitionStyle -ne $PartitionStyle) + { + Write-Verbose -Message ( @( + "$($MyInvocation.MyCommand): " + $($script:localizedData.DiskPartitionStyleNotMatchMessage ` + -f $DiskIdType, $DiskId, $disk.PartitionStyle, $PartitionStyle) + ) -join '' ) + + if ($disk.PartitionStyle -eq 'RAW' -or ($AllowDestructive -and $ClearDisk)) + { + return $false + } + else + { + # This disk is initialized but with the incorrect partition style + New-InvalidOperationException ` + -Message ($script:localizedData.DiskInitializedWithWrongPartitionStyleError ` + -f $DiskIdType, $DiskId, $disk.PartitionStyle, $PartitionStyle) + } + } # if + + $partition = Get-Partition ` + -DriveLetter $DriveLetter ` + -ErrorAction SilentlyContinue | Select-Object -First 1 + + if ($partition.DriveLetter -ne $DriveLetter) + { + Write-Verbose -Message ( @( + "$($MyInvocation.MyCommand): " + $($script:localizedData.DriveLetterNotFoundMessage -f $DriveLetter) + ) -join '' ) + + return $false + } # if + + # Check the partition size + if ($partition -and -not ($PSBoundParameters.ContainsKey('Size'))) + { + $supportedSize = ($partition | Get-PartitionSupportedSize) + + <# + If the difference in size between the supported partition size + and the current partition size is less than 1MB then set the + desired partition size to the current size. This will prevent + any size difference less than 1MB from trying to contiuously + resize. See https://github.com/dsccommunity/StorageDsc/issues/181 + #> + if (($supportedSize.SizeMax - $partition.Size) -lt 1MB) + { + $Size = $partition.Size + } + else + { + $Size = $supportedSize.SizeMax + } + } + + if ($Size) + { + if ($partition.Size -ne $Size) + { + # The partition size mismatches + if ($AllowDestructive) + { + Write-Verbose -Message ( @( + "$($MyInvocation.MyCommand): " + $($script:localizedData.SizeMismatchWithAllowDestructiveMessage ` + -f $DriveLetter, $Partition.Size, $Size) + ) -join '' ) + + return $false + } + else + { + Write-Verbose -Message ( @( + "$($MyInvocation.MyCommand): " + $($script:localizedData.SizeMismatchMessage ` + -f $DriveLetter, $Partition.Size, $Size) + ) -join '' ) + } + } # if + } # if + + $blockSize = (Get-CimInstance ` + -Query "SELECT BlockSize from Win32_Volume WHERE DriveLetter = '$($DriveLetter):'" ` + -ErrorAction SilentlyContinue).BlockSize + + if ($blockSize -gt 0 -and $AllocationUnitSize -ne 0) + { + if ($AllocationUnitSize -ne $blockSize) + { + # The allocation unit size mismatches + Write-Verbose -Message ( @( + "$($MyInvocation.MyCommand): " + $($script:localizedData.AllocationUnitSizeMismatchMessage ` + -f $DriveLetter, $($blockSize.BlockSize / 1KB), $($AllocationUnitSize / 1KB)) + ) -join '' ) + + if ($AllowDestructive) + { + return $false + } + } # if + } # if + + # Get the volume so the properties can be checked + $volume = Get-Volume ` + -DriveLetter $DriveLetter ` + -ErrorAction SilentlyContinue + + if ($PSBoundParameters.ContainsKey('FSFormat')) + { + # Check the filesystem format + $fileSystem = $volume.FileSystem + if ($fileSystem -ne $FSFormat) + { + Write-Verbose -Message ( @( + "$($MyInvocation.MyCommand): " + $($script:localizedData.FileSystemFormatMismatch ` + -f $DriveLetter, $fileSystem, $FSFormat) + ) -join '' ) + + if ($AllowDestructive) + { + return $false + } + } # if + } # if + + if ($PSBoundParameters.ContainsKey('FSLabel')) + { + # Check the volume label + $label = $volume.FileSystemLabel + if ($label -ne $FSLabel) + { + # The assigned volume label is different and needs updating + Write-Verbose -Message ( @( + "$($MyInvocation.MyCommand): " + $($script:localizedData.DriveLabelMismatch ` + -f $DriveLetter, $label, $FSLabel) + ) -join '' ) + + return $false + } # if + } # if + + return $true +} # Test-TargetResource + +Export-ModuleMember -Function *-TargetResource diff --git a/deployment/dsc/azshcihost/StorageDsc/5.0.1/DSCResources/DSC_Disk/DSC_Disk.schema.mof b/deployment/dsc/azshcihost/StorageDsc/5.0.1/DSCResources/DSC_Disk/DSC_Disk.schema.mof new file mode 100644 index 0000000..39bc860 --- /dev/null +++ b/deployment/dsc/azshcihost/StorageDsc/5.0.1/DSCResources/DSC_Disk/DSC_Disk.schema.mof @@ -0,0 +1,15 @@ + +[ClassVersion("1.0.0.0"), FriendlyName("Disk")] +class DSC_Disk : OMI_BaseResource +{ + [Key, Description("Specifies the identifier for which disk to modify.")] String DriveLetter; + [Required, Description("Specifies the disk identifier for the disk to modify.")] String DiskId; + [Write, Description("Specifies the identifier type the DiskId contains. Defaults to Number."), ValueMap{"Number","UniqueId","Guid","Location"}, Values{"Number","UniqueId","Guid","Location"}] String DiskIdType; + [Write, Description("Specifies the partition style of the disk. Defaults to GPT."), ValueMap{"MBR","GPT"}, Values{"MBR","GPT"}] String PartitionStyle; + [Write, Description("Specifies the size of new volume. Leave empty to use the remaining free space.")] Uint64 Size; + [Write, Description("Define volume label if required.")] String FSLabel; + [Write, Description("Specifies the allocation unit size to use when formatting the volume.")] Uint32 AllocationUnitSize; + [Write, Description("Specifies the file system format of the new volume."), ValueMap{"NTFS","ReFS"}, Values{"NTFS","ReFS"}] String FSFormat; + [Write, Description("Specifies if potentially destructive operations may occur.")] Boolean AllowDestructive; + [Write, Description("Specifies if the disks partition schema should be removed entirely, even if data and OEM partitions are present. Only possible with AllowDestructive enabled.")] Boolean ClearDisk; +}; diff --git a/deployment/dsc/azshcihost/StorageDsc/5.0.1/DSCResources/DSC_Disk/README.md b/deployment/dsc/azshcihost/StorageDsc/5.0.1/DSCResources/DSC_Disk/README.md new file mode 100644 index 0000000..882fc26 --- /dev/null +++ b/deployment/dsc/azshcihost/StorageDsc/5.0.1/DSCResources/DSC_Disk/README.md @@ -0,0 +1,68 @@ +# Description + +The resource is used to initialize, format and mount the partition/volume as a drive +letter. +The disk to add the partition/volume to is selected by specifying the _DiskId_ and +optionally _DiskIdType_. +The _DiskId_ value can be a _Disk Number_, _Unique Id_, _Guid_ or _Location_. + +**Important: The _Disk Number_ is not a reliable method of selecting a disk because +it has been shown to change between reboots in some environments. +It is recommended to use the _Unique Id_ if possible.** + +The _Disk Number_, _Unique Id_, _Guid_ and _Location_ can be identified for a +disk by using the PowerShell command: + +```powershell +Get-Disk | Select-Object -Property FriendlyName,DiskNumber,UniqueId,Guid,Location +``` + +Note: The _Guid_ identifier method of specifying disks is only supported as an +identifier for disks with `GPT` partition table format. If the disk is `RAW` +(e.g. the disk has been initialized) then the _Guid_ identifier method can not +be used. This is because the _Guid_ for a disk is only assigned once the partition +table for the disk has been created. + +## Known Issues + +### Defragsvc Conflict + +The 'defragsvc' service ('Optimize Drives') may cause the following errors when +enabled with this resource. The following error may occur when testing the state +of the resource: + +```text +PartitionSupportedSize ++ CategoryInfo : NotSpecified: (StorageWMI:) [], CimException ++ FullyQualifiedErrorId : StorageWMI 4,Get-PartitionSupportedSize ++ PSComputerName : localhost +``` + +The 'defragsvc' service should be stopped and set to manual start up to prevent +this error. Use the `Service` resource in either the 'xPSDesiredStateConfgiuration' +or 'PSDSCResources' resource module to set the 'defragsvc' service is always +stopped and set to manual start up. + +### Null Location + +The _Location_ for a disk may be `null` for some types of disk, +e.g. file-based virtual disks. Physical disks or Virtual disks provided via a +hypervisor or other hardware virtualization platform should not be affected. + +### Maximum Supported Partition Size + +On some disks the _maximum supported partition size_ may differ from the actual +size of a partition created when specifying the maximum size. This difference +in reported size is always less than **1MB**, so if the reported _maximum supported +partition size_ is less than **1MB** then the partition will be considered to be +in the correct state. This is a work around for [this issue](https://windowsserver.uservoice.com/forums/301869-powershell/suggestions/36967870-get-partitionsupportedsize-and-msft-partition-clas) +that has been reported on user voice and also discussed in [issue #181](https://github.com/dsccommunity/StorageDsc/issues/181). + +### ReFS on Windows Server 2019 + +On Windows Server 2019 (build 17763 and above), `Format-Volume` throws an +'Invalid Parameter' exception when called with `ReFS` as the `FileSystem` +parameter. This results in an 'Invalid Parameter' exception being thrown +in the `Set` in the 'Disk' resource. +There is currently no known work around for this issue. It is being tracked +in [issue #227](https://github.com/dsccommunity/StorageDsc/issues/227). diff --git a/deployment/dsc/azshcihost/StorageDsc/5.0.1/DSCResources/DSC_Disk/en-US/DSC_Disk.strings.psd1 b/deployment/dsc/azshcihost/StorageDsc/5.0.1/DSCResources/DSC_Disk/en-US/DSC_Disk.strings.psd1 new file mode 100644 index 0000000..257c42d --- /dev/null +++ b/deployment/dsc/azshcihost/StorageDsc/5.0.1/DSCResources/DSC_Disk/en-US/DSC_Disk.strings.psd1 @@ -0,0 +1,42 @@ +ConvertFrom-StringData @' + GettingDiskMessage = Getting disk with {0} '{1}' status for drive letter '{2}'. + SettingDiskMessage = Setting disk with {0} '{1}' status for drive letter '{2}'. + SetDiskOnlineMessage = Setting disk with {0} '{1}' online. + SetDiskReadWriteMessage = Setting disk with {0} '{1}' to read/write. + CheckingDiskPartitionStyleMessage = Checking disk with {0} '{1}' partition style. + InitializingDiskMessage = Initializing disk with {0} '{1}' as '{2}'. + DiskAlreadyInitializedMessage = Disk with {0} '{1}' is already initialized with '{2}'. + CreatingPartitionMessage = Creating partition on disk with {0} '{1}' with drive letter '{2}' using {3}. + FormattingVolumeMessage = Formatting the volume as '{0}'. + SuccessfullyInitializedMessage = Successfully initialized '{0}'. + AssigningDriveLetterMessage = Assigning drive letter '{0}'. + ChangingVolumeLabelMessage = Changing volume '{0}' label to '{1}'. + NewPartitionIsReadOnlyMessage = New partition '{2}' on disk with {0} '{1}' is readonly. Waiting for it to become writable. + TestingDiskMessage = Testing disk with {0} '{1}' status for drive letter '{2}'. + CheckDiskInitializedMessage = Checking if disk with {0} '{1}' is initialized. + DiskNotFoundMessage = Disk with {0} '{1}' was not found. + DiskNotOnlineMessage = Disk with {0} '{1}' is not online. + DiskReadOnlyMessage = Disk with {0} '{1}' is readonly. + DiskPartitionStyleNotMatchMessage = Disk with {0} '{1}' is initialized with partition style '{2}' but '{3}' is required. + DiskInitializedWithWrongPartitionStyleError = Disk with {0} '{1}' is already initialized with partition style '{2}' but '{3}' is required. Set AllowDestructive and ClearDisk to $true to allow disk to be reinitialized. + DriveLetterNotFoundMessage = Drive {0} was not found. + SizeMismatchMessage = Partition assigned to drive {0} has size {1}, which does not match expected size {2}. Set AllowDestructive to $true to enable resizing of partition. + SizeMismatchWithAllowDestructiveMessage = Partition assigned to drive {0} has size {1}, which does not match expected size {2}. + AllocationUnitSizeMismatchMessage = Volume assigned to drive {0} has allocation unit size {1} KB does not match expected allocation unit size {2} KB. + FileSystemFormatMismatch = Volume assigned to drive {0} filesystem format '{1}' does not match expected format '{2}'. + DriveLabelMismatch = Volume assigned to drive {0} label '{1}' does not match expected label '{2}'. + PartitionAlreadyAssignedMessage = Partition '{1}' is already assigned as drive {0}. + MatchingPartitionNotFoundMessage = Disk with {0} '{1}' already contains partitions, but none match required size. + MatchingPartitionNoSizeMessage = Disk with {0} '{1}' already contains partitions, but size parameter is not specified. + SearchForVolumeMessage = Searching for {3} volume with no drive letter on partition '{2}' disk with {0} '{1}'. + VolumeFoundMessage = Found {3} volume with no drive letter on partition '{2}' disk with {0} '{1}'. + MatchingPartitionFoundMessage = Disk with {0} '{1}' already contains partitions, and partition '{2}' matches required size. + DriveNotFoundOnPartitionMessage = Disk with {0} '{1}' does not contain a partition assigned to drive letter '{2}'. + ClearingDiskMessage = Clearing disk with {0} '{1}' of all existing partitions and volumes. + NewParitionIsReadOnlyError = New partition '{2}' on disk with {0} '{1}' did not become writable in the expected time. + VolumeFormatInProgressMessage = Switch AllowDestructive is specified. Attempting to format volume on {0} with '{2}', was '{1}'. + SizeMismatchCorrectionMessage = Switch AllowDestructive is specified. Attempting to resize partition {0} from {1} to {2}. + FreeSpaceViolationError = Attempted to resize partition {0} from {1} to {2} while maximum allowed size was {3}. + ResizeRefsNotPossibleMessage = Skipping resize of {0} from {1} to {2}. Resizing ReFS partitions is currently not possible. + ResizeNotAllowedMessage = Skipping resize of {0} from {1} to {2}. AllowDestructive is not set to $true. +'@ diff --git a/deployment/dsc/azshcihost/StorageDsc/5.0.1/DSCResources/DSC_DiskAccessPath/DSC_DiskAccessPath.psm1 b/deployment/dsc/azshcihost/StorageDsc/5.0.1/DSCResources/DSC_DiskAccessPath/DSC_DiskAccessPath.psm1 new file mode 100644 index 0000000..3d78b4a --- /dev/null +++ b/deployment/dsc/azshcihost/StorageDsc/5.0.1/DSCResources/DSC_DiskAccessPath/DSC_DiskAccessPath.psm1 @@ -0,0 +1,835 @@ +$modulePath = Join-Path -Path (Split-Path -Path (Split-Path -Path $PSScriptRoot -Parent) -Parent) -ChildPath 'Modules' + +# Import the Storage Common Module. +Import-Module -Name (Join-Path -Path $modulePath ` + -ChildPath (Join-Path -Path 'StorageDsc.Common' ` + -ChildPath 'StorageDsc.Common.psm1')) + +Import-Module -Name (Join-Path -Path $modulePath -ChildPath 'DscResource.Common') + +# Import Localization Strings. +$script:localizedData = Get-LocalizedData -DefaultUICulture 'en-US' + +<# + .SYNOPSIS + Returns the current state of the Disk and Partition. + + .PARAMETER AccessPath + Specifies the access path folder to the assign the disk volume to + + .PARAMETER NoDefaultDriveLetter + Prevents a default drive letter from being assigned to a newly created partition. Defaults to True. + + .PARAMETER DiskId + Specifies the disk identifier for the disk to modify. + + .PARAMETER DiskIdType + Specifies the identifier type the DiskId contains. Defaults to Number. + + .PARAMETER Size + Specifies the size of new volume (use all available space on disk if not provided). + + .PARAMETER FSLabel + Specifies the volume label to assign to the volume. + + .PARAMETER AllocationUnitSize + Specifies the allocation unit size to use when formatting the volume. + + .PARAMETER FSFormat + Specifies the file system format of the new volume. +#> +function Get-TargetResource +{ + [CmdletBinding()] + [OutputType([System.Collections.Hashtable])] + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $AccessPath, + + [Parameter()] + [System.Boolean] + $NoDefaultDriveLetter = $true, + + [Parameter(Mandatory = $true)] + [System.String] + $DiskId, + + [Parameter()] + [ValidateSet('Number', 'UniqueId', 'Guid', 'Location')] + [System.String] + $DiskIdType = 'Number', + + [Parameter()] + [System.UInt64] + $Size, + + [Parameter()] + [System.String] + $FSLabel, + + [Parameter()] + [System.UInt32] + $AllocationUnitSize, + + [Parameter()] + [ValidateSet('NTFS', 'ReFS')] + [System.String] + $FSFormat = 'NTFS' + ) + + Write-Verbose -Message ( @( + "$($MyInvocation.MyCommand): " + $($script:localizedData.GettingDiskMessage -f $DiskIdType, $DiskId, $AccessPath) + ) -join '' ) + + # Validate the AccessPath parameter adding a trailing slash + $AccessPath = Assert-AccessPathValid -AccessPath $AccessPath -Slash + + # Get the Disk using the identifiers supplied + $disk = Get-DiskByIdentifier ` + -DiskId $DiskId ` + -DiskIdType $DiskIdType + + # Get the partitions on the disk + $partition = $disk | Get-Partition -ErrorAction SilentlyContinue + + # Check if the disk has an existing partition assigned to the access path + $assignedPartition = $partition | + Where-Object -Property AccessPaths -Contains -Value $AccessPath + + $volume = $assignedPartition | Get-Volume + + $fileSystem = $volume.FileSystem + $FSLabel = $volume.FileSystemLabel + + # Prepare the AccessPath used in the CIM query (replaces '\' with '\\') + $queryAccessPath = $AccessPath -replace '\\', '\\' + + $blockSize = (Get-CimInstance ` + -Query "SELECT BlockSize from Win32_Volume WHERE Name = '$queryAccessPath'" ` + -ErrorAction SilentlyContinue).BlockSize + + $returnValue = @{ + DiskId = $DiskId + DiskIdType = $DiskIdType + AccessPath = $AccessPath + NoDefaultDriveLetter = $assignedPartition.NoDefaultDriveLetter + Size = $assignedPartition.Size + FSLabel = $FSLabel + AllocationUnitSize = $blockSize + FSFormat = $fileSystem + } + + return $returnValue +} # Get-TargetResource + +<# + .SYNOPSIS + Initializes the Disk and Partition and assigns the access path. + + .PARAMETER AccessPath + Specifies the access path folder to the assign the disk volume to + + .PARAMETER NoDefaultDriveLetter + Prevents a default drive letter from being assigned to a newly created partition. Defaults to True. + + .PARAMETER DiskId + Specifies the disk identifier for the disk to modify. + + .PARAMETER DiskIdType + Specifies the identifier type the DiskId contains. Defaults to Number. + + .PARAMETER Size + Specifies the size of new volume (use all available space on disk if not provided). + + .PARAMETER FSLabel + Specifies the volume label to assign to the volume. + + .PARAMETER AllocationUnitSize + Specifies the allocation unit size to use when formatting the volume. + + .PARAMETER FSFormat + Specifies the file system format of the new volume. +#> +function Set-TargetResource +{ + # Should process is called in a helper functions but not directly in Set-TargetResource + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSShouldProcess', '')] + [CmdletBinding(SupportsShouldProcess = $true)] + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $AccessPath, + + [Parameter()] + [System.Boolean] + $NoDefaultDriveLetter = $true, + + [Parameter(Mandatory = $true)] + [System.String] + $DiskId, + + [Parameter()] + [ValidateSet('Number', 'UniqueId', 'Guid', 'Location')] + [System.String] + $DiskIdType = 'Number', + + [Parameter()] + [System.UInt64] + $Size, + + [Parameter()] + [System.String] + $FSLabel, + + [Parameter()] + [System.UInt32] + $AllocationUnitSize, + + [Parameter()] + [ValidateSet('NTFS', 'ReFS')] + [System.String] + $FSFormat = 'NTFS' + ) + + Write-Verbose -Message ( @( + "$($MyInvocation.MyCommand): " + $($script:localizedData.SettingDiskMessage -f $DiskIdType, $DiskId, $AccessPath) + ) -join '' ) + + <# + This call is required to force a refresh of the list of drives in + the disk subsystem. If this refresh does not occur then the list of + disks may not be up-to-date, resulting in an error occuring when the + access path is mounted. The result of the test is not used and is discarded. + #> + $null = Test-AccessPathInPSDrive -AccessPath $AccessPath + + # Validate the AccessPath parameter adding a trailing slash + $AccessPath = Assert-AccessPathValid -AccessPath $AccessPath -Slash + + # Get the Disk using the identifiers supplied + $disk = Get-DiskByIdentifier ` + -DiskId $DiskId ` + -DiskIdType $DiskIdType + + if ($disk.IsOffline) + { + # Disk is offline, so bring it online + Write-Verbose -Message ( @( + "$($MyInvocation.MyCommand): " + $($script:localizedData.SetDiskOnlineMessage -f $DiskIdType, $DiskId) + ) -join '' ) + + $disk | Set-Disk -IsOffline $false + } # if + + if ($disk.IsReadOnly) + { + # Disk is read-only, so make it read/write + Write-Verbose -Message ( @( + "$($MyInvocation.MyCommand): " + $($script:localizedData.SetDiskReadWriteMessage -f $DiskIdType, $DiskId) + ) -join '' ) + + $disk | Set-Disk -IsReadOnly $false + } # if + + Write-Verbose -Message ( @( + "$($MyInvocation.MyCommand): " + $($script:localizedData.CheckingDiskPartitionStyleMessage -f $DiskIdType, $DiskId) + ) -join '' ) + + switch ($disk.PartitionStyle) + { + 'RAW' + { + # The disk partition table is not yet initialized, so initialize it with GPT + Write-Verbose -Message ( @( + "$($MyInvocation.MyCommand): " + $($script:localizedData.InitializingDiskMessage -f $DiskIdType, $DiskId) + ) -join '' ) + + $disk | Initialize-Disk ` + -PartitionStyle 'GPT' + break + } # 'RAW' + + 'GPT' + { + # The disk partition is already initialized with GPT. + Write-Verbose -Message ( @( + "$($MyInvocation.MyCommand): " + $($script:localizedData.DiskAlreadyInitializedMessage -f $DiskIdType, $DiskId) + ) -join '' ) + break + } # 'GPT' + + default + { + # This disk is initialized but not as GPT - so raise an exception. + New-InvalidOperationException ` + -Message ($script:localizedData.DiskAlreadyInitializedError -f ` + $DiskIdType, $DiskId, $Disk.PartitionStyle) + } # default + } # switch + + # Get the partitions on the disk + $partition = $disk | Get-Partition -ErrorAction SilentlyContinue + + # Check if the disk has an existing partition assigned to the access path + $assignedPartition = $partition | + Where-Object -Property AccessPaths -Contains -Value $AccessPath + + if ($null -eq $assignedPartition) + { + # There is no partiton with this access path + Write-Verbose -Message ( @( + "$($MyInvocation.MyCommand): " + $($script:localizedData.AccessPathNotFoundOnPartitionMessage -f $DiskIdType, $DiskId, $AccessPath) + ) -join '' ) + + # Are there any partitions defined on this disk? + if ($partition) + { + # There are partitions defined - identify if one matches the size required + if ($Size) + { + # Find the first basic partition matching the size + $partition = $partition | + Where-Object -Filter { $_.Type -eq 'Basic' -and $_.Size -eq $Size } | + Select-Object -First 1 + + if ($partition) + { + # A partition matching the required size was found + Write-Verbose -Message ($script:localizedData.MatchingPartitionFoundMessage -f ` + $DiskIdType, $DiskId, $partition.PartitionNumber) + } + else + { + # A partition matching the required size was not found + Write-Verbose -Message ($script:localizedData.MatchingPartitionNotFoundMessage -f ` + $DiskIdType, $DiskId) + } # if + } + else + { + <# + No size specified, so see if there is a partition that has a volume + matching the file system type that is not assigned to an access path. + #> + Write-Verbose -Message ($script:localizedData.MatchingPartitionNoSizeMessage -f ` + $DiskIdType, $DiskId) + + $searchPartitions = $partition | Where-Object -FilterScript { + $_.Type -eq 'Basic' -and -not (Test-AccessPathAssignedToLocal -AccessPath $_.AccessPaths) + } + + $partition = $null + + foreach ($searchPartition in $searchPartitions) + { + # Look for the volume in the partition. + Write-Verbose -Message ($script:localizedData.SearchForVolumeMessage -f ` + $DiskIdType, $DiskId, $searchPartition.PartitionNumber, $FSFormat) + + $searchVolumes = $searchPartition | Get-Volume + + $volumeMatch = $searchVolumes | Where-Object -FilterScript { + $_.FileSystem -eq $FSFormat + } + + if ($volumeMatch) + { + <# + Found a partition with a volume that matches file system + type and not assigned an access path. + #> + $partition = $searchPartition + + Write-Verbose -Message ($script:localizedData.VolumeFoundMessage -f ` + $DiskIdType, $DiskId, $searchPartition.PartitionNumber, $FSFormat) + + break + } # if + } # foreach + } # if + } # if + + # Do we need to create a new partition? + if (-not $partition) + { + # Attempt to create a new partition + $partitionParams = @{} + + if ($Size) + { + # Use only a specific size + Write-Verbose -Message ( @( + "$($MyInvocation.MyCommand): " + $($script:localizedData.CreatingPartitionMessage ` + -f $DiskIdType, $DiskId, "$($Size/1KB) KB") + ) -join '' ) + $partitionParams['Size'] = $Size + } + else + { + # Use the entire disk + Write-Verbose -Message ( @( + "$($MyInvocation.MyCommand): " + $($script:localizedData.CreatingPartitionMessage ` + -f $DiskIdType, $DiskId, 'all free space') + ) -join '' ) + $partitionParams['UseMaximumSize'] = $true + } # if + + # Create the partition + $partition = $disk | New-Partition @partitionParams + + <# + After creating the partition it can take a few seconds for it to become writeable + Wait for up to 30 seconds for the partition to become writeable + #> + $timeout = (Get-Date) + (New-Timespan -Second 30) + while ($partition.IsReadOnly -and (Get-Date) -lt $timeout) + { + Write-Verbose -Message ($script:localizedData.NewPartitionIsReadOnlyMessage -f ` + $DiskIdType, $DiskId, $partition.PartitionNumber) + + Start-Sleep -Seconds 1 + + # Pull the partition details again to check if it is readonly + $partition = $partition | Get-Partition + } # while + } # if + + if ($partition.IsReadOnly) + { + # The partition is still readonly - throw an exception + New-InvalidOperationException ` + -Message ($script:localizedData.NewParitionIsReadOnlyError -f ` + $DiskIdType, $DiskId, $partition.PartitionNumber) + } # if + + $assignAccessPath = $true + } + else + { + # The disk already has a partition on it that is assigned to the access path + Write-Verbose -Message ( @( + "$($MyInvocation.MyCommand): " + $($script:localizedData.PartitionAlreadyAssignedMessage -f ` + $AccessPath, $assignedPartition.PartitionNumber) + ) -join '' ) + + $assignAccessPath = $false + } + + # Get the Volume on the partition + $volume = $partition | Get-Volume + + # Is the volume already formatted? + if ($volume.FileSystem -eq '') + { + # The volume is not formatted + $volParams = @{ + FileSystem = $FSFormat + Confirm = $false + } + + if ($FSLabel) + { + # Set the File System label on the new volume + $volParams["NewFileSystemLabel"] = $FSLabel + } # if + + if ($AllocationUnitSize) + { + # Set the Allocation Unit Size on the new volume + $volParams["AllocationUnitSize"] = $AllocationUnitSize + } # if + + Write-Verbose -Message ( @( + "$($MyInvocation.MyCommand): " + $($script:localizedData.FormattingVolumeMessage -f $volParams.FileSystem) + ) -join '' ) + + # Format the volume + $volume = $partition | Format-Volume @VolParams + } + else + { + # The volume is already formatted + if ($PSBoundParameters.ContainsKey('FSFormat')) + { + # Check the filesystem format + $fileSystem = $volume.FileSystem + if ($fileSystem -ne $FSFormat) + { + # The file system format does not match + # There is nothing we can do to resolve this (yet) + Write-Verbose -Message ( @( + "$($MyInvocation.MyCommand): " + $($script:localizedData.FileSystemFormatMismatch -f ` + $AccessPath, $fileSystem, $FSFormat) + ) -join '' ) + } # if + } # if + + # Check the volume label + if ($PSBoundParameters.ContainsKey('FSLabel')) + { + # The volume should have a label assigned + if ($volume.FileSystemLabel -ne $FSLabel) + { + # The volume lable needs to be changed because it is different. + Write-Verbose -Message ( @( + "$($MyInvocation.MyCommand): " + $($script:localizedData.ChangingVolumeLabelMessage ` + -f $AccessPath, $FSLabel) + ) -join '' ) + + $volume | Set-Volume -NewFileSystemLabel $FSLabel + } # if + } # if + } # if + + # Assign the access path if it isn't assigned + if ($assignAccessPath) + { + <# + Add the partition access path, but do not pipe $disk to + Add-PartitionAccessPath because it is not supported in + Windows Server 2012 R2 + #> + $null = Add-PartitionAccessPath ` + -AccessPath $AccessPath ` + -DiskNumber $disk.Number ` + -PartitionNumber $partition.PartitionNumber + + Write-Verbose -Message ( @( + "$($MyInvocation.MyCommand): " + $($script:localizedData.SuccessfullyInitializedMessage -f $AccessPath) + ) -join '' ) + } # if + + # Get the partitions on the disk + $partition = $disk | Get-Partition -ErrorAction SilentlyContinue + + # Get the current partition state for NoDefaultDriveLetter + $assignedPartition = $partition | + Where-Object -Property AccessPaths -Contains -Value $AccessPath + + if ($assignedPartition.NoDefaultDriveLetter -ne $NoDefaultDriveLetter) + { + Write-Verbose -Message ( @( + "$($MyInvocation.MyCommand): " + "$($script:localizedData.NoDefaultDriveLetterMismatchMessage -f $assignedPartition.NoDefaultDriveLetter, $NoDefaultDriveLetter)" + ) -join '' ) + + # Setting the partition property NoDefaultDriveLetter + Set-Partition -PartitionNumber $assignedPartition.PartitionNumber ` + -DiskNumber $disk.Number ` + -NoDefaultDriveLetter $NoDefaultDriveLetter + } # if +} # Set-TargetResource + +<# + .SYNOPSIS + Tests if the disk is initialized, the partion exists and the access path is assigned. + + .PARAMETER AccessPath + Specifies the access path folder to the assign the disk volume to + + .PARAMETER NoDefaultDriveLetter + Prevents a default drive letter from being assigned to a newly created partition. Defaults to True. + + .PARAMETER DiskId + Specifies the disk identifier for the disk to modify. + + .PARAMETER DiskIdType + Specifies the identifier type the DiskId contains. Defaults to Number. + + .PARAMETER Size + Specifies the size of new volume (use all available space on disk if not provided). + + .PARAMETER FSLabel + Specifies the volume label to assign to the volume. + + .PARAMETER AllocationUnitSize + Specifies the allocation unit size to use when formatting the volume. + + .PARAMETER FSFormat + Specifies the file system format of the new volume. +#> +function Test-TargetResource +{ + [CmdletBinding()] + [OutputType([System.Boolean])] + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $AccessPath, + + [Parameter()] + [System.Boolean] + $NoDefaultDriveLetter = $true, + + [Parameter(Mandatory = $true)] + [System.String] + $DiskId, + + [Parameter()] + [ValidateSet('Number', 'UniqueId', 'Guid', 'Location')] + [System.String] + $DiskIdType = 'Number', + + [Parameter()] + [System.UInt64] + $Size, + + [Parameter()] + [System.String] + $FSLabel, + + [Parameter()] + [System.UInt32] + $AllocationUnitSize, + + [Parameter()] + [ValidateSet('NTFS', 'ReFS')] + [System.String] + $FSFormat = 'NTFS' + ) + + Write-Verbose -Message ( @( + "$($MyInvocation.MyCommand): " + $($script:localizedData.TestingDiskMessage -f $DiskIdType, $DiskId, $AccessPath) + ) -join '' ) + + # Validate the AccessPath parameter adding a trailing slash + $AccessPath = Assert-AccessPathValid -AccessPath $AccessPath -Slash + + Write-Verbose -Message ( @( + "$($MyInvocation.MyCommand): " + $($script:localizedData.CheckDiskInitializedMessage -f $DiskIdType, $DiskId) + ) -join '' ) + + # Get the Disk using the identifiers supplied + $disk = Get-DiskByIdentifier ` + -DiskId $DiskId ` + -DiskIdType $DiskIdType + + if (-not $disk) + { + Write-Verbose -Message ( @( + "$($MyInvocation.MyCommand): " + $($script:localizedData.DiskNotFoundMessage -f $DiskIdType, $DiskId) + ) -join '' ) + return $false + } # if + + if ($disk.IsOffline) + { + Write-Verbose -Message ( @( + "$($MyInvocation.MyCommand): " + $($script:localizedData.DiskNotOnlineMessage -f $DiskIdType, $DiskId) + ) -join '' ) + return $false + } # if + + if ($disk.IsReadOnly) + { + Write-Verbose -Message ( @( + "$($MyInvocation.MyCommand): " + $($script:localizedData.DiskReadOnlyMessage -f $DiskIdType, $DiskId) + ) -join '' ) + return $false + } # if + + if ($disk.PartitionStyle -ne 'GPT') + { + Write-Verbose -Message ( @( + "$($MyInvocation.MyCommand): " + $($script:localizedData.DiskNotGPTMessage -f $DiskIdType, $DiskId, $Disk.PartitionStyle) + ) -join '' ) + return $false + } # if + + # Get the partitions on the disk + $partition = $disk | Get-Partition -ErrorAction SilentlyContinue + + # Check if the disk has an existing partition assigned to the access path + $assignedPartition = $partition | + Where-Object -Property AccessPaths -Contains -Value $AccessPath + + if (-not $assignedPartition) + { + Write-Verbose -Message ( @( + "$($MyInvocation.MyCommand): " + $($script:localizedData.AccessPathNotFoundMessage -f $AccessPath) + ) -join '' ) + return $false + } # if + + # Check if the partition NoDefaultDriveLetter parameter is correct + if ($assignedPartition.NoDefaultDriveLetter -ne $NoDefaultDriveLetter) + { + Write-Verbose -Message ( @( + "$($MyInvocation.MyCommand): " + $($script:localizedData.NoDefaultDriveLetterMismatchMessage -f $assignedPartition.NoDefaultDriveLetter, $NoDefaultDriveLetter) + ) -join '' ) + return $false + } # if + + # Partition size was passed so check it + if ($Size) + { + if ($assignedPartition.Size -ne $Size) + { + # The partition size mismatches but can't be changed (yet) + Write-Verbose -Message ( @( + "$($MyInvocation.MyCommand): " + $($script:localizedData.SizeMismatchMessage -f ` + $AccessPath, $assignedPartition.Size, $Size) + ) -join '' ) + } # if + } # if + + # Prepare the AccessPath used in the CIM query (replaces '\' with '\\') + $queryAccessPath = $AccessPath -replace '\\', '\\' + + $blockSize = (Get-CimInstance ` + -Query "SELECT BlockSize from Win32_Volume WHERE Name = '$queryAccessPath'" ` + -ErrorAction SilentlyContinue).BlockSize + + if ($blockSize -gt 0 -and $AllocationUnitSize -ne 0) + { + if ($AllocationUnitSize -ne $blockSize) + { + # The allocation unit size mismatches but can't be changed (yet) + Write-Verbose -Message ( @( + "$($MyInvocation.MyCommand): " + $($script:localizedData.AllocationUnitSizeMismatchMessage -f ` + $AccessPath, $($blockSize.BlockSize / 1KB), $($AllocationUnitSize / 1KB)) + ) -join '' ) + } # if + } # if + + # Get the volume so the properties can be checked + $volume = $assignedPartition | Get-Volume + + if ($PSBoundParameters.ContainsKey('FSFormat')) + { + # Check the filesystem format + $fileSystem = $volume.FileSystem + if ($fileSystem -ne $FSFormat) + { + # The file system format does not match but can't be changed (yet) + Write-Verbose -Message ( @( + "$($MyInvocation.MyCommand): " + $($script:localizedData.FileSystemFormatMismatch -f ` + $AccessPath, $fileSystem, $FSFormat) + ) -join '' ) + } # if + } # if + + if ($PSBoundParameters.ContainsKey('FSLabel')) + { + # Check the volume label + $label = $volume.FileSystemLabel + if ($label -ne $FSLabel) + { + # The assigned volume label is different and needs updating + Write-Verbose -Message ( @( + "$($MyInvocation.MyCommand): " + $($script:localizedData.DriveLabelMismatch -f ` + $AccessPAth, $label, $FSLabel) + ) -join '' ) + return $false + } # if + } # if + + return $true +} # Test-TargetResource + +<# + .SYNOPSIS + Check access path is found in PSDrives. + + .DESCRIPTION + Check if the access path is found in the list of PSDrives and if not + then forces a full refresh of the list and searches that. + + .PARAMETER AccessPath + Specifies the access path folder. + + .NOTES + The full refresh of PSDrive without any parameters is required because + this is the only way to completely force a refresh of disks and other + related CIM objects. + + If the refresh is not forced then there is a risk that the drive will + appear to not be mounted, causing an error when the resource attempts + to remount it. + + See https://github.com/dsccommunity/StorageDsc/issues/121 +#> +function Test-AccessPathInPSDrive +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $AccessPath + ) + + try + { + $accessPathDisk = $AccessPath.Split(':')[0] + Write-Verbose -Message ( @( + "$($MyInvocation.MyCommand): " + $($script:localizedData.CheckingPSDriveMessage -f $AccessPath, $accessPathDisk) + ) -join '' ) + + $accessPathDrive = Get-PSDrive -Name $accessPathDisk -ErrorAction Stop + } + catch + { + Write-Verbose -Message ( @( + "$($MyInvocation.MyCommand): " + $($script:localizedData.UnavailablePSDriveMessage -f $AccessPath, $accessPathDisk) + ) -join '' ) + + $accessPathDrive = Get-PSDrive + $accessPathDrive = $accessPathDrive | Where-Object -Property Name -eq $accessPathDisk + } + + $accessPathFound = ($null -ne $accessPathDrive) + + if ($accessPathFound) + { + Write-Verbose -Message ( @( + "$($MyInvocation.MyCommand): " + $($script:localizedData.PSDriveFoundMessage -f $AccessPath, $accessPathDisk) + ) -join '' ) + } + else + { + Write-Verbose -Message ( @( + "$($MyInvocation.MyCommand): " + $($script:localizedData.PSDriveNotFoundMessage -f $AccessPath, $accessPathDisk) + ) -join '' ) + } + + return $accessPathFound +} # Test-AccessPathInPSDrive + +Export-ModuleMember -Function *-TargetResource diff --git a/deployment/dsc/azshcihost/StorageDsc/5.0.1/DSCResources/DSC_DiskAccessPath/DSC_DiskAccessPath.schema.mof b/deployment/dsc/azshcihost/StorageDsc/5.0.1/DSCResources/DSC_DiskAccessPath/DSC_DiskAccessPath.schema.mof new file mode 100644 index 0000000..a4832e4 --- /dev/null +++ b/deployment/dsc/azshcihost/StorageDsc/5.0.1/DSCResources/DSC_DiskAccessPath/DSC_DiskAccessPath.schema.mof @@ -0,0 +1,13 @@ + +[ClassVersion("1.0.0.0"), FriendlyName("DiskAccessPath")] +class DSC_DiskAccessPath : OMI_BaseResource +{ + [Key, Description("Specifies the access path folder to the assign the disk volume to.")] String AccessPath; + [Write, Description("Specifies no automatic drive letter assignment to the partition: Defaults to True")] Boolean NoDefaultDriveLetter; + [Required, Description("Specifies the disk identifier for the disk to modify.")] String DiskId; + [Write, Description("Specifies the identifier type the DiskId contains. Defaults to Number."), ValueMap{"Number","UniqueId","Guid","Location"}, Values{"Number","UniqueId","Guid","Location"}] String DiskIdType; + [Write, Description("Specifies the size of new volume.")] Uint64 Size; + [Write, Description("Define volume label if required.")] String FSLabel; + [Write, Description("Specifies the allocation unit size to use when formatting the volume.")] Uint32 AllocationUnitSize; + [Write, Description("Specifies the file system format of the new volume."), ValueMap{"NTFS","ReFS"}, Values{"NTFS","ReFS"}] String FSFormat; +}; diff --git a/deployment/dsc/azshcihost/StorageDsc/5.0.1/DSCResources/DSC_DiskAccessPath/README.md b/deployment/dsc/azshcihost/StorageDsc/5.0.1/DSCResources/DSC_DiskAccessPath/README.md new file mode 100644 index 0000000..72592f3 --- /dev/null +++ b/deployment/dsc/azshcihost/StorageDsc/5.0.1/DSCResources/DSC_DiskAccessPath/README.md @@ -0,0 +1,30 @@ +# Description + +The resource is used to initialize, format and mount the partition/volume to a folder +access path. +The disk to add the partition/volume to is selected by specifying the _DiskId_ and +optionally _DiskIdType_. +The _DiskId_ value can be a _Disk Number_, _Unique Id_, _Guid_ or _Location_. + +**Important: The _Disk Number_ is not a reliable method of selecting a disk because +it has been shown to change between reboots in some environments. +It is recommended to use the _Unique Id_ if possible.** + +The _Disk Number_, _Unique Id_, _Guid_ and _Location_ can be identified for a +disk by using the PowerShell command: + +```powershell +Get-Disk | Select-Object -Property FriendlyName,DiskNumber,UniqueId,Guid,Location +``` + +Note: The _Guid_ for a disk is only assigned once the partition table for the disk +has been created (e.g. the disk has been initialized). Therefore to use this method +of disk selection the disk must have been initialized by some other method. + +## Known Issues + +### Null Location + +The _Location_ for a disk may be `null` for some types of disk, +e.g. file-based virtual disks. Physical disks or Virtual disks provided via a +hypervisor or other hardware virtualization platform should not be affected. diff --git a/deployment/dsc/azshcihost/StorageDsc/5.0.1/DSCResources/DSC_DiskAccessPath/en-US/DSC_DiskAccessPath.strings.psd1 b/deployment/dsc/azshcihost/StorageDsc/5.0.1/DSCResources/DSC_DiskAccessPath/en-US/DSC_DiskAccessPath.strings.psd1 new file mode 100644 index 0000000..4728c77 --- /dev/null +++ b/deployment/dsc/azshcihost/StorageDsc/5.0.1/DSCResources/DSC_DiskAccessPath/en-US/DSC_DiskAccessPath.strings.psd1 @@ -0,0 +1,39 @@ +ConvertFrom-StringData @' + GettingDiskMessage = Getting disk with {0} '{1}' status for access path '{2}'. + SettingDiskMessage = Setting disk with {0} '{1}' status for access path '{2}'. + SetDiskOnlineMessage = Setting disk with {0} '{1}' online. + SetDiskReadWriteMessage = Setting disk with {0} '{1}' to read/write. + CheckingDiskPartitionStyleMessage = Checking disk with {0} '{1}' partition style. + InitializingDiskMessage = Initializing disk with {0} '{1}'. + DiskAlreadyInitializedMessage = Disk with {0} '{1}' is already initialized with GPT. + CreatingPartitionMessage = Creating partition on disk with {0} '{1}' using {2}. + FormattingVolumeMessage = Formatting the volume as '{0}'. + SuccessfullyInitializedMessage = Successfully initialized volume and assigned to access path '{0}'. + ChangingVolumeLabelMessage = Changing Volume assigned to access path '{0}' label to '{1}'. + NewPartitionIsReadOnlyMessage = New partition '{1}' on disk with {0} '{1}' is readonly. Waiting for it to become writable. + TestingDiskMessage = Testing disk with {0} '{1}' status for access path '{2}'. + CheckDiskInitializedMessage = Checking if disk with {0} '{1}' is initialized. + DiskNotFoundMessage = Disk with {0} '{1}' was not found. + DiskNotOnlineMessage = Disk with {0} '{1}' is not online. + DiskReadOnlyMessage = Disk with {0} '{1}'is readonly. + DiskNotGPTMessage = Disk with {0} '{1}' is initialised with '{2}' partition style. GPT required. + AccessPathNotFoundMessage = A volume assigned to access path '{0}' was not found. + NoDefaultDriveLetterMismatchMessage = Partition default drive letter assigmemt parameter '{0}' does not match '{1}'. + SizeMismatchMessage = Partition assigned to access path '{0}' has size {1}, which does not match expected size {2}. + AllocationUnitSizeMismatchMessage = Volume assigned to access path '{0}' has allocation unit size {1} KB does not match expected allocation unit size {2} KB. + FileSystemFormatMismatch = Volume assigned to access path '{0}' filesystem format '{1}' does not match expected format '{2}'. + DriveLabelMismatch = Volume assigned to access path '{0}' label '{1}' does not match expected label '{2}'. + PartitionAlreadyAssignedMessage = Partition '{1}' is already assigned to access path '{0}'. + MatchingPartitionNotFoundMessage = Disk with {0} '{1}' already contains paritions, but none match required size. + MatchingPartitionNoSizeMessage = Disk with {0} '{1}' already contains partitions, but size parameter is not specified. + SearchForVolumeMessage = Searching for {3} volume not assigned to path on partition '{2}' disk with {0} '{1}'. + VolumeFoundMessage = Found {3} volume not assigned to path on partition '{2}' disk with {0} '{1}'. + MatchingPartitionFoundMessage = Disk with {0} '{1}' already contains paritions, and partition '{2}' matches required size. + AccessPathNotFoundOnPartitionMessage = Disk with {0} '{1}' does not contain a partition assigned to access path '{2}'. + CheckingPSDriveMessage = Checking access path '{0}' is available as PSDrive '{1}'. + UnavailablePSDriveMessage = Access path '{0}' is unavailable as PSDrive '{1}', refreshing PSDrives list. + PSDriveFoundMessage = Access path '{0}' found as PSDrive '{1}'. + PSDriveNotFoundMessage = Access path '{0}' not found as PSDrive '{1}'. + DiskAlreadyInitializedError = Disk with {0} '{1}' is already initialized with {2}. + NewParitionIsReadOnlyError = New partition '{2}' on disk with {0} '{1}' did not become writable in the expected time. +'@ diff --git a/deployment/dsc/azshcihost/StorageDsc/5.0.1/DSCResources/DSC_MountImage/DSC_MountImage.psm1 b/deployment/dsc/azshcihost/StorageDsc/5.0.1/DSCResources/DSC_MountImage/DSC_MountImage.psm1 new file mode 100644 index 0000000..bc85389 --- /dev/null +++ b/deployment/dsc/azshcihost/StorageDsc/5.0.1/DSCResources/DSC_MountImage/DSC_MountImage.psm1 @@ -0,0 +1,594 @@ +$modulePath = Join-Path -Path (Split-Path -Path (Split-Path -Path $PSScriptRoot -Parent) -Parent) -ChildPath 'Modules' + +# Import the Storage Common Module. +Import-Module -Name (Join-Path -Path $modulePath ` + -ChildPath (Join-Path -Path 'StorageDsc.Common' ` + -ChildPath 'StorageDsc.Common.psm1')) + +Import-Module -Name (Join-Path -Path $modulePath -ChildPath 'DscResource.Common') + +# Import Localization Strings. +$script:localizedData = Get-LocalizedData -DefaultUICulture 'en-US' + +<# + .SYNOPSIS + Returns the current mount state of the VHD or ISO file. + + .PARAMETER ImagePath + Specifies the path of the VHD or ISO file. +#> +function Get-TargetResource +{ + [CmdletBinding()] + [OutputType([System.Collections.Hashtable])] + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $ImagePath + ) + + Write-Verbose -Message ( @( + "$($MyInvocation.MyCommand): " + $($script:localizedData.GettingMountedImageMessage ` + -f $ImagePath) + ) -join '' ) + + $diskImage = Get-DiskImage -ImagePath $ImagePath + + if ($diskImage.Attached) + { + $returnValue = @{ + ImagePath = $ImagePath + DriveLetter = '' + StorageType = [Microsoft.PowerShell.Cmdletization.GeneratedTypes.DiskImage.StorageType] $diskImage.StorageType + Access = 'ReadOnly' + Ensure = 'Present' + } + + # Determine the Disk Image Access mode + if ($diskImage.StorageType ` + -eq [Microsoft.PowerShell.Cmdletization.GeneratedTypes.DiskImage.StorageType]::ISO) + { + # Get the Drive Letter the ISO is mounted as + $volume = $diskImage | Get-Volume + $returnValue.Driveletter = $volume.DriveLetter + } + else + { + # Look up the disk and find out if it is readwrite. + $disk = Get-Disk -Number $diskImage.Number + if (-not $disk.IsReadOnly) + { + $returnValue.Access = 'ReadWrite' + } # if + + # Find the first 'Basic' partition + $partitions = $disk | Get-Partition + $partition = $partitions | Where-Object -Property Type -EQ 'Basic' + + # Find the first volume in the partition and get the mounted Drive Letter + $volumes = $partition | Get-Volume + $volume = $volumes | Select-Object -First 1 + + $returnValue.Driveletter = $volume.DriveLetter + } # if + } + else + { + $returnValue = @{ + ImagePath = $ImagePath + Ensure = 'Absent' + } + } # if + + $returnValue +} # Get-TargetResource + +<# + .SYNOPSIS + Mounts or dismounts the ISO or VHD. + + .PARAMETER ImagePath + Specifies the path of the VHD or ISO file. + + .PARAMETER DriveLetter + Specifies the drive letter to mount this VHD or ISO to. + + .PARAMETER StorageType + Specifies the storage type of a file. If the StorageType parameter is not specified, then the storage type is determined by file extension. + + .PARAMETER Access + Allows a VHD file to be mounted in read-only or read-write mode. ISO files are mounted in read-only mode regardless of what parameter value you provide. + + .PARAMETER Ensure + Determines whether the setting should be applied or removed. +#> +function Set-TargetResource +{ + # Should process is called in a helper functions but not directly in Set-TargetResource + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSShouldProcess', '')] + [CmdletBinding(SupportsShouldProcess = $true)] + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $ImagePath, + + [Parameter()] + [System.String] + $DriveLetter, + + [Parameter()] + [ValidateSet('ISO','VHD','VHDx','VHDSet')] + [System.String] + $StorageType, + + [Parameter()] + [ValidateSet('ReadOnly','ReadWrite')] + [System.String] + $Access, + + [Parameter()] + [ValidateSet('Present','Absent')] + [System.String] + $Ensure = 'Present' + ) + + Write-Verbose -Message ( @( + "$($MyInvocation.MyCommand): " + $($script:localizedData.SettingMountedImageMessage ` + -f $ImagePath) + ) -join '' ) + + # Check the parameter combo passed is valid, and throw if not. + $null = Test-ParameterValid @PSBoundParameters + + # Get the current mount state of this disk image + $currentState = Get-TargetResource -ImagePath $ImagePath + + # Remove Ensure from PSBoundParameters so it can be splatted + $null = $PSBoundParameters.Remove('Ensure') + + if ($Ensure -eq 'Present') + { + # Get the normalized DriveLetter (colon removed) + $normalizedDriveLetter = Assert-DriveLetterValid -DriveLetter $DriveLetter + + # The Disk Image should be mounted + $needsMount = $false + if ($currentState.Ensure -eq 'Absent') + { + $needsMount = $true + } + else + { + if ($currentState.DriveLetter -ne $normalizedDriveLetter) + { + # The disk image is mounted to the wrong DriveLetter -remount disk + Write-Verbose -Message ( @( + "$($MyInvocation.MyCommand): " + $($script:localizedData.DismountingImageMessage ` + -f $ImagePath,$currentState.DriveLetter) + ) -join '' ) + + Dismount-DiskImage -ImagePath $ImagePath + $needsMount = $true + } # if + } # if + + if ($currentState.StorageType -ne 'ISO') + { + if ($PSBoundParameters.ContainsKey('Access')) + { + # For VHD/VHDx/VHDSet disks check the access mode + if ($currentState.Access -ne $Access) + { + # The access mode is wrong -remount disk + Write-Verbose -Message ( @( + "$($MyInvocation.MyCommand): " + $($script:localizedData.DismountingImageMessage ` + -f $ImagePath,$currentState.DriveLetter) + ) -join '' ) + + Dismount-DiskImage -ImagePath $ImagePath + $needsMount = $true + } # if + } # if + } # if + + if ($needsMount) + { + Write-Verbose -Message ( @( + "$($MyInvocation.MyCommand): " + $($script:localizedData.MountingImageMessage ` + -f $ImagePath,$normalizedDriveLetter) + ) -join '' ) + + Mount-DiskImageToLetter @PSBoundParameters + } # if + } + else + { + # The Disk Image should not be mounted + if ($currentState.Ensure -eq 'Present') + { + # It is mounted so dismount it + Write-Verbose -Message ( @( + "$($MyInvocation.MyCommand): " + $($script:localizedData.DismountingImageMessage ` + -f $ImagePath,$currentState.DriveLetter) + ) -join '' ) + + Dismount-DiskImage -ImagePath $ImagePath + } + } # if +} # Set-TargetResource + +<# + .SYNOPSIS + Tests if the ISO or VHD file mount is in the correct state. + + .PARAMETER ImagePath + Specifies the path of the VHD or ISO file. + + .PARAMETER DriveLetter + Specifies the drive letter to mount this VHD or ISO to. + + .PARAMETER StorageType + Specifies the storage type of a file. If the StorageType parameter is not + specified, then the storage type is determined by file extension. + + .PARAMETER Access + Allows a VHD file to be mounted in read-only or read-write mode. ISO files + are mounted in read-only mode regardless of what parameter value you provide. + + .PARAMETER Ensure + Determines whether the setting should be applied or removed. +#> +function Test-TargetResource +{ + [CmdletBinding()] + [OutputType([System.Boolean])] + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $ImagePath, + + [Parameter()] + [System.String] + $DriveLetter, + + [Parameter()] + [ValidateSet('ISO','VHD','VHDx','VHDSet')] + [System.String] + $StorageType, + + [Parameter()] + [ValidateSet('ReadOnly','ReadWrite')] + [System.String] + $Access, + + [Parameter()] + [ValidateSet('Present','Absent')] + [System.String] + $Ensure = 'Present' + ) + + Write-Verbose -Message ( @( + "$($MyInvocation.MyCommand): " + $($script:localizedData.TestingMountedImageMessage ` + -f $DriveLetter) + ) -join '' ) + + # Check the parameter combo passed is valid, and throw if not. + $null = Test-ParameterValid @PSBoundParameters + + # Get the current mount state of this disk image + $currentState = Get-TargetResource -ImagePath $ImagePath + + if ($Ensure -eq 'Present') + { + # Get the normalized DriveLetter (colon removed) + $normalizedDriveLetter = Assert-DriveLetterValid -DriveLetter $DriveLetter + + # The Disk Image should be mounted + if ($currentState.Ensure -eq 'Absent') + { + # The disk image isn't mounted + Write-Verbose -Message ( @( + "$($MyInvocation.MyCommand): " + $($script:localizedData.ImageIsNotMountedButShouldBeMessage ` + -f $ImagePath,$normalizedDriveLetter) + ) -join '' ) + return $false + } # if + + if ($currentState.DriveLetter -ne $normalizedDriveLetter) + { + # The disk image is mounted to the wrong DriveLetter + Write-Verbose -Message ( @( + "$($MyInvocation.MyCommand): " + $($script:localizedData.ImageIsMountedToTheWrongDriveLetterMessage ` + -f $ImagePath,$currentState.DriveLetter,$normalizedDriveLetter) + ) -join '' ) + return $false + } # if + + if ($currentState.StorageType -ne 'ISO') + { + if ($PSBoundParameters.ContainsKey('Access')) + { + # For VHD/VHDx/VHDSet disks check the access mode + if ($currentState.Access -ne $Access) + { + Write-Verbose -Message ( @( + "$($MyInvocation.MyCommand): " + $($script:localizedData.VHDAccessModeMismatchMessage ` + -f $ImagePath,$normalizedDriveLetter,$currentState.Access,$Access) + ) -join '' ) + return $false + } # if + } # if + } # if + + # The Disk Image is mounted to the expected Drive - nothing to change. + Write-Verbose -Message ( @( + "$($MyInvocation.MyCommand): " + $($script:localizedData.ImageIsMountedAndShouldBeMessage ` + -f $ImagePath,$normalizedDriveLetter) + ) -join '' ) + } + else + { + # The Disk Image should not be mounted + if ($currentState.Ensure -eq 'Present') + { + # The disk image is mounted + Write-Verbose -Message ( @( + "$($MyInvocation.MyCommand): " + $($script:localizedData.ImageIsMountedButShouldNotBeMessage ` + -f $ImagePath,$currentState.DriveLetter) + ) -join '' ) + return $false + } # if + + # The image is not mounted and should not be + Write-Verbose -Message ( @( + "$($MyInvocation.MyCommand): " + $($script:localizedData.ImageIsNotMountedAndShouldNotBeMessage ` + -f $ImagePath) + ) -join '' ) + } # if + + # No changes are needed + return $true +} # Test-TargetResource + +<# + .SYNOPSIS + Validates that the parameters passed are valid. If the parameter combination + is invalid then an exception will be thrown. Also validates the DriveLetter + value that is passed is valid. + + .PARAMETER ImagePath + Specifies the path of the VHD or ISO file. + + .PARAMETER DriveLetter + Specifies the drive letter to mount this VHD or ISO to. + + .PARAMETER StorageType + Specifies the storage type of a file. If the StorageType parameter is not + specified, then the storage type is determined by file extension. + + .PARAMETER Access + Allows a VHD file to be mounted in read-only or read-write mode. ISO files + are mounted in read-only mode regardless of what parameter value you provide. + + .PARAMETER Ensure + Determines whether the setting should be applied or removed. + + .OUTPUTS + If ensure is present then returns a normalized array of Drive Letters. +#> +function Test-ParameterValid +{ + [CmdletBinding()] + [OutputType([String[]])] + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $ImagePath, + + [Parameter()] + [System.String] + $DriveLetter, + + [Parameter()] + [ValidateSet('ISO','VHD','VHDx','VHDSet')] + [System.String] + $StorageType, + + [Parameter()] + [ValidateSet('ReadOnly','ReadWrite')] + [System.String] + $Access, + + [Parameter()] + [ValidateSet('Present','Absent')] + [System.String] + $Ensure = 'Present' + ) + + if ($Ensure -eq 'Absent') + { + if ($PSBoundParameters.ContainsKey('DriveLetter')) + { + # The DriveLetter should not be set if Ensure is Absent + New-InvalidOperationException ` + -Message ($script:localizedData.InvalidParameterSpecifiedError -f ` + 'Absent','DriveLetter') + } # if + + if ($PSBoundParameters.ContainsKey('StorageType')) + { + # The StorageType should not be set if Ensure is Absent + New-InvalidOperationException ` + -Message ($script:localizedData.InvalidParameterSpecifiedError -f ` + 'Absent','StorageType') + } # if + + if ($PSBoundParameters.ContainsKey('Access')) + { + # The Access should not be set if Ensure is Absent + New-InvalidOperationException ` + -Message ($script:localizedData.InvalidParameterSpecifiedError -f ` + 'Absent','Access') + } # if + } + else + { + if (-not (Test-Path -Path $ImagePath)) + { + # The file specified by ImagePath should be found + New-InvalidOperationException ` + -Message ($script:localizedData.DiskImageFileNotFoundError -f ` + $ImagePath) + } # if + + if ($PSBoundParameters.ContainsKey('DriveLetter')) + { + # Test the Drive Letter to ensure it is valid + $normalizedDriveLetter = Assert-DriveLetterValid -DriveLetter $DriveLetter + } + else + { + # Drive letter is not specified but Ensure is present. + New-InvalidOperationException ` + -Message ($script:localizedData.InvalidParameterNotSpecifiedError -f ` + 'Present','DriveLetter') + } # if + } # if +} # Test-ParameterValid + +<# + .SYNOPSIS + Mounts a Disk Image to a specific Drive Letter. + + .PARAMETER ImagePath + Specifies the path of the VHD or ISO file. + + .PARAMETER DriveLetter + Specifies the drive letter to mount this VHD or ISO to. + + .PARAMETER StorageType + Specifies the storage type of a file. If the StorageType parameter is not + specified, then the storage type is determined by file extension. + + .PARAMETER Access + Allows a VHD file to be mounted in read-only or read-write mode. ISO files + are mounted in read-only mode regardless of what parameter value you provide. +#> +function Mount-DiskImageToLetter +{ + [CmdletBinding()] + [OutputType([String[]])] + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $ImagePath, + + [Parameter()] + [System.String] + $DriveLetter, + + [Parameter()] + [ValidateSet('ISO','VHD','VHDx','VHDSet')] + [System.String] + $StorageType, + + [Parameter()] + [ValidateSet('ReadOnly','ReadWrite')] + [System.String] + $Access + ) + + # Get the normalized DriveLetter (colon removed) + $normalizedDriveLetter = Assert-DriveLetterValid -DriveLetter $DriveLetter + + # Mount the Diskimage + $mountParams = @{ + ImagePath = $ImagePath + } + + if ($PSBoundParameters.ContainsKey('Access')) + { + $mountParams += @{ + Access = $Access + } + } # if + $null = Mount-DiskImage @mountParams + + # Get the DiskImage object + $diskImage = Get-DiskImage -ImagePath $ImagePath + + # Determine the Storage Type expected + if (-not $PSBoundParameters.ContainsKey('StorageType')) + { + $StorageType = [Microsoft.PowerShell.Cmdletization.GeneratedTypes.DiskImage.StorageType] $diskImage.StorageType + } # if + + # Different StorageType images require different methods of getting the Volume object. + if ($StorageType -eq [Microsoft.PowerShell.Cmdletization.GeneratedTypes.DiskImage.StorageType]::ISO) + { + # This is a ISO diskimage + $volume = $diskImage | Get-Volume + } + else + { + # This is a VHD/VHDx/VHDSet diskimage + $disk = Get-Disk -Number $diskImage.Number + + # Find the first 'Basic' partition to mount + $partitions = $disk | Get-Partition + $partition = $partitions | Where-Object -Property Type -EQ 'Basic' + + # Find the first volume in the partition and get the mounted Drive Letter + $volumes = $partition | Get-Volume + $volume = $volumes | Select-Object -First 1 + } # if + + $currentDriveLetter = $volume.DriveLetter + + # Verify that the drive letter assigned to the drive is the one needed. + if ($currentDriveLetter -ne $normalizedDriveLetter) + { + Write-Verbose -Message ( @( + "$($MyInvocation.MyCommand): " + $($script:localizedData.ChangingImageDriveLetterMessage ` + -f $ImagePath,$currentDriveLetter,$normalizedDriveLetter) + ) -join '' ) + + <# + Use CIM to change the DriveLetter. + The Win32_Volume must be looked up using the ObjectId found in the Volume object + ObjectId is more verbose than DeviceId in Windows 10 Anniversary Edition, look for + DeviceId in the ObjectId string to match volumes. + #> + $cimVolume = Get-CimInstance -ClassName Win32_Volume | + Where-Object -FilterScript { + $volume.ObjectId.IndexOf($_.DeviceId) -ne -1 + } + + Set-CimInstance ` + -InputObject $cimVolume ` + -Property @{ + DriveLetter = "$($normalizedDriveLetter):" + } + } # if +} # Mount-DiskImageToLetter + +Export-ModuleMember -Function *-TargetResource diff --git a/deployment/dsc/azshcihost/StorageDsc/5.0.1/DSCResources/DSC_MountImage/DSC_MountImage.schema.mof b/deployment/dsc/azshcihost/StorageDsc/5.0.1/DSCResources/DSC_MountImage/DSC_MountImage.schema.mof new file mode 100644 index 0000000..682fe2b --- /dev/null +++ b/deployment/dsc/azshcihost/StorageDsc/5.0.1/DSCResources/DSC_MountImage/DSC_MountImage.schema.mof @@ -0,0 +1,11 @@ + +[ClassVersion("1.0.0.0"), FriendlyName("MountImage")] +class DSC_MountImage : OMI_BaseResource +{ + [Key, Description("Specifies the path of the VHD or ISO file.")] String ImagePath; + [Write, Description("Specifies the drive letter to mount this VHD or ISO to.")] String DriveLetter; + [Write, Description("Specifies the storage type of a file. If the StorageType parameter is not specified, then the storage type is determined by file extension."), ValueMap{"ISO","VHD","VHDx","VHDSet"}, Values{"ISO","VHD","VHDx","VHDSet"}] String StorageType; + [Write, Description("Allows a VHD file to be mounted in read-only or read-write mode. ISO files are mounted in read-only mode regardless of what parameter value you provide."), ValueMap{"ReadOnly","ReadWrite"}, Values{"ReadOnly","ReadWrite"}] String Access; + [Write, Description("Determines whether the VHD or ISO should be mounted or not."), ValueMap{"Present","Absent"}, Values{"Present","Absent"}] String Ensure; +}; + diff --git a/deployment/dsc/azshcihost/StorageDsc/5.0.1/DSCResources/DSC_MountImage/README.md b/deployment/dsc/azshcihost/StorageDsc/5.0.1/DSCResources/DSC_MountImage/README.md new file mode 100644 index 0000000..094f10d --- /dev/null +++ b/deployment/dsc/azshcihost/StorageDsc/5.0.1/DSCResources/DSC_MountImage/README.md @@ -0,0 +1,4 @@ +# Description + +The resource is used to mount or unmount an ISO/VHD disk image. It can be +mounted as read-only (ISO, VHD, VHDx) or read/write (VHD, VHDx). diff --git a/deployment/dsc/azshcihost/StorageDsc/5.0.1/DSCResources/DSC_MountImage/en-US/DSC_MountImage.strings.psd1 b/deployment/dsc/azshcihost/StorageDsc/5.0.1/DSCResources/DSC_MountImage/en-US/DSC_MountImage.strings.psd1 new file mode 100644 index 0000000..ca888cf --- /dev/null +++ b/deployment/dsc/azshcihost/StorageDsc/5.0.1/DSCResources/DSC_MountImage/en-US/DSC_MountImage.strings.psd1 @@ -0,0 +1,17 @@ +ConvertFrom-StringData @' + GettingMountedImageMessage = Getting Mounted Drive for image file '{0}'. + SettingMountedImageMessage = Setting Mounted Drive for image file '{0}'. + DismountingImageMessage = The image file '{0}' is being dismounted from drive '{1}'. + TestingMountedImageMessage = Testing Mounted Drive for image file '{0}'. + ImageIsNotMountedButShouldBeMessage = The image file '{0}' is not mounted to drive '{1}' but should be. Change required. + ImageIsMountedToTheWrongDriveLetterMessage = The image file '{0}' is mounted as drive '{1}' but should be mounted as drive '{2}'. Change required. + VHDAccessModeMismatchMessage = The image file '{0}' is mounted to drive '{1}' but the access is '{2}' and should be '{3}'. Change required. + ImageIsMountedAndShouldBeMessage = The image file '{0}' is mounted to drive '{1}' and should be. + ImageIsMountedButShouldNotBeMessage = The image file '{0}' is mounted to drive '{1}' but should not be. Change required. + ImageIsNotMountedAndShouldNotBeMessage = The image file '{0}' is not mounted and should not be. + InvalidParameterSpecifiedError = Ensure is '{0}' but '{1}' was specified. + InvalidParameterNotSpecifiedError = Ensure is '{0}' but '{1}' was not specified. + DiskImageFileNotFoundError = The image file '{0}' could not be found. + MountingImageMessage = The image file '{0}' is being mounted as drive '{1}'. + ChangingImageDriveLetterMessage = The image file '{0}' mounted as drive '{1}' will be changed to '{2}'. +'@ diff --git a/deployment/dsc/azshcihost/StorageDsc/5.0.1/DSCResources/DSC_OpticalDiskDriveLetter/DSC_OpticalDiskDriveLetter.psm1 b/deployment/dsc/azshcihost/StorageDsc/5.0.1/DSCResources/DSC_OpticalDiskDriveLetter/DSC_OpticalDiskDriveLetter.psm1 new file mode 100644 index 0000000..01c7a34 --- /dev/null +++ b/deployment/dsc/azshcihost/StorageDsc/5.0.1/DSCResources/DSC_OpticalDiskDriveLetter/DSC_OpticalDiskDriveLetter.psm1 @@ -0,0 +1,390 @@ +$modulePath = Join-Path -Path (Split-Path -Path (Split-Path -Path $PSScriptRoot -Parent) -Parent) -ChildPath 'Modules' + +# Import the Storage Common Module. +Import-Module -Name (Join-Path -Path $modulePath ` + -ChildPath (Join-Path -Path 'StorageDsc.Common' ` + -ChildPath 'StorageDsc.Common.psm1')) + +Import-Module -Name (Join-Path -Path $modulePath -ChildPath 'DscResource.Common') + +# Import Localization Strings. +$script:localizedData = Get-LocalizedData -DefaultUICulture 'en-US' + +<# + .SYNOPSIS + This helper function returns a hashtable containing the current + drive letter assigned to the optical disk in the system matching + the disk number. + + If the drive exists but is not mounted to a drive letter then + the DriveLetter will be empty, but the DeviceId will contain the + DeviceId representing the optical disk. + + If there are no optical disks found in the system an exception + will be thrown. + + .PARAMETER DiskId + Specifies the optical disk number for the disk to return the drive + letter of. + + .NOTES + The Caption and DeviceID properties are checked to avoid + mounted ISO images in Windows 2012+ and Windows 10. The + device ID is required because a CD/DVD in a Hyper-V virtual + machine has the same caption as a mounted ISO. + + Example DeviceID for a virtual drive in a Hyper-V VM - SCSI\CDROM&VEN_MSFT&PROD_VIRTUAL_DVD-ROM\000006 + Example DeviceID for a mounted ISO in a Hyper-V VM - SCSI\CDROM&VEN_MSFT&PROD_VIRTUAL_DVD-ROM\2&1F4ADFFE&0&000002 +#> +function Get-OpticalDiskDriveLetter +{ + [CmdletBinding()] + [OutputType([System.Collections.Hashtable])] + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $DiskId + ) + + $driveLetter = $null + + Write-Verbose -Message ( @( + "$($MyInvocation.MyCommand): " + $($script:localizedData.UsingGetCimInstanceToFetchDriveLetter -f $DiskId) + ) -join '' ) + + # Get the optical disk matching the Id + $opticalDisks = Get-CimInstance -ClassName Win32_CDROMDrive | + Where-Object -FilterScript { + -not ( + $_.Caption -eq 'Microsoft Virtual DVD-ROM' -and + ($_.DeviceID.Split('\')[-1]).Length -gt 10 + ) + } + + if ($opticalDisks) + { + <# + To behave in a similar fashion to the other xStorage resources the + DiskId represents the number of the optical disk in the system. + However as these are returned as an array of 0..x elements then + subtract one from the DiskId to get the actual optical disk number + that is required. + #> + $opticalDisk = $opticalDisks[$DiskId - 1] + + if ($opticalDisk) + { + try + { + # Make sure the current DriveLetter is an actual drive letter + $driveLetter = Assert-DriveLetterValid -DriveLetter $opticalDisk.Drive -Colon + } + catch + { + # Optical drive exists but is not mounted to a drive letter + $driveLetter = '' + + Write-Verbose -Message ( @( + "$($MyInvocation.MyCommand): " + $($script:localizedData.OpticalDiskNotAssignedDriveLetter -f $DiskId) + ) -join '' ) + } + + $deviceId = $opticalDisk.Drive + } + } + + if ([System.String]::IsNullOrEmpty($deviceId)) + { + # The requested optical drive does not exist in the system + Write-Verbose -Message ( @( + "$($MyInvocation.MyCommand): " + $($script:localizedData.OpticalDiskDriveDoesNotExist -f $DiskId) + ) -join '' ) + } + + return @{ + DriveLetter = $driveLetter + DeviceId = $deviceId + } +} + +<# + .SYNOPSIS + Returns the current drive letter assigned to the optical disk. + + .PARAMETER DiskId + Specifies the optical disk number for the disk to assign the drive + letter to. + + .PARAMETER DriveLetter + Specifies the drive letter to assign to the optical disk. Can be a + single letter, optionally followed by a colon. This value is ignored + if Ensure is set to Absent. +#> +function Get-TargetResource +{ + [CmdletBinding()] + [OutputType([System.Collections.Hashtable])] + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $DiskId, + + [Parameter(Mandatory = $true)] + [System.String] + $DriveLetter + ) + + $ensure = 'Absent' + + # Get the drive letter assigned to the optical disk + $currentDriveInfo = Get-OpticalDiskDriveLetter -DiskId $DiskId + + if ([System.String]::IsNullOrEmpty($currentDriveInfo.DeviceId)) + { + $currentDriveLetter = '' + } + else + { + $currentDriveLetter = $currentDriveInfo.DriveLetter + + if ([System.String]::IsNullOrWhiteSpace($currentDriveLetter)) + { + Write-Verbose -Message ( @( + "$($MyInvocation.MyCommand): " + $($script:localizedData.OpticalDiskNotAssignedDriveLetter -f $DiskId) + ) -join '' ) + } + else + { + Write-Verbose -Message ( @( + "$($MyInvocation.MyCommand): " + $($script:localizedData.OpticalDiskAssignedDriveLetter -f $DiskId, $DriveLetter) + ) -join '' ) + + $ensure = 'Present' + } + } + + $returnValue = @{ + DiskId = $DiskId + DriveLetter = $currentDriveLetter + Ensure = $ensure + } + + return $returnValue +} # Get-TargetResource + +<# + .SYNOPSIS + Sets the drive letter of an optical disk. + + .PARAMETER DiskId + Specifies the optical disk number for the disk to assign the drive + letter to. + + .PARAMETER DriveLetter + Specifies the drive letter to assign to the optical disk. Can be a + single letter, optionally followed by a colon. This value is ignored + if Ensure is set to Absent. + + .PARAMETER Ensure + Determines whether a drive letter should be assigned to the + optical disk. Defaults to 'Present'. +#> +function Set-TargetResource +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $DiskId, + + [Parameter(Mandatory = $true)] + [System.String] + $DriveLetter, + + [Parameter()] + [ValidateSet('Present', 'Absent')] + [System.String] + $Ensure = 'Present' + ) + + # Allow use of drive letter without colon + $DriveLetter = Assert-DriveLetterValid -DriveLetter $DriveLetter -Colon + + # Get the drive letter assigned to the optical disk + $currentDriveInfo = Get-OpticalDiskDriveLetter -DiskId $DiskId + $currentDriveLetter = $currentDriveInfo.DriveLetter + + if ([System.String]::IsNullOrWhiteSpace($currentDriveLetter)) + { + <# + If the current drive letter is empty then the volume must be looked up by DeviceId + The DeviceId in the volume will show as \\?\Volume{bba1802b-e7a1-11e3-824e-806e6f6e6963}\ + So we need to change the currentDriveLetter to match this value when we set the drive letter + #> + $deviceId = $currentDriveInfo.DeviceId + + $volume = Get-CimInstance ` + -ClassName Win32_Volume ` + -Filter "DeviceId = '\\\\?\\$deviceId\\'" + } + else + { + $volume = Get-CimInstance ` + -ClassName Win32_Volume ` + -Filter "DriveLetter = '$currentDriveLetter'" + } + + # Does the Drive Letter need to be added or removed + if ($Ensure -eq 'Absent') + { + if (-not [System.String]::IsNullOrEmpty($currentDriveInfo.DeviceId)) + { + Write-Verbose -Message ( @( + "$($MyInvocation.MyCommand): " + $($script:localizedData.AttemptingToRemoveDriveLetter -f $diskId, $currentDriveLetter) + ) -join '' ) + + $volume | Set-CimInstance -Property @{ + DriveLetter = $null + } + } + } + else + { + Write-Verbose -Message ( @( + "$($MyInvocation.MyCommand): " + $($script:localizedData.AttemptingToSetDriveLetter -f $diskId, $currentDriveLetter, $DriveLetter) + ) -join '' ) + + $volume | Set-CimInstance -Property @{ + DriveLetter = $DriveLetter + } + } +} # Set-TargetResource + +<# + .SYNOPSIS + Tests the disk letter assigned to an optical disk is correct. + + .PARAMETER DiskId + Specifies the optical disk number for the disk to assign the drive + letter to. + + .PARAMETER DriveLetter + Specifies the drive letter to assign to the optical disk. Can be a + single letter, optionally followed by a colon. This value is ignored + if Ensure is set to Absent. + + .PARAMETER Ensure + Determines whether a drive letter should be assigned to the + optical disk. Defaults to 'Present'. +#> +function Test-TargetResource +{ + [CmdletBinding()] + [OutputType([System.Boolean])] + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $DiskId, + + [Parameter(Mandatory = $true)] + [System.String] + $DriveLetter, + + [Parameter()] + [ValidateSet('Present', 'Absent')] + [System.String] + $Ensure = 'Present' + ) + + $desiredConfigurationMatch = $true + + # Allow use of drive letter without colon + $DriveLetter = Assert-DriveLetterValid -DriveLetter $DriveLetter -Colon + + # Get the drive letter assigned to the optical disk + $currentDriveInfo = Get-OpticalDiskDriveLetter -DiskId $DiskId + $currentDriveLetter = $currentDriveInfo.DriveLetter + + if ($Ensure -eq 'Absent') + { + if (-not [System.String]::IsNullOrEmpty($currentDriveInfo.DeviceId)) + { + # The Drive Letter should be absent from the optical disk + if ([System.String]::IsNullOrWhiteSpace($currentDriveLetter)) + { + Write-Verbose -Message ( @( + "$($MyInvocation.MyCommand): " + $($script:localizedData.DriveLetterDoesNotExistAndShouldNot -f $DiskId) + ) -join '' ) + } + else + { + # The Drive Letter needs to be dismounted + Write-Verbose -Message ( @( + "$($MyInvocation.MyCommand): " + $($script:localizedData.DriveLetterExistsButShouldNot -f $DiskId, $currentDriveLetter) + ) -join '' ) + + $desiredConfigurationMatch = $false + } + } + } + else + { + # Throw an exception if the desired optical disk does not exist + if ([System.String]::IsNullOrEmpty($currentDriveInfo.DeviceId)) + { + New-InvalidArgumentException ` + -Message ($script:localizedData.NoOpticalDiskDriveError -f $DiskId) ` + -ArgumentName 'DiskId' + } + + if ($currentDriveLetter -eq $DriveLetter) + { + # The optical disk drive letter is already set correctly + Write-Verbose -Message ( @( + "$($MyInvocation.MyCommand): " + $($script:localizedData.DriverLetterExistsAndIsCorrect -f $DiskId, $DriveLetter) + ) -join '' ) + } + else + { + # Is a desired drive letter already assigned to a different drive? + $existingVolume = Get-CimInstance ` + -ClassName Win32_Volume ` + -Filter "DriveLetter = '$DriveLetter'" + + if ($existingVolume) + { + # The desired drive letter is already assigned to another drive - can't proceed + New-InvalidOperationException ` + -Message $($script:localizedData.DriveLetterAssignedToAnotherDrive -f $DriveLetter) + } + else + { + # The optical drive letter needs to be changed + Write-Verbose -Message ( @( + "$($MyInvocation.MyCommand): " + $($script:localizedData.DriverLetterExistsAndIsNotCorrect -f $DiskId, $currentDriveLetter, $DriveLetter) + ) -join '' ) + + $desiredConfigurationMatch = $false + } + } + } + + return $desiredConfigurationMatch +} + +Export-ModuleMember -Function *-TargetResource diff --git a/deployment/dsc/azshcihost/StorageDsc/5.0.1/DSCResources/DSC_OpticalDiskDriveLetter/DSC_OpticalDiskDriveLetter.schema.mof b/deployment/dsc/azshcihost/StorageDsc/5.0.1/DSCResources/DSC_OpticalDiskDriveLetter/DSC_OpticalDiskDriveLetter.schema.mof new file mode 100644 index 0000000..1b92027 --- /dev/null +++ b/deployment/dsc/azshcihost/StorageDsc/5.0.1/DSCResources/DSC_OpticalDiskDriveLetter/DSC_OpticalDiskDriveLetter.schema.mof @@ -0,0 +1,8 @@ + +[ClassVersion("1.0.0.0"), FriendlyName("OpticalDiskDriveLetter")] +class DSC_OpticalDiskDriveLetter : OMI_BaseResource +{ + [Key, Description("Specifies the optical disk number for the disk to assign the drive letter to.")] String DiskId; + [Required, Description("Specifies the drive letter to assign to the optical disk. Can be a single letter, optionally followed by a colon. This value is ignored if Ensure is set to Absent.")] String DriveLetter; + [Write, Description("Determines whether a drive letter should be assigned to the optical disk. Defaults to 'Present'."), ValueMap{"Present","Absent"}, Values{"Present","Absent"}] String Ensure; +}; diff --git a/deployment/dsc/azshcihost/StorageDsc/5.0.1/DSCResources/DSC_OpticalDiskDriveLetter/README.md b/deployment/dsc/azshcihost/StorageDsc/5.0.1/DSCResources/DSC_OpticalDiskDriveLetter/README.md new file mode 100644 index 0000000..aba1f40 --- /dev/null +++ b/deployment/dsc/azshcihost/StorageDsc/5.0.1/DSCResources/DSC_OpticalDiskDriveLetter/README.md @@ -0,0 +1,39 @@ +# Description + +The resource is used to set the drive letter of an optical disk drive (e.g. +a CDROM or DVD drive). + +It can be used to set the drive letter of a specific optical disk drive if +there are multiple in the system by specifying a value greater than 1 for +the `DiskId` parameter. + +In a system with a single optical disk drive then the `DiskId` should +be set to 1. + +In systems with multiple optical disks, the `DiskId` should be set to +the ordinal number of the required optical disk found in the list +returned when executing the following cmdlet: + +```powershell +Get-CimInstance -ClassName Win32_CDROMDrive +``` + +Warning: Adding and removing optical drive devices to a system may cause the +order the optical drives appear in the system to change. Therefore, the +drive ordinal number may be affected in these situations. + +It is designed to ignore _temporary_ optical disk drives that are created +when mounting ISOs on Windows Server 2012+. + +With the Device ID, we look for the length of the string after the final +backslash (crude, but appears to work so far). + +Example: + +```powershell +# DeviceID for a virtual drive in a Hyper-V VM +"SCSI\CDROM&VEN_MSFT&PROD_VIRTUAL_DVD-ROM\**000006**" + +# DeviceID for a mounted ISO in a Hyper-V VM +"SCSI\CDROM&VEN_MSFT&PROD_VIRTUAL_DVD-ROM\**2&1F4ADFFE&0&000002**" +``` diff --git a/deployment/dsc/azshcihost/StorageDsc/5.0.1/DSCResources/DSC_OpticalDiskDriveLetter/en-US/DSC_OpticalDiskDriveLetter.strings.psd1 b/deployment/dsc/azshcihost/StorageDsc/5.0.1/DSCResources/DSC_OpticalDiskDriveLetter/en-US/DSC_OpticalDiskDriveLetter.strings.psd1 new file mode 100644 index 0000000..e380ede --- /dev/null +++ b/deployment/dsc/azshcihost/StorageDsc/5.0.1/DSCResources/DSC_OpticalDiskDriveLetter/en-US/DSC_OpticalDiskDriveLetter.strings.psd1 @@ -0,0 +1,16 @@ +ConvertFrom-StringData @' + UsingGetCimInstanceToFetchDriveLetter = Using Get-CimInstance to get the drive letter of optical disk {0} in the system. + OpticalDiskAssignedDriveLetter = The optical disk {0} is currently assigned drive letter '{1}'. + OpticalDiskNotAssignedDriveLetter = The optical disk {0} is not currently assigned a drive letter. + OpticalDiskDriveDoesNotExist = The optical disk {0} could not be found in the system. + NoOpticalDiskDriveError = The optical disk {0} could not be found in the system, so this resource has nothing to do. This resource does not change the drive letter of mounted ISOs. + + AttemptingToSetDriveLetter = The optical disk {0} drive letter is '{1}', attempting to set to '{2}'. + AttemptingToRemoveDriveLetter = The optical disk {0} drive letter is '{1}', attempting to remove it. + + DriveLetterDoesNotExistAndShouldNot = The optical disk {0} does not have a drive letter assigned. Change not required. + DriveLetterExistsButShouldNot = The optical disk {0} is assigned the drive letter '{1}' which should be removed. Change required. + DriverLetterExistsAndIsCorrect = The optical disk {0} is assigned the drive letter '{1}' which is correct. Change not required. + DriveLetterAssignedToAnotherDrive = Drive letter '{0}' is already present but assigned to a another volume. Change can not proceed. + DriverLetterExistsAndIsNotCorrect = The optical disk {0} is assigned the drive letter '{1}' but should be '{2}'. Change required. +'@ diff --git a/deployment/dsc/azshcihost/StorageDsc/5.0.1/DSCResources/DSC_WaitForDisk/DSC_WaitForDisk.psm1 b/deployment/dsc/azshcihost/StorageDsc/5.0.1/DSCResources/DSC_WaitForDisk/DSC_WaitForDisk.psm1 new file mode 100644 index 0000000..6fd425c --- /dev/null +++ b/deployment/dsc/azshcihost/StorageDsc/5.0.1/DSCResources/DSC_WaitForDisk/DSC_WaitForDisk.psm1 @@ -0,0 +1,220 @@ +$modulePath = Join-Path -Path (Split-Path -Path (Split-Path -Path $PSScriptRoot -Parent) -Parent) -ChildPath 'Modules' + +# Import the Storage Common Module. +Import-Module -Name (Join-Path -Path $modulePath ` + -ChildPath (Join-Path -Path 'StorageDsc.Common' ` + -ChildPath 'StorageDsc.Common.psm1')) + +Import-Module -Name (Join-Path -Path $modulePath -ChildPath 'DscResource.Common') + +# Import Localization Strings. +$script:localizedData = Get-LocalizedData -DefaultUICulture 'en-US' + +<# + .SYNOPSIS + Returns the current state of the wait for disk resource. + + .PARAMETER DiskId + Specifies the disk identifier for the disk to wait for. + + .PARAMETER DiskIdType + Specifies the identifier type the DiskId contains. Defaults to Number. + + .PARAMETER RetryIntervalSec + Specifies the number of seconds to wait for the disk to become available. + + .PARAMETER RetryCount + The number of times to loop the retry interval while waiting for the disk. +#> +function Get-TargetResource +{ + [CmdletBinding()] + [OutputType([System.Collections.Hashtable])] + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $DiskId, + + [Parameter()] + [ValidateSet('Number','UniqueId','Guid','Location')] + [System.String] + $DiskIdType = 'Number', + + [Parameter()] + [System.UInt32] + $RetryIntervalSec = 10, + + [Parameter()] + [System.UInt32] + $RetryCount = 60 + ) + + $isAvailable = Test-TargetResource @PSBoundParameters + + Write-Verbose -Message ( @( + "$($MyInvocation.MyCommand): " + $($script:localizedData.GettingWaitForDiskStatusMessage -f $DiskIdType,$DiskId) + ) -join '' ) + + $returnValue = @{ + DiskId = $DiskId + DiskIdType = $DiskIdType + RetryIntervalSec = $RetryIntervalSec + RetryCount = $RetryCount + IsAvailable = $isAvailable + } + + return $returnValue +} # function Get-TargetResource + +<# + .SYNOPSIS + Sets the current state of the wait for disk resource. + + .PARAMETER DiskId + Specifies the disk identifier for the disk to wait for. + + .PARAMETER DiskIdType + Specifies the identifier type the DiskId contains. Defaults to Number. + + .PARAMETER RetryIntervalSec + Specifies the number of seconds to wait for the disk to become available. + + .PARAMETER RetryCount + The number of times to loop the retry interval while waiting for the disk. +#> +function Set-TargetResource +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $DiskId, + + [Parameter()] + [ValidateSet('Number','UniqueId','Guid','Location')] + [System.String] + $DiskIdType = 'Number', + + [Parameter()] + [System.UInt32] + $RetryIntervalSec = 10, + + [Parameter()] + [System.UInt32] + $RetryCount = 60 + ) + + Write-Verbose -Message ( @( + "$($MyInvocation.MyCommand): " + $($script:localizedData.CheckingForDiskStatusMessage -f $DiskIdType,$DiskId) + ) -join '' ) + + $diskFound = $false + + for ($count = 0; $count -lt $RetryCount; $count++) + { + # Get the Disk using the identifiers supplied + $disk = Get-DiskByIdentifier ` + -DiskId $DiskId ` + -DiskIdType $DiskIdType + + if ($disk) + { + Write-Verbose -Message ( @( + "$($MyInvocation.MyCommand): " + $($script:localizedData.DiskFoundMessage -f $DiskIdType,$DiskId,$disk.FriendlyName) + ) -join '' ) + + $diskFound = $true + break + } + else + { + Write-Verbose -Message ( @( + "$($MyInvocation.MyCommand): " + $($script:localizedData.DiskNotFoundRetryingMessage -f $DiskIdType,$DiskId,$RetryIntervalSec) + ) -join '' ) + + Start-Sleep -Seconds $RetryIntervalSec + } # if + } # for + + if (-not $diskFound) + { + New-InvalidOperationException ` + -Message $($script:localizedData.DiskNotFoundAfterError -f $DiskIdType,$DiskId,$RetryCount) + } # if +} # function Set-TargetResource + +<# + .SYNOPSIS + Tests the current state of the wait for disk resource. + + .PARAMETER DiskId + Specifies the disk identifier for the disk to wait for. + + .PARAMETER DiskIdType + Specifies the identifier type the DiskId contains. Defaults to Number. + + .PARAMETER RetryIntervalSec + Specifies the number of seconds to wait for the disk to become available. + + .PARAMETER RetryCount + The number of times to loop the retry interval while waiting for the disk. +#> +function Test-TargetResource +{ + [CmdletBinding()] + [OutputType([System.Boolean])] + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $DiskId, + + [Parameter()] + [ValidateSet('Number','UniqueId','Guid','Location')] + [System.String] + $DiskIdType = 'Number', + + [Parameter()] + [System.UInt32] + $RetryIntervalSec = 10, + + [Parameter()] + [System.UInt32] + $RetryCount = 60 + ) + + Write-Verbose -Message ( @( + "$($MyInvocation.MyCommand): " + $($script:localizedData.CheckingForDiskStatusMessage -f $DiskIdType,$DiskId) + ) -join '' ) + + # Get the Disk using the identifiers supplied + $disk = Get-DiskByIdentifier ` + -DiskId $DiskId ` + -DiskIdType $DiskIdType + + if ($disk) + { + Write-Verbose -Message ( @( + "$($MyInvocation.MyCommand): " + $($script:localizedData.DiskFoundMessage -f $DiskIdType,$DiskId,$disk.FriendlyName) + ) -join '' ) + + return $true + } + + Write-Verbose -Message ( @( + "$($MyInvocation.MyCommand): " + $($script:localizedData.DiskNotFoundMessage -f $DiskIdType,$DiskId) + ) -join '' ) + + return $false +} # function Test-TargetResource + +Export-ModuleMember -Function *-TargetResource diff --git a/deployment/dsc/azshcihost/StorageDsc/5.0.1/DSCResources/DSC_WaitForDisk/DSC_WaitForDisk.schema.mof b/deployment/dsc/azshcihost/StorageDsc/5.0.1/DSCResources/DSC_WaitForDisk/DSC_WaitForDisk.schema.mof new file mode 100644 index 0000000..0ce2f8c --- /dev/null +++ b/deployment/dsc/azshcihost/StorageDsc/5.0.1/DSCResources/DSC_WaitForDisk/DSC_WaitForDisk.schema.mof @@ -0,0 +1,10 @@ + +[ClassVersion("1.0.0.0"), FriendlyName("WaitForDisk")] +class DSC_WaitForDisk : OMI_BaseResource +{ + [Key, Description("Specifies the disk identifier for the disk to wait for.")] String DiskId; + [Write, Description("Specifies the identifier type the DiskId contains. Defaults to Number."), ValueMap{"Number","UniqueId","Guid","Location"}, Values{"Number","UniqueId","Guid","Location"}] String DiskIdType; + [Write, Description("Specifies the number of seconds to wait for the disk to become available.")] Uint32 RetryIntervalSec; + [Write, Description("The number of times to loop the retry interval while waiting for the disk.")] Uint32 RetryCount; + [Read, Description("Will indicate whether Disk is available.")] Boolean IsAvailable; +}; diff --git a/deployment/dsc/azshcihost/StorageDsc/5.0.1/DSCResources/DSC_WaitForDisk/README.md b/deployment/dsc/azshcihost/StorageDsc/5.0.1/DSCResources/DSC_WaitForDisk/README.md new file mode 100644 index 0000000..242e0d9 --- /dev/null +++ b/deployment/dsc/azshcihost/StorageDsc/5.0.1/DSCResources/DSC_WaitForDisk/README.md @@ -0,0 +1,29 @@ +# Description + +This resource is used to wait for a disk to become available. +The disk to wait for is selected by specifying the _DiskId_ and optionally +_DiskIdType_. +The _DiskId_ value can be a _Disk Number_, _Unique Id_, _Guid_ or _Location_. + +**Important: The _Disk Number_ is not a reliable method of selecting a disk because +it has been shown to change between reboots in some environments. +It is recommended to use the _Unique Id_ if possible.** + +The _Disk Number_, _Unique Id_, _Guid_ and _Location_ can be identified for a +disk by using the PowerShell command: + +```powershell +Get-Disk | Select-Object -Property FriendlyName,DiskNumber,UniqueId,Guid,Location +``` + +Note: The _Guid_ for a disk is only assigned once the partition table for the disk +has been created (e.g. the disk has been initialized). Therefore to use this method +of disk selection the disk must have been initialized by some other method. + +## Known Issues + +### Null Location + +The _Location_ for a disk may be `null` for some types of disk, +e.g. file-based virtual disks. Physical disks or Virtual disks provided via a +hypervisor or other hardware virtualization platform should not be affected. diff --git a/deployment/dsc/azshcihost/StorageDsc/5.0.1/DSCResources/DSC_WaitForDisk/en-US/DSC_WaitForDisk.strings.psd1 b/deployment/dsc/azshcihost/StorageDsc/5.0.1/DSCResources/DSC_WaitForDisk/en-US/DSC_WaitForDisk.strings.psd1 new file mode 100644 index 0000000..b7fb73b --- /dev/null +++ b/deployment/dsc/azshcihost/StorageDsc/5.0.1/DSCResources/DSC_WaitForDisk/en-US/DSC_WaitForDisk.strings.psd1 @@ -0,0 +1,8 @@ +ConvertFrom-StringData @' + GettingWaitForDiskStatusMessage = Getting Wait for Disk status for disk with {0} '{1}'. + CheckingForDiskStatusMessage = Checking for disk with {0} '{1}'. + DiskFoundMessage = Found disk with {0} '{1}' named '{2}'. + DiskNotFoundRetryingMessage = Disk with {0} '{1}' not found, retrying in {2} seconds. + DiskNotFoundAfterError = Disk with {0} '{1}' not found after {2} counts. + DiskNotFoundMessage = Disk with {0} '{1}' not found. +'@ diff --git a/deployment/dsc/azshcihost/StorageDsc/5.0.1/DSCResources/DSC_WaitForVolume/DSC_WaitForVolume.psm1 b/deployment/dsc/azshcihost/StorageDsc/5.0.1/DSCResources/DSC_WaitForVolume/DSC_WaitForVolume.psm1 new file mode 100644 index 0000000..1cd6b28 --- /dev/null +++ b/deployment/dsc/azshcihost/StorageDsc/5.0.1/DSCResources/DSC_WaitForVolume/DSC_WaitForVolume.psm1 @@ -0,0 +1,205 @@ +$modulePath = Join-Path -Path (Split-Path -Path (Split-Path -Path $PSScriptRoot -Parent) -Parent) -ChildPath 'Modules' + +# Import the Storage Common Module. +Import-Module -Name (Join-Path -Path $modulePath ` + -ChildPath (Join-Path -Path 'StorageDsc.Common' ` + -ChildPath 'StorageDsc.Common.psm1')) + +Import-Module -Name (Join-Path -Path $modulePath -ChildPath 'DscResource.Common') + +# Import Localization Strings. +$script:localizedData = Get-LocalizedData -DefaultUICulture 'en-US' + +<# + .SYNOPSIS + Returns the current state of the wait for drive resource. + + .PARAMETER DriveLetter + Specifies the name of the drive to wait for. + + .PARAMETER RetryIntervalSec + Specifies the number of seconds to wait for the drive to become available. + + .PARAMETER RetryCount + The number of times to loop the retry interval while waiting for the drive. +#> +function Get-TargetResource +{ + [CmdletBinding()] + [OutputType([System.Collections.Hashtable])] + param + ( + [Parameter(Mandatory = $true)] + [String] + $DriveLetter, + + [Parameter()] + [UInt32] + $RetryIntervalSec = 10, + + [Parameter()] + [UInt32] + $RetryCount = 60 + ) + + Write-Verbose -Message ( @( + "$($MyInvocation.MyCommand): " + $($script:localizedData.GettingWaitForVolumeStatusMessage -f $DriveLetter) + ) -join '' ) + + # Validate the DriveLetter parameter + $DriveLetter = Assert-DriveLetterValid -DriveLetter $DriveLetter + + $returnValue = @{ + DriveLetter = $DriveLetter + RetryIntervalSec = $RetryIntervalSec + RetryCount = $RetryCount + } + return $returnValue +} # function Get-TargetResource + +<# + .SYNOPSIS + Sets the current state of the wait for drive resource. + + .PARAMETER DriveLetter + Specifies the name of the drive to wait for. + + .PARAMETER RetryIntervalSec + Specifies the number of seconds to wait for the drive to become available. + + .PARAMETER RetryCount + The number of times to loop the retry interval while waiting for the drive. +#> +function Set-TargetResource +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [String] + $DriveLetter, + + [Parameter()] + [UInt32] + $RetryIntervalSec = 10, + + [Parameter()] + [UInt32] + $RetryCount = 60 + ) + + Write-Verbose -Message ( @( + "$($MyInvocation.MyCommand): " + $($script:localizedData.CheckingForVolumeStatusMessage -f $DriveLetter) + ) -join '' ) + + # Validate the DriveLetter parameter + $DriveLetter = Assert-DriveLetterValid -DriveLetter $DriveLetter + + $volumeFound = $false + + for ($count = 0; $count -lt $RetryCount; $count++) + { + $volume = Get-Volume -DriveLetter $DriveLetter -ErrorAction SilentlyContinue + if ($volume) + { + Write-Verbose -Message ( @( + "$($MyInvocation.MyCommand): " + $($script:localizedData.VolumeFoundMessage -f $DriveLetter) + ) -join '' ) + + $volumeFound = $true + break + } + else + { + Write-Verbose -Message ( @( + "$($MyInvocation.MyCommand): " + $($script:localizedData.VolumeNotFoundRetryingMessage -f $DriveLetter,$RetryIntervalSec) + ) -join '' ) + + Start-Sleep -Seconds $RetryIntervalSec + + <# + This command forces a refresh of the PS Drive subsystem. + So triggers any "missing" drives to show up. + #> + $null = Get-PSDrive + } # if + } # for + + if (-not $volumeFound) + { + New-InvalidOperationException ` + -Message $($script:localizedData.VolumeNotFoundAfterError -f $DriveLetter,$RetryCount) + } # if +} # function Set-TargetResource + +<# + .SYNOPSIS + Tests the current state of the wait for drive resource. + + .PARAMETER DriveLetter + Specifies the name of the drive to wait for. + + .PARAMETER RetryIntervalSec + Specifies the number of seconds to wait for the drive to become available. + + .PARAMETER RetryCount + The number of times to loop the retry interval while waiting for the drive. +#> +function Test-TargetResource +{ + [CmdletBinding()] + [OutputType([System.Boolean])] + param + ( + [Parameter(Mandatory = $true)] + [String] + $DriveLetter, + + [Parameter()] + [UInt32] + $RetryIntervalSec = 10, + + [Parameter()] + [UInt32] + $RetryCount = 60 + ) + + Write-Verbose -Message ( @( + "$($MyInvocation.MyCommand): " + $($script:localizedData.CheckingForVolumeStatusMessage -f $DriveLetter) + ) -join '' ) + + # Validate the DriveLetter parameter + $DriveLetter = Assert-DriveLetterValid -DriveLetter $DriveLetter + + <# + This command forces a refresh of the PS Drive subsystem. + So triggers any "missing" drives to show up. + #> + $null = Get-PSDrive + + $volume = Get-Volume -DriveLetter $DriveLetter -ErrorAction SilentlyContinue + + if ($volume) + { + Write-Verbose -Message ( @( + "$($MyInvocation.MyCommand): " + $($script:localizedData.VolumeFoundMessage -f $DriveLetter) + ) -join '' ) + + return $true + } + + Write-Verbose -Message ( @( + "$($MyInvocation.MyCommand): " + $($script:localizedData.VolumeNotFoundMessage -f $DriveLetter) + ) -join '' ) + + return $false +} # function Test-TargetResource + +Export-ModuleMember -Function *-TargetResource diff --git a/deployment/dsc/azshcihost/StorageDsc/5.0.1/DSCResources/DSC_WaitForVolume/DSC_WaitForVolume.schema.mof b/deployment/dsc/azshcihost/StorageDsc/5.0.1/DSCResources/DSC_WaitForVolume/DSC_WaitForVolume.schema.mof new file mode 100644 index 0000000..28b79f5 --- /dev/null +++ b/deployment/dsc/azshcihost/StorageDsc/5.0.1/DSCResources/DSC_WaitForVolume/DSC_WaitForVolume.schema.mof @@ -0,0 +1,8 @@ + +[ClassVersion("1.0.0.0"), FriendlyName("WaitForVolume")] +class DSC_WaitForVolume : OMI_BaseResource +{ + [Key, Description("Specifies the drive letter of the volume to wait for.")] String DriveLetter; + [Write, Description("Specifies the number of seconds to wait for the volume to become available.")] Uint32 RetryIntervalSec; + [Write, Description("The number of times to loop the retry interval while waiting for the volume.")] Uint32 RetryCount; +}; diff --git a/deployment/dsc/azshcihost/StorageDsc/5.0.1/DSCResources/DSC_WaitForVolume/README.md b/deployment/dsc/azshcihost/StorageDsc/5.0.1/DSCResources/DSC_WaitForVolume/README.md new file mode 100644 index 0000000..7eede33 --- /dev/null +++ b/deployment/dsc/azshcihost/StorageDsc/5.0.1/DSCResources/DSC_WaitForVolume/README.md @@ -0,0 +1,3 @@ +# Description + +The resource is used to wait for a drive to be mounted and become available. diff --git a/deployment/dsc/azshcihost/StorageDsc/5.0.1/DSCResources/DSC_WaitForVolume/en-US/DSC_WaitForVolume.strings.psd1 b/deployment/dsc/azshcihost/StorageDsc/5.0.1/DSCResources/DSC_WaitForVolume/en-US/DSC_WaitForVolume.strings.psd1 new file mode 100644 index 0000000..0a8342a --- /dev/null +++ b/deployment/dsc/azshcihost/StorageDsc/5.0.1/DSCResources/DSC_WaitForVolume/en-US/DSC_WaitForVolume.strings.psd1 @@ -0,0 +1,8 @@ +ConvertFrom-StringData @' + GettingWaitForVolumeStatusMessage = Getting Wait for Volume status for volume '{0}'. + CheckingForVolumeStatusMessage = Checking for volume '{0}'. + VolumeFoundMessage = Found volume '{0}'. + VolumeNotFoundRetryingMessage = Volume '{0}' not found, retrying in {1} seconds. + VolumeNotFoundAfterError = Volume '{0}' not found after {1} counts. + VolumeNotFoundMessage = Volume '{0}' not found. +'@ diff --git a/deployment/dsc/azshcihost/StorageDsc/5.0.1/Modules/DscResource.Common/0.9.3/DscResource.Common.psd1 b/deployment/dsc/azshcihost/StorageDsc/5.0.1/Modules/DscResource.Common/0.9.3/DscResource.Common.psd1 new file mode 100644 index 0000000..0c18ec0 --- /dev/null +++ b/deployment/dsc/azshcihost/StorageDsc/5.0.1/Modules/DscResource.Common/0.9.3/DscResource.Common.psd1 @@ -0,0 +1,73 @@ +@{ + # Script module or binary module file associated with this manifest. + RootModule = 'DscResource.Common.psm1' + + # Version number of this module. + ModuleVersion = '0.9.3' + + # ID used to uniquely identify this module + GUID = '9c9daa5b-5c00-472d-a588-c96e8e498450' + + # Author of this module + Author = 'DSC Community' + + # Company or vendor of this module + CompanyName = 'DSC Community' + + # Copyright statement for this module + Copyright = 'Copyright the DSC Community contributors. All rights reserved.' + + # Description of the functionality provided by this module + Description = 'Common functions used in DSC Resources' + + # Minimum version of the PowerShell engine required by this module + PowerShellVersion = '4.0' + + # Functions to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no functions to export. + FunctionsToExport = @('Assert-BoundParameter','Assert-IPAddress','Assert-Module','Compare-ResourcePropertyState','ConvertTo-CimInstance','ConvertTo-HashTable','Get-LocalizedData','Get-TemporaryFolder','New-InvalidArgumentException','New-InvalidDataException','New-InvalidOperationException','New-InvalidResultException','New-NotImplementedException','New-ObjectNotFoundException','Remove-CommonParameter','Set-DscMachineRebootRequired','Set-PSModulePath','Test-DscParameterState','Test-IsNanoServer') + + # Cmdlets to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no cmdlets to export. + CmdletsToExport = @() + + # Variables to export from this module + VariablesToExport = @() + + # Aliases to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no aliases to export. + AliasesToExport = @() + + # Private data to pass to the module specified in RootModule/ModuleToProcess. This may also contain a PSData hashtable with additional module metadata used by PowerShell. + PrivateData = @{ + + PSData = @{ + # Tags applied to this module. These help with module discovery in online galleries. + Tags = @('DSC', 'Localization') + + # A URL to the license for this module. + LicenseUri = 'https://github.com/dsccommunity/DscResource.Common/blob/master/LICENSE' + + # A URL to the main website for this project. + ProjectUri = 'https://github.com/dsccommunity/DscResource.Common' + + # A URL to an icon representing this module. + IconUri = 'https://dsccommunity.org/images/DSC_Logo_300p.png' + + # ReleaseNotes of this module + ReleaseNotes = '## [0.9.3] - 2020-07-25 + +## Fixed + +- Correction to `Test-DscParameterState` returning false positive when parameter + with an empty array is passed in `DesriedValues` or `CurrentValues` - fixes + [issue #53](https://github.com/dsccommunity/DscResource.Common/issues/53). + +' + + Prerelease = '' + } # End of PSData hashtable + + } # End of PrivateData hashtable +} + + + + diff --git a/deployment/dsc/azshcihost/StorageDsc/5.0.1/Modules/DscResource.Common/0.9.3/DscResource.Common.psm1 b/deployment/dsc/azshcihost/StorageDsc/5.0.1/Modules/DscResource.Common/0.9.3/DscResource.Common.psm1 new file mode 100644 index 0000000..17d1159 --- /dev/null +++ b/deployment/dsc/azshcihost/StorageDsc/5.0.1/Modules/DscResource.Common/0.9.3/DscResource.Common.psm1 @@ -0,0 +1,2179 @@ +#Region './prefix.ps1' 0 +$script:modulesFolderPath = Split-Path -Path $PSScriptRoot -Parent +#EndRegion './prefix.ps1' 1 +#Region './Private/Test-DscObjectHasProperty.ps1' 0 +<# + .SYNOPSIS + Tests if an object has a property. + + .DESCRIPTION + Tests if the specified object has the specified property and return + $true or $false. + + .PARAMETER Object + Specifies the object to test for the specified property. + + .PARAMETER PropertyName + Specifies the property name to test for. + + .EXAMPLE + Test-DscObjectHasProperty -Object 'AnyString' -PropertyName 'Length' +#> +function Test-DscObjectHasProperty +{ + [CmdletBinding()] + [OutputType([System.Boolean])] + param + ( + [Parameter(Mandatory = $true)] + [System.Object] + $Object, + + [Parameter(Mandatory = $true)] + [System.String] + $PropertyName + ) + + if ($Object.PSObject.Properties.Name -contains $PropertyName) + { + return [System.Boolean] $Object.$PropertyName + } + + return $false +} +#EndRegion './Private/Test-DscObjectHasProperty.ps1' 39 +#Region './Private/Test-DscPropertyState.ps1' 0 +<# + .SYNOPSIS + Compares the current and the desired value of a property. + + .DESCRIPTION + This function is used to compare the current and the desired value of a + property. + + .PARAMETER Values + This is set to a hash table with the current value (the CurrentValue key) + and desired value (the DesiredValue key). + + .EXAMPLE + Test-DscPropertyState -Values @{ + CurrentValue = 'John' + DesiredValue = 'Alice' + } + + .EXAMPLE + Test-DscPropertyState -Values @{ + CurrentValue = 1 + DesiredValue = 2 + } + + .NOTES + This function is used by the cmdlet Compare-ResourcePropertyState. +#> +function Test-DscPropertyState +{ + [CmdletBinding()] + [OutputType([System.Boolean])] + param + ( + [Parameter(Mandatory = $true)] + [System.Collections.Hashtable] + $Values + ) + + if ($null -eq $Values.CurrentValue -and $null -eq $Values.DesiredValue) + { + # Both values are $null so return $true + $returnValue = $true + } + elseif ($null -eq $Values.CurrentValue -or $null -eq $Values.DesiredValue) + { + # Either CurrentValue or DesiredValue are $null so return $false + $returnValue = $false + } + elseif ( + $Values.DesiredValue -is [Microsoft.Management.Infrastructure.CimInstance[]] ` + -or $Values.DesiredValue -is [System.Array] -and $Values.DesiredValue[0] -is [Microsoft.Management.Infrastructure.CimInstance] + ) + { + if (-not $Values.ContainsKey('KeyProperties')) + { + $errorMessage = $script:localizedData.KeyPropertiesMissing + + New-InvalidOperationException -Message $errorMessage + } + + $propertyState = @() + + <# + It is a collection of CIM instances, then recursively call + Test-DscPropertyState for each CIM instance in the collection. + #> + foreach ($desiredCimInstance in $Values.DesiredValue) + { + $currentCimInstance = $Values.CurrentValue + + <# + Use the CIM instance Key properties to filter out the current + values if the exist. + #> + foreach ($keyProperty in $Values.KeyProperties) + { + $currentCimInstance = $currentCimInstance | + Where-Object -Property $keyProperty -EQ -Value $desiredCimInstance.$keyProperty + } + + if ($currentCimInstance.Count -gt 1) + { + $errorMessage = $script:localizedData.TooManyCimInstances + + New-InvalidOperationException -Message $errorMessage + } + + if ($currentCimInstance) + { + $keyCimInstanceProperties = $currentCimInstance.CimInstanceProperties | + Where-Object -FilterScript { + $_.Name -in $Values.KeyProperties + } + + <# + For each key property build a string representation of the + property name and its value. + #> + $keyPropertyValues = $keyCimInstanceProperties.ForEach({'{0}="{1}"' -f $_.Name, ($_.Value -join ',')}) + + Write-Debug -Message ( + $script:localizedData.TestingCimInstance -f @( + $currentCimInstance.CimClass.CimClassName, + ($keyPropertyValues -join ';') + ) + ) + } + else + { + $keyCimInstanceProperties = $desiredCimInstance.CimInstanceProperties | + Where-Object -FilterScript { + $_.Name -in $Values.KeyProperties + } + + <# + For each key property build a string representation of the + property name and its value. + #> + $keyPropertyValues = $keyCimInstanceProperties.ForEach({'{0}="{1}"' -f $_.Name, ($_.Value -join ',')}) + + Write-Debug -Message ( + $script:localizedData.MissingCimInstance -f @( + $desiredCimInstance.CimClass.CimClassName, + ($keyPropertyValues -join ';') + ) + ) + } + + # Recursively call Test-DscPropertyState with the CimInstance to evaluate. + $propertyState += Test-DscPropertyState -Values @{ + CurrentValue = $currentCimInstance + DesiredValue = $desiredCimInstance + } + } + + # Return $false if one property is found to not be in desired state. + $returnValue = -not ($false -in $propertyState) + } + elseif ($Values.DesiredValue -is [Microsoft.Management.Infrastructure.CimInstance]) + { + $propertyState = @() + + <# + It is a CIM instance, recursively call Test-DscPropertyState for each + CIM instance property. + #> + $desiredCimInstanceProperties = $Values.DesiredValue.CimInstanceProperties | + Select-Object -Property @('Name', 'Value') + + if ($desiredCimInstanceProperties) + { + foreach ($desiredCimInstanceProperty in $desiredCimInstanceProperties) + { + <# + Recursively call Test-DscPropertyState to evaluate each property + in the CimInstance. + #> + $propertyState += Test-DscPropertyState -Values @{ + CurrentValue = $Values.CurrentValue.($desiredCimInstanceProperty.Name) + DesiredValue = $desiredCimInstanceProperty.Value + } + } + } + else + { + if ($Values.CurrentValue.CimInstanceProperties.Count -gt 0) + { + # Current value did not have any CIM properties, but desired state has. + $propertyState += $false + } + } + + # Return $false if one property is found to not be in desired state. + $returnValue = -not ($false -in $propertyState) + } + elseif ($Values.DesiredValue -is [System.Array] -or $Values.CurrentValue -is [System.Array]) + { + $compareObjectParameters = @{ + ReferenceObject = $Values.CurrentValue + DifferenceObject = $Values.DesiredValue + } + + $arrayCompare = Compare-Object @compareObjectParameters + + if ($null -ne $arrayCompare) + { + Write-Debug -Message $script:localizedData.ArrayDoesNotMatch + + $arrayCompare | + ForEach-Object -Process { + if ($_.SideIndicator -eq '=>') + { + Write-Debug -Message ( + $script:localizedData.ArrayValueIsAbsent -f $_.InputObject + ) + } + else + { + Write-Debug -Message ( + $script:localizedData.ArrayValueIsPresent -f $_.InputObject + ) + } + } + + $returnValue = $false + } + else + { + $returnValue = $true + } + } + elseif ($Values.CurrentValue -ne $Values.DesiredValue) + { + $desiredType = $Values.DesiredValue.GetType() + + $returnValue = $false + + $supportedTypes = @( + 'String' + 'Int32' + 'UInt32' + 'Int16' + 'UInt16' + 'Single' + 'Boolean' + ) + + if ($desiredType.Name -notin $supportedTypes) + { + Write-Warning -Message ($script:localizedData.UnableToCompareType -f $desiredType.Name) + } + else + { + Write-Debug -Message ( + $script:localizedData.PropertyValueOfTypeDoesNotMatch ` + -f $desiredType.Name, $Values.CurrentValue, $Values.DesiredValue + ) + } + } + else + { + $returnValue = $true + } + + return $returnValue +} +#EndRegion './Private/Test-DscPropertyState.ps1' 246 +#Region './Public/Assert-BoundParameter.ps1' 0 +<# + .SYNOPSIS + Throws an error if there is a bound parameter that exists in both the + mutually exclusive lists. + + .DESCRIPTION + Throws an error if there is a bound parameter that exists in both the + mutually exclusive lists. + + .PARAMETER BoundParameterList + The parameters that should be evaluated against the mutually exclusive + lists MutuallyExclusiveList1 and MutuallyExclusiveList2. This parameter is + normally set to the $PSBoundParameters variable. + + .PARAMETER MutuallyExclusiveList1 + An array of parameter names that are not allowed to be bound at the + same time as those in MutuallyExclusiveList2. + + .PARAMETER MutuallyExclusiveList2 + An array of parameter names that are not allowed to be bound at the + same time as those in MutuallyExclusiveList1. + + .EXAMPLE + $assertBoundParameterParameters = @{ + BoundParameterList = $PSBoundParameters + MutuallyExclusiveList1 = @( + 'Parameter1' + ) + MutuallyExclusiveList2 = @( + 'Parameter2' + ) + } + + Assert-BoundParameter @assertBoundParameterParameters + + This example throws an exception if `$PSBoundParameters` contains both + the parameters `Parameter1` and `Parameter2`. +#> +function Assert-BoundParameter +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [AllowEmptyCollection()] + [System.Collections.Hashtable] + $BoundParameterList, + + [Parameter(Mandatory = $true)] + [System.String[]] + $MutuallyExclusiveList1, + + [Parameter(Mandatory = $true)] + [System.String[]] + $MutuallyExclusiveList2 + ) + + $itemFoundFromList1 = $BoundParameterList.Keys.Where({ $_ -in $MutuallyExclusiveList1 }) + $itemFoundFromList2 = $BoundParameterList.Keys.Where({ $_ -in $MutuallyExclusiveList2 }) + + if ($itemFoundFromList1.Count -gt 0 -and $itemFoundFromList2.Count -gt 0) + { + $errorMessage = ` + $script:localizedData.ParameterUsageWrong ` + -f ($MutuallyExclusiveList1 -join "','"), ($MutuallyExclusiveList2 -join "','") + + New-InvalidArgumentException -ArgumentName 'Parameters' -Message $errorMessage + } +} +#EndRegion './Public/Assert-BoundParameter.ps1' 69 +#Region './Public/Assert-IPAddress.ps1' 0 +<# + .SYNOPSIS + Asserts that the specified IP address is valid. + + .DESCRIPTION + Checks the IP address so that it is valid and do not conflict with address + family. If any problems are detected an exception will be thrown. + + .PARAMETER AddressFamily + IP address family that the supplied Address should be in. Valid values are + 'IPv4' or 'IPv6'. + + .PARAMETER Address + Specifies an IPv4 or IPv6 address. + + .EXAMPLE + Assert-IPAddress -Address '127.0.0.1' + + This will assert that the supplied address is a valid IPv4 address. + If it is not an exception will be thrown. + + .EXAMPLE + Assert-IPAddress -Address 'fe80:ab04:30F5:002b::1' + + This will assert that the supplied address is a valid IPv6 address. + If it is not an exception will be thrown. + + .EXAMPLE + Assert-IPAddress -Address 'fe80:ab04:30F5:002b::1' -AddressFamily 'IPv6' + + This will assert that address is valid and that it matches the + supplied address family. If the supplied address family does not match + the address an exception will be thrown. +#> +function Assert-IPAddress +{ + [CmdletBinding()] + param + ( + [Parameter()] + [ValidateSet('IPv4', 'IPv6')] + [System.String] + $AddressFamily, + + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $Address + ) + + [System.Net.IPAddress] $ipAddress = $null + + if (-not ([System.Net.IPAddress]::TryParse($Address, [ref] $ipAddress))) + { + New-InvalidArgumentException ` + -Message ($script:localizedData.AddressFormatError -f $Address) ` + -ArgumentName 'Address' + } + + if ($AddressFamily) + { + switch ($AddressFamily) + { + 'IPv4' + { + if ($ipAddress.AddressFamily -ne [System.Net.Sockets.AddressFamily]::InterNetwork.ToString()) + { + New-InvalidArgumentException ` + -Message ($script:localizedData.AddressIPv6MismatchError -f $Address, $AddressFamily) ` + -ArgumentName 'AddressFamily' + } + } + + 'IPv6' + { + if ($ipAddress.AddressFamily -ne [System.Net.Sockets.AddressFamily]::InterNetworkV6.ToString()) + { + New-InvalidArgumentException ` + -Message ($script:localizedData.AddressIPv4MismatchError -f $Address, $AddressFamily) ` + -ArgumentName 'AddressFamily' + } + } + } + } +} +#EndRegion './Public/Assert-IPAddress.ps1' 85 +#Region './Public/Assert-Module.ps1' 0 +<# + .SYNOPSIS + Assert if the specific module is available to be imported. + + .DESCRIPTION + Assert if the specific module is available to be imported. + + .PARAMETER ModuleName + Specifies the name of the module to assert. + + .PARAMETER ImportModule + Specfiies to import the module if it is asserted. + + .EXAMPLE + Assert-Module -ModuleName 'DhcpServer' + + This asserts that the module DhcpServer is available on the system. +#> +function Assert-Module +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $ModuleName, + + [Parameter()] + [System.Management.Automation.SwitchParameter] + $ImportModule + ) + + if (-not (Get-Module -Name $ModuleName -ListAvailable)) + { + $errorMessage = $script:localizedData.ModuleNotFound -f $ModuleName + New-ObjectNotFoundException -Message $errorMessage + } + + if ($ImportModule) + { + Import-Module -Name $ModuleName + } +} +#EndRegion './Public/Assert-Module.ps1' 43 +#Region './Public/Compare-ResourcePropertyState.ps1' 0 +<# + .SYNOPSIS + Compare current and desired property values for any DSC resource. + + .DESCRIPTION + This function is used to compare current and desired property values for any + DSC resource, and return a hashtable with the metadata from the comparison. + + .PARAMETER CurrentValues + The current values that should be compared to to desired values. Normally + the values returned from Get-TargetResource. + + .PARAMETER DesiredValues + The values set in the configuration and is provided in the call to the + functions *-TargetResource, and that will be compared against current + values. Normally set to $PSBoundParameters. + + .PARAMETER Properties + An array of property names, from the keys provided in DesiredValues, that + will be compared. If this parameter is left out, all the keys in the + DesiredValues will be compared. + + .PARAMETER IgnoreProperties + An array of property names, from the keys provided in DesiredValues, that + will be ignored in the comparison. If this parameter is left out, all the + keys in the DesiredValues will be compared. + + .PARAMETER CimInstanceKeyProperties + A hashtable containing a key for each property that contain a collection + of CimInstances and the value is an array of strings of the CimInstance + key properties. + @{ + Permission = @('State') + } + + .EXAMPLE + $compareTargetResourceStateParameters = @{ + CurrentValues = (Get-TargetResource $PSBoundParameters) + DesiredValues = $PSBoundParameters + } + + $propertyState = Compare-ResourcePropertyState @compareTargetResourceStateParameters + + This examples call Compare-ResourcePropertyState with the current state + and the desired state and returns a hashtable array of all the properties + that was evaluated based on the properties pass in the parameter DesiredValues. +#> +function Compare-ResourcePropertyState +{ + [CmdletBinding()] + [OutputType([System.Collections.Hashtable[]])] + param + ( + [Parameter(Mandatory = $true)] + [System.Collections.Hashtable] + $CurrentValues, + + [Parameter(Mandatory = $true)] + [System.Collections.Hashtable] + $DesiredValues, + + [Parameter()] + [ValidateNotNullOrEmpty()] + [System.String[]] + $Properties, + + [Parameter()] + [ValidateNotNullOrEmpty()] + [System.String[]] + $IgnoreProperties, + + [Parameter()] + [ValidateNotNull()] + [System.Collections.Hashtable] + $CimInstanceKeyProperties = @{} + ) + + if ($PSBoundParameters.ContainsKey('Properties')) + { + # Filter out the parameters (keys) not specified in Properties + $desiredValuesToRemove = $DesiredValues.Keys | + Where-Object -FilterScript { + $_ -notin $Properties + } + + $desiredValuesToRemove | + ForEach-Object -Process { + $DesiredValues.Remove($_) + } + } + else + { + <# + Remove any common parameters that might be part of DesiredValues, + if it $PSBoundParameters was used to pass the desired values. + #> + $commonParametersToRemove = $DesiredValues.Keys | + Where-Object -FilterScript { + $_ -in [System.Management.Automation.PSCmdlet]::CommonParameters ` + -or $_ -in [System.Management.Automation.PSCmdlet]::OptionalCommonParameters + } + + $commonParametersToRemove | + ForEach-Object -Process { + $DesiredValues.Remove($_) + } + } + + # Remove any properties that should be ignored. + if ($PSBoundParameters.ContainsKey('IgnoreProperties')) + { + $IgnoreProperties | + ForEach-Object -Process { + if ($DesiredValues.ContainsKey($_)) + { + $DesiredValues.Remove($_) + } + } + } + + $compareTargetResourceStateReturnValue = @() + + foreach ($parameterName in $DesiredValues.Keys) + { + Write-Debug -Message ($script:localizedData.EvaluatePropertyState -f $parameterName) + + $parameterState = @{ + ParameterName = $parameterName + Expected = $DesiredValues.$parameterName + Actual = $CurrentValues.$parameterName + } + + # Check if the parameter is in compliance. + $isPropertyInDesiredState = Test-DscPropertyState -Values @{ + CurrentValue = $CurrentValues.$parameterName + DesiredValue = $DesiredValues.$parameterName + KeyProperties = $CimInstanceKeyProperties.$parameterName + } + + if ($isPropertyInDesiredState) + { + Write-Verbose -Message ($script:localizedData.PropertyInDesiredState -f $parameterName) + + $parameterState['InDesiredState'] = $true + } + else + { + Write-Verbose -Message ($script:localizedData.PropertyNotInDesiredState -f $parameterName) + + $parameterState['InDesiredState'] = $false + } + + $compareTargetResourceStateReturnValue += $parameterState + } + + return $compareTargetResourceStateReturnValue +} +#EndRegion './Public/Compare-ResourcePropertyState.ps1' 157 +#Region './Public/ConvertTo-CimInstance.ps1' 0 +<# + .SYNOPSIS + Converts a hashtable into a CimInstance array. + + .DESCRIPTION + This function is used to convert a hashtable into MSFT_KeyValuePair objects. + These are stored as an CimInstance array. DSC cannot handle hashtables but + CimInstances arrays storing MSFT_KeyValuePair. + + .PARAMETER Hashtable + A hashtable with the values to convert. + + .OUTPUTS + An object array with CimInstance objects. + + .EXAMPLE + ConvertTo-CimInstance -Hashtable @{ + String = 'a string' + Bool = $true + Int = 99 + Array = 'a, b, c' + } + + This example returns an CimInstance with the provided hashtable values. +#> +function ConvertTo-CimInstance +{ + [CmdletBinding()] + [OutputType([System.Object[]])] + param + ( + [Parameter(Mandatory = $true, ValueFromPipeline = $true, ParameterSetName = 'Hashtable')] + [System.Collections.Hashtable] + $Hashtable + ) + + process + { + foreach ($item in $Hashtable.GetEnumerator()) + { + New-CimInstance -ClassName 'MSFT_KeyValuePair' -Namespace 'root/microsoft/Windows/DesiredStateConfiguration' -Property @{ + Key = $item.Key + Value = if ($item.Value -is [array]) + { + $item.Value -join ',' + } + else + { + $item.Value + } + } -ClientOnly + } + } +} +#EndRegion './Public/ConvertTo-CimInstance.ps1' 54 +#Region './Public/ConvertTo-HashTable.ps1' 0 +<# + .SYNOPSIS + Converts CimInstances into a hashtable. + + .DESCRIPTION + This function is used to convert a CimInstance array containing + MSFT_KeyValuePair objects into a hashtable. + + .PARAMETER CimInstance + An array of CimInstances or a single CimInstance object to convert. + + .OUTPUTS + Hashtable + + .EXAMPLE + $newInstanceParameters = @{ + ClassName = 'MSFT_KeyValuePair' + Namespace = 'root/microsoft/Windows/DesiredStateConfiguration' + ClientOnly = $true + } + + $cimInstance = [Microsoft.Management.Infrastructure.CimInstance[]] ( + (New-CimInstance @newInstanceParameters -Property @{ + Key = 'FirstName' + Value = 'John' + }), + + (New-CimInstance @newInstanceParameters -Property @{ + Key = 'LastName' + Value = 'Smith' + }) + ) + + ConvertTo-HashTable -CimInstance $cimInstance + + This creates a array om CimInstances of the class name MSFT_KeyValuePair + and passes it to ConvertTo-HashTable which returns a hashtable. +#> +function ConvertTo-HashTable +{ + [CmdletBinding()] + [OutputType([System.Collections.Hashtable])] + param + ( + [Parameter(Mandatory = $true, ValueFromPipeline = $true, ParameterSetName = 'CimInstance')] + [AllowEmptyCollection()] + [Microsoft.Management.Infrastructure.CimInstance[]] + $CimInstance + ) + + begin + { + $result = @{ } + } + + process + { + foreach ($ci in $CimInstance) + { + $result.Add($ci.Key, $ci.Value) + } + } + + end + { + $result + } +} +#EndRegion './Public/ConvertTo-HashTable.ps1' 68 +#Region './Public/Get-LocalizedData.ps1' 0 +<# + .SYNOPSIS + Gets language-specific data into scripts and functions based on the UI culture + that is selected for the operating system. + Similar to Import-LocalizedData, with extra parameter 'DefaultUICulture'. + + .DESCRIPTION + The Get-LocalizedData cmdlet dynamically retrieves strings from a subdirectory + whose name matches the UI language set for the current user of the operating system. + It is designed to enable scripts to display user messages in the UI language selected + by the current user. + + Get-LocalizedData imports data from .psd1 files in language-specific subdirectories + of the script directory and saves them in a local variable that is specified in the + command. The cmdlet selects the subdirectory and file based on the value of the + $PSUICulture automatic variable. When you use the local variable in the script to + display a user message, the message appears in the user's UI language. + + You can use the parameters of G-LocalizedData to specify an alternate UI culture, + path, and file name, to add supported commands, and to suppress the error message that + appears if the .psd1 files are not found. + + The G-LocalizedData cmdlet supports the script internationalization + initiative that was introduced in Windows PowerShell 2.0. This initiative + aims to better serve users worldwide by making it easy for scripts to display + user messages in the UI language of the current user. For more information + about this and about the format of the .psd1 files, see about_Script_Internationalization. + + .PARAMETER BindingVariable + Specifies the variable into which the text strings are imported. Enter a variable + name without a dollar sign ($). + + In Windows PowerShell 2.0, this parameter is required. In Windows PowerShell 3.0, + this parameter is optional. If you omit this parameter, Import-LocalizedData + returns a hash table of the text strings. The hash table is passed down the pipeline + or displayed at the command line. + + When using Import-LocalizedData to replace default text strings specified in the + DATA section of a script, assign the DATA section to a variable and enter the name + of the DATA section variable in the value of the BindingVariable parameter. Then, + when Import-LocalizedData saves the imported content in the BindingVariable, the + imported data will replace the default text strings. If you are not specifying + default text strings, you can select any variable name. + + .PARAMETER UICulture + Specifies an alternate UI culture. The default is the value of the $PsUICulture + automatic variable. Enter a UI culture in - format, such as + en-US, de-DE, or ar-SA. + + The value of the UICulture parameter determines the language-specific subdirectory + (within the base directory) from which Import-LocalizedData gets the .psd1 file + for the script. + + The cmdlet searches for a subdirectory with the same name as the value of the + UICulture parameter or the $PsUICulture automatic variable, such as de-DE or + ar-SA. If it cannot find the directory, or the directory does not contain a .psd1 + file for the script, it searches for a subdirectory with the name of the language + code, such as de or ar. If it cannot find the subdirectory or .psd1 file, the + command fails and the data is displayed in the default language specified in the + script. + + .PARAMETER BaseDirectory + Specifies the base directory where the .psd1 files are located. The default is + the directory where the script is located. Import-LocalizedData searches for + the .psd1 file for the script in a language-specific subdirectory of the base + directory. + + .PARAMETER FileName + Specifies the name of the data file (.psd1) to be imported. Enter a file name. + You can specify a file name that does not include its .psd1 file name extension, + or you can specify the file name including the .psd1 file name extension. + + The FileName parameter is required when Import-LocalizedData is not used in a + script. Otherwise, the parameter is optional and the default value is the base + name of the script. You can use this parameter to direct Import-LocalizedData + to search for a different .psd1 file. + + For example, if the FileName is omitted and the script name is FindFiles.ps1, + Import-LocalizedData searches for the FindFiles.psd1 data file. + + .PARAMETER SupportedCommand + Specifies cmdlets and functions that generate only data. + + Use this parameter to include cmdlets and functions that you have written or + tested. For more information, see about_Script_Internationalization. + + .PARAMETER DefaultUICulture + Specifies which UICulture to default to if current UI culture or its parents + culture don't have matching data file. + + For example, if you have a data file in 'en-US' but not in 'en' or 'en-GB' and + your current culture is 'en-GB', you can default back to 'en-US'. + + .NOTES + Before using Import-LocalizedData, localize your user messages. Format the messages + for each locale (UI culture) in a hash table of key/value pairs, and save the + hash table in a file with the same name as the script and a .psd1 file name extension. + Create a directory under the script directory for each supported UI culture, and + then save the .psd1 file for each UI culture in the directory with the UI + culture name. + + For example, localize your user messages for the de-DE locale and format them in + a hash table. Save the hash table in a .psd1 file. Then create a de-DE + subdirectory under the script directory, and save the de-DE .psd1 + file in the de-DE subdirectory. Repeat this method for each locale that you support. + + Import-LocalizedData performs a structured search for the localized user + messages for a script. + + Import-LocalizedData begins the search in the directory where the script file + is located (or the value of the BaseDirectory parameter). It then searches within + the base directory for a subdirectory with the same name as the value of the + $PsUICulture variable (or the value of the UICulture parameter), such as de-DE or + ar-SA. Then, it searches in that subdirectory for a .psd1 file with the same name + as the script (or the value of the FileName parameter). + + If Import-LocalizedData cannot find a subdirectory with the name of the UI culture, + or the subdirectory does not contain a .psd1 file for the script, it searches for + a .psd1 file for the script in a subdirectory with the name of the language code, + such as de or ar. If it cannot find the subdirectory or .psd1 file, the command + fails, the data is displayed in the default language in the script, and an error + message is displayed explaining that the data could not be imported. To suppress + the message and fail gracefully, use the ErrorAction common parameter with a value + of SilentlyContinue. + + If Import-LocalizedData finds the subdirectory and the .psd1 file, it imports the + hash table of user messages into the value of the BindingVariable parameter in the + command. Then, when you display a message from the hash table in the variable, the + localized message is displayed. + + For more information, see about_Script_Internationalization. + + .EXAMPLE + $script:localizedData = Get-LocalizedData -DefaultUICulture 'en-US' + + This is an example that can be used in DSC resources to import the + localized strings and if the current UI culture localized folder does + not exist the UI culture 'en-US' is returned. +#> +function Get-LocalizedData +{ + [CmdletBinding(DefaultParameterSetName = 'DefaultUICulture')] + param + ( + [Parameter(Position = 0)] + [Alias('Variable')] + [ValidateNotNullOrEmpty()] + [System.String] + $BindingVariable, + + [Parameter(Position = 1, ParameterSetName = 'TargetedUICulture')] + [System.String] + $UICulture, + + [Parameter()] + [System.String] + $BaseDirectory, + + [Parameter()] + [System.String] + $FileName, + + [Parameter()] + [System.String[]] + $SupportedCommand, + + [Parameter(Position = 1, ParameterSetName = 'DefaultUICulture')] + [System.String] + $DefaultUICulture = 'en-US' + ) + + begin + { + <# + Because Proxy Command changes the Invocation origin, we need to be explicit + when handing the pipeline back to original command. + #> + if (!$PSBoundParameters.ContainsKey('FileName')) + { + if ($myInvocation.ScriptName) + { + $file = [System.IO.FileInfo] $myInvocation.ScriptName + } + else + { + $file = [System.IO.FileInfo] $myInvocation.MyCommand.Module.Path + } + + $FileName = $file.BaseName + + $PSBoundParameters.Add('FileName', $file.Name) + } + + if ($PSBoundParameters.ContainsKey('BaseDirectory')) + { + $callingScriptRoot = $BaseDirectory + } + else + { + $callingScriptRoot = $MyInvocation.PSScriptRoot + + $PSBoundParameters.Add('BaseDirectory', $callingScriptRoot) + } + + if ($PSBoundParameters.ContainsKey('DefaultUICulture') -and !$PSBoundParameters.ContainsKey('UICulture')) + { + <# + We don't want the resolution to eventually return the ModuleManifest + so we run the same GetFilePath() logic than here: + https://github.com/PowerShell/PowerShell/blob/master/src/Microsoft.PowerShell.Commands.Utility/commands/utility/Import-LocalizedData.cs#L302-L333 + and if we see it will return the wrong thing, set the UICulture to DefaultUI culture, and return the logic to Import-LocalizedData + #> + $currentCulture = Get-UICulture + + $evaluateDefaultCulture = $true + + <# + If the LCID is 127 then use default UI culture instead. + + See more information in issue https://github.com/dsccommunity/DscResource.Common/issues/11. + #> + if ($currentCulture.LCID -eq 127) + { + $currentCulture = New-Object -TypeName 'System.Globalization.CultureInfo' -ArgumentList @($DefaultUICulture) + $PSBoundParameters['UICulture'] = $DefaultUICulture + + $evaluateDefaultCulture = $false + } + + $languageFile = $null + + $localizedFileNames = @( + $FileName + '.psd1' + $FileName + '.strings.psd1' + ) + + while ($null -ne $currentCulture -and $currentCulture.Name -and -not $languageFile) + { + foreach ($fullFileName in $localizedFileNames) + { + $filePath = [System.IO.Path]::Combine($callingScriptRoot, $CurrentCulture.Name, $fullFileName) + + if (Test-Path -Path $filePath) + { + Write-Debug -Message "Found $filePath" + + $languageFile = $filePath + + # Set the filename to the file we found. + $PSBoundParameters['FileName'] = $fullFileName + + # Exit loop if we find the first filename. + break + } + else + { + Write-Debug -Message "File $filePath not found" + } + } + + if (-not $languageFile) + { + <# + Evaluate the parent culture if there is one. + + If the parent culture is LCID 127 then move to the default culture. + See more information in issue https://github.com/dsccommunity/DscResource.Common/issues/11. + #> + if ($currentCulture.Parent -and $currentCulture.Parent.LCID -ne 127) + { + $currentCulture = $currentCulture.Parent + } + else + { + if ($evaluateDefaultCulture) + { + $evaluateDefaultCulture = $false + + <# + Could not find localized strings file for the the operating + system UI culture. Evaluating the default UI culture (which + defaults to 'en-US' if not specifically set). + #> + $currentCulture = New-Object -TypeName 'System.Globalization.CultureInfo' -ArgumentList @($DefaultUICulture) + $PSBoundParameters['UICulture'] = $DefaultUICulture + } + else + { + <# + Already evaluated everything we could, exit and let + Import-LocalizedData throw an exception. + #> + break + } + } + } + } + + <# + Removes the parameter DefaultUICulture so that isn't used when + calling Import-LocalizedData. + #> + $null = $PSBoundParameters.Remove('DefaultUICulture') + } + + try + { + $outBuffer = $null + + if ($PSBoundParameters.TryGetValue('OutBuffer', [ref] $outBuffer)) + { + $PSBoundParameters['OutBuffer'] = 1 + } + + $wrappedCmd = $ExecutionContext.InvokeCommand.GetCommand('Microsoft.PowerShell.Utility\Import-LocalizedData', [System.Management.Automation.CommandTypes]::Cmdlet) + $scriptCmd = { & $wrappedCmd @PSBoundParameters } + + $steppablePipeline = $scriptCmd.GetSteppablePipeline($myInvocation.CommandOrigin) + $steppablePipeline.Begin($PSCmdlet) + } + catch + { + throw + } + } + + process + { + try + { + $steppablePipeline.Process($_) + } + catch + { + throw + } + } + + end + { + if ($BindingVariable -and ($valueToBind = Get-Variable -Name $BindingVariable -ValueOnly -ErrorAction 'Ignore')) + { + # Bringing the variable to the parent scope + Set-Variable -Scope 1 -Name $BindingVariable -Force -ErrorAction 'SilentlyContinue' -Value $valueToBind + } + + try + { + $steppablePipeline.End() + } + catch + { + throw + } + } +} +#EndRegion './Public/Get-LocalizedData.ps1' 356 +#Region './Public/Get-TemporaryFolder.ps1' 0 +<# + .SYNOPSIS + Returns the path of the current user's temporary folder. + + .DESCRIPTION + Returns the path of the current user's temporary folder. + + .NOTES + This is the same as doing the following + - Windows: $env:TEMP + - macOS: $env:TMPDIR + - Linux: /tmp/ + + .EXAMPLE + Get-TemporaryFolder + + Returns the current user temporary folder on the current operating system. +#> +function Get-TemporaryFolder +{ + [CmdletBinding()] + [OutputType([System.String])] + param () + + return [IO.Path]::GetTempPath() +} +#EndRegion './Public/Get-TemporaryFolder.ps1' 26 +#Region './Public/New-InvalidArgumentException.ps1' 0 +<# + .SYNOPSIS + Creates and throws an invalid argument exception. + + .DESCRIPTION + Creates and throws an invalid argument exception. + + .PARAMETER Message + The message explaining why this error is being thrown. + + .PARAMETER ArgumentName + The name of the invalid argument that is causing this error to be thrown. + + .EXAMPLE + $errorMessage = $script:localizedData.ActionCannotBeUsedInThisContextMessage ` + -f $Action, $Parameter + + New-InvalidArgumentException -ArgumentName 'Action' -Message $errorMessage +#> +function New-InvalidArgumentException +{ + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '')] + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $Message, + + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $ArgumentName + ) + + $argumentException = New-Object -TypeName 'ArgumentException' ` + -ArgumentList @($Message, $ArgumentName) + + $newObjectParameters = @{ + TypeName = 'System.Management.Automation.ErrorRecord' + ArgumentList = @($argumentException, $ArgumentName, 'InvalidArgument', $null) + } + + $errorRecord = New-Object @newObjectParameters + + throw $errorRecord +} +#EndRegion './Public/New-InvalidArgumentException.ps1' 48 +#Region './Public/New-InvalidDataException.ps1' 0 +<# + .SYNOPSIS + Creates and throws an invalid data exception. + + .DESCRIPTION + Creates and throws an invalid data exception. + + .PARAMETER ErrorId + The error Id to assign to the exception. + + .PARAMETER ErrorMessage + The error message to assign to the exception. + + .EXAMPLE + if ( -not $resultOfEvaluation ) + { + $errorMessage = $script:localizedData.InvalidData -f $Action + + New-InvalidDataException -ErrorId 'InvalidDataError' -ErrorMessage $errorMessage + } +#> +function New-InvalidDataException +{ + [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '')] + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $ErrorId, + + [Parameter(Mandatory = $true)] + [System.String] + $ErrorMessage + ) + + $errorCategory = [System.Management.Automation.ErrorCategory]::InvalidData + $exception = New-Object ` + -TypeName System.InvalidOperationException ` + -ArgumentList $ErrorMessage + $errorRecord = New-Object ` + -TypeName System.Management.Automation.ErrorRecord ` + -ArgumentList $exception, $ErrorId, $errorCategory, $null + + throw $errorRecord +} +#EndRegion './Public/New-InvalidDataException.ps1' 46 +#Region './Public/New-InvalidOperationException.ps1' 0 +<# + .SYNOPSIS + Creates and throws an invalid operation exception. + + .DESCRIPTION + Creates and throws an invalid operation exception. + + .PARAMETER Message + The message explaining why this error is being thrown. + + .PARAMETER ErrorRecord + The error record containing the exception that is causing this terminating error. + + .EXAMPLE + try + { + Start-Process @startProcessArguments + } + catch + { + $errorMessage = $script:localizedData.InstallationFailedMessage -f $Path, $processId + New-InvalidOperationException -Message $errorMessage -ErrorRecord $_ + } +#> +function New-InvalidOperationException +{ + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '')] + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $Message, + + [Parameter()] + [ValidateNotNull()] + [System.Management.Automation.ErrorRecord] + $ErrorRecord + ) + + if ($null -eq $ErrorRecord) + { + $invalidOperationException = New-Object -TypeName 'InvalidOperationException' ` + -ArgumentList @($Message) + } + else + { + $invalidOperationException = New-Object -TypeName 'InvalidOperationException' ` + -ArgumentList @($Message, $ErrorRecord.Exception) + } + + $newObjectParameters = @{ + TypeName = 'System.Management.Automation.ErrorRecord' + ArgumentList = @( + $invalidOperationException.ToString(), + 'MachineStateIncorrect', + 'InvalidOperation', + $null + ) + } + + $errorRecordToThrow = New-Object @newObjectParameters + + throw $errorRecordToThrow +} +#EndRegion './Public/New-InvalidOperationException.ps1' 66 +#Region './Public/New-InvalidResultException.ps1' 0 +<# + .SYNOPSIS + Creates and throws an invalid result exception. + + .DESCRIPTION + Creates and throws an invalid result exception. + + .PARAMETER Message + The message explaining why this error is being thrown. + + .PARAMETER ErrorRecord + The error record containing the exception that is causing this terminating error. + + .EXAMPLE + try + { + $numberOfObjects = Get-ChildItem -Path $path + if ($numberOfObjects -eq 0) + { + throw 'To few files.' + } + } + catch + { + $errorMessage = $script:localizedData.TooFewFilesMessage -f $path + New-InvalidResultException -Message $errorMessage -ErrorRecord $_ + } +#> +function New-InvalidResultException +{ + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '')] + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $Message, + + [Parameter()] + [ValidateNotNull()] + [System.Management.Automation.ErrorRecord] + $ErrorRecord + ) + + if ($null -eq $ErrorRecord) + { + $exception = New-Object -TypeName 'System.Exception' ` + -ArgumentList @($Message) + } + else + { + $exception = New-Object -TypeName 'System.Exception' ` + -ArgumentList @($Message, $ErrorRecord.Exception) + } + + $newObjectParameters = @{ + TypeName = 'System.Management.Automation.ErrorRecord' + ArgumentList = @( + $exception.ToString(), + 'MachineStateIncorrect', + 'InvalidResult', + $null + ) + } + + $errorRecordToThrow = New-Object @newObjectParameters + + throw $errorRecordToThrow +} +#EndRegion './Public/New-InvalidResultException.ps1' 70 +#Region './Public/New-NotImplementedException.ps1' 0 +<# + .SYNOPSIS + Creates and throws an not implemented exception. + + .DESCRIPTION + Creates and throws an not implemented exception. + + .PARAMETER Message + The message explaining why this error is being thrown. + + .PARAMETER ErrorRecord + The error record containing the exception that is causing this terminating error. + + .EXAMPLE + if ($runFeature) + { + $errorMessage = $script:localizedData.FeatureMissing -f $path + New-NotImplementedException -Message $errorMessage -ErrorRecord $_ + } + + Throws an not implemented exception if the variable $runFeature contains + a value. +#> +function New-NotImplementedException +{ + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '')] + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $Message, + + [Parameter()] + [ValidateNotNull()] + [System.Management.Automation.ErrorRecord] + $ErrorRecord + ) + + if ($null -eq $ErrorRecord) + { + $invalidOperationException = New-Object -TypeName 'NotImplementedException' ` + -ArgumentList @($Message) + } + else + { + $invalidOperationException = New-Object -TypeName 'NotImplementedException' ` + -ArgumentList @($Message, $ErrorRecord.Exception) + } + + $newObjectParameters = @{ + TypeName = 'System.Management.Automation.ErrorRecord' + ArgumentList = @( + $invalidOperationException.ToString(), + 'MachineStateIncorrect', + 'NotImplemented', + $null + ) + } + + $errorRecordToThrow = New-Object @newObjectParameters + + throw $errorRecordToThrow +} +#EndRegion './Public/New-NotImplementedException.ps1' 65 +#Region './Public/New-ObjectNotFoundException.ps1' 0 + +<# + .SYNOPSIS + Creates and throws an object not found exception. + + .DESCRIPTION + Creates and throws an object not found exception. + + .PARAMETER Message + The message explaining why this error is being thrown. + + .PARAMETER ErrorRecord + The error record containing the exception that is causing this terminating error. + + .EXAMPLE + try + { + Get-ChildItem -Path $path + } + catch + { + $errorMessage = $script:localizedData.PathNotFoundMessage -f $path + New-ObjectNotFoundException -Message $errorMessage -ErrorRecord $_ + } +#> +function New-ObjectNotFoundException +{ + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '')] + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $Message, + + [Parameter()] + [ValidateNotNull()] + [System.Management.Automation.ErrorRecord] + $ErrorRecord + ) + + if ($null -eq $ErrorRecord) + { + $exception = New-Object -TypeName 'System.Exception' ` + -ArgumentList @($Message) + } + else + { + $exception = New-Object -TypeName 'System.Exception' ` + -ArgumentList @($Message, $ErrorRecord.Exception) + } + + $newObjectParameters = @{ + TypeName = 'System.Management.Automation.ErrorRecord' + ArgumentList = @( + $exception.ToString(), + 'MachineStateIncorrect', + 'ObjectNotFound', + $null + ) + } + + $errorRecordToThrow = New-Object @newObjectParameters + + throw $errorRecordToThrow +} +#EndRegion './Public/New-ObjectNotFoundException.ps1' 67 +#Region './Public/Remove-CommonParameter.ps1' 0 +<# + .SYNOPSIS + Removes common parameters from a hashtable. + + .DESCRIPTION + This function serves the purpose of removing common parameters and option + common parameters from a parameter hashtable. + + .PARAMETER Hashtable + The parameter hashtable that should be pruned. + + .EXAMPLE + Remove-CommonParameter -Hashtable $PSBoundParameters + + Returns a new hashtable without the common and optional common parameters. +#> +function Remove-CommonParameter +{ + [Diagnostics.CodeAnalysis.SuppressMessageAttribute( + 'PSUseShouldProcessForStateChangingFunctions', + '', + Justification = 'ShouldProcess is not supported in DSC resources.' + )] + [OutputType([System.Collections.Hashtable])] + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [System.Collections.Hashtable] + $Hashtable + ) + + $inputClone = $Hashtable.Clone() + + $commonParameters = [System.Management.Automation.PSCmdlet]::CommonParameters + $commonParameters += [System.Management.Automation.PSCmdlet]::OptionalCommonParameters + + $Hashtable.Keys | Where-Object -FilterScript { + $_ -in $commonParameters + } | ForEach-Object -Process { + $inputClone.Remove($_) + } + + return $inputClone +} +#EndRegion './Public/Remove-CommonParameter.ps1' 45 +#Region './Public/Set-DscMachineRebootRequired.ps1' 0 +<# + .SYNOPSIS + Set the DSC reboot required status variable. + + .DESCRIPTION + Sets the global DSCMachineStatus variable to a value of 1. + This function is used to set the global variable that indicates + to the LCM that a reboot of the node is required. + + .EXAMPLE + PS C:\> Set-DscMachineRebootRequired + + Sets the $global:DSCMachineStatus variable to 1. + + .NOTES + This function is implemented so that individual resource modules + do not need to use and therefore suppress Global variables + directly. It also enables mocking to increase testability of + consumers. +#> +function Set-DscMachineRebootRequired +{ + [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '')] + # Suppressing this rule because $global:DSCMachineStatus is used to trigger a reboot. + [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidGlobalVars', '')] + <# + Suppressing this rule because $global:DSCMachineStatus is only set, + never used (by design of Desired State Configuration). + #> + [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', '')] + [CmdletBinding()] + param + ( + ) + + $global:DSCMachineStatus = 1 +} +#EndRegion './Public/Set-DscMachineRebootRequired.ps1' 37 +#Region './Public/Set-PSModulePath.ps1' 0 + +<# + .SYNOPSIS + Set environment variable PSModulePath in the current session or machine + wide. + + .DESCRIPTION + This is a wrapper to set environment variable PSModulePath in current + session or machine wide. + + .PARAMETER Path + A string with all the paths separated by semi-colons. + + .PARAMETER Machine + If set the PSModulePath will be changed machine wide. If not set, only + the current session will be changed. + + .EXAMPLE + Set-PSModulePath -Path ';' + + .EXAMPLE + Set-PSModulePath -Path ';' -Machine +#> +function Set-PSModulePath +{ + [Diagnostics.CodeAnalysis.SuppressMessageAttribute( + 'PSUseShouldProcessForStateChangingFunctions', + '', + Justification = 'ShouldProcess is not supported in DSC resources.' + )] + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $Path, + + [Parameter()] + [System.Management.Automation.SwitchParameter] + $Machine + ) + + if ($Machine.IsPresent) + { + [System.Environment]::SetEnvironmentVariable('PSModulePath', $Path, [System.EnvironmentVariableTarget]::Machine) + } + else + { + $env:PSModulePath = $Path + } +} +#EndRegion './Public/Set-PSModulePath.ps1' 52 +#Region './Public/Test-DscParameterState.ps1' 0 +<# + .SYNOPSIS + This method is used to compare current and desired values for any DSC resource. + + .DESCRIPTION + This function tests the parameter status of DSC resource parameters against + the current values present on the system. + + .PARAMETER CurrentValues + A hashtable with the current values on the system, obtained by e.g. + Get-TargetResource. + + .PARAMETER DesiredValues + The hashtable of desired values. For example $PSBoundParameters with the + desired values. + + .PARAMETER Properties + This is a list of properties in the desired values list should be checked. + If this is empty then all values in DesiredValues are checked. + + .PARAMETER ExcludeProperties + This is a list of which properties in the desired values list should be checked. + If this is empty then all values in DesiredValues are checked. + + .PARAMETER TurnOffTypeChecking + Indicates that the type of the parameter should not be checked. + + .PARAMETER ReverseCheck + Indicates that a reverse check should be done. The current and desired state + are swapped for another test. + + .PARAMETER SortArrayValues + If the sorting of array values does not matter, values are sorted internally + before doing the comparison. + + .EXAMPLE + $currentState = Get-TargetResource @PSBoundParameters + + $returnValue = Test-DscParameterState -CurrentValues $currentState -DesiredValues $PSBoundParameters + + The function Get-TargetResource is called first using all bound parameters + to get the values in the current state. The result is then compared to the + desired state by calling `Test-DscParameterState`. + + .EXAMPLE + $getTargetResourceParameters = @{ + ServerName = $ServerName + InstanceName = $InstanceName + Name = $Name + } + + $returnValue = Test-DscParameterState ` + -CurrentValues (Get-TargetResource @getTargetResourceParameters) ` + -DesiredValues $PSBoundParameters ` + -ExcludeProperties @( + 'FailsafeOperator' + 'NotificationMethod' + ) + + This compares the values in the current state against the desires state. + The function Get-TargetResource is called using just the required parameters + to get the values in the current state. The parameter 'ExcludeProperties' + is used to exclude the properties 'FailsafeOperator' and + 'NotificationMethod' from the comparison. + + .EXAMPLE + $getTargetResourceParameters = @{ + ServerName = $ServerName + InstanceName = $InstanceName + Name = $Name + } + + $returnValue = Test-DscParameterState ` + -CurrentValues (Get-TargetResource @getTargetResourceParameters) ` + -DesiredValues $PSBoundParameters ` + -Properties ServerName, Name + + This compares the values in the current state against the desires state. + The function Get-TargetResource is called using just the required parameters + to get the values in the current state. The 'Properties' parameter is used + to to only compare the properties 'ServerName' and 'Name'. +#> +function Test-DscParameterState +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [System.Object] + $CurrentValues, + + [Parameter(Mandatory = $true)] + [System.Object] + $DesiredValues, + + [Parameter()] + [System.String[]] + [Alias('ValuesToCheck')] + $Properties, + + [Parameter()] + [System.String[]] + $ExcludeProperties, + + [Parameter()] + [System.Management.Automation.SwitchParameter] + $TurnOffTypeChecking, + + [Parameter()] + [System.Management.Automation.SwitchParameter] + $ReverseCheck, + + [Parameter()] + [System.Management.Automation.SwitchParameter] + $SortArrayValues + ) + + $returnValue = $true + + if ($CurrentValues -is [Microsoft.Management.Infrastructure.CimInstance] -or + $CurrentValues -is [Microsoft.Management.Infrastructure.CimInstance[]]) + { + $CurrentValues = ConvertTo-HashTable -CimInstance $CurrentValues + } + + if ($DesiredValues -is [Microsoft.Management.Infrastructure.CimInstance] -or + $DesiredValues -is [Microsoft.Management.Infrastructure.CimInstance[]]) + { + $DesiredValues = ConvertTo-HashTable -CimInstance $DesiredValues + } + + $types = 'System.Management.Automation.PSBoundParametersDictionary', 'System.Collections.Hashtable', 'Microsoft.Management.Infrastructure.CimInstance' + + if ($DesiredValues.GetType().FullName -notin $types) + { + New-InvalidArgumentException ` + -Message ($script:localizedData.InvalidDesiredValuesError -f $DesiredValues.GetType().FullName) ` + -ArgumentName 'DesiredValues' + } + + if ($CurrentValues.GetType().FullName -notin $types) + { + New-InvalidArgumentException ` + -Message ($script:localizedData.InvalidCurrentValuesError -f $CurrentValues.GetType().FullName) ` + -ArgumentName 'CurrentValues' + } + + if ($DesiredValues -is [Microsoft.Management.Infrastructure.CimInstance] -and -not $Properties) + { + New-InvalidArgumentException ` + -Message $script:localizedData.InvalidPropertiesError ` + -ArgumentName Properties + } + + $desiredValuesClean = Remove-CommonParameter -Hashtable $DesiredValues + + if (-not $Properties) + { + $keyList = $desiredValuesClean.Keys + } + else + { + $keyList = $Properties + } + if ($ExcludeProperties) + { + $keyList = $keyList | Where-Object -FilterScript { $_ -notin $ExcludeProperties } + } + + foreach ($key in $keyList) + { + $desiredValue = $desiredValuesClean.$key + $currentValue = $CurrentValues.$key + + if ($desiredValue -is [Microsoft.Management.Infrastructure.CimInstance] -or + $desiredValue -is [Microsoft.Management.Infrastructure.CimInstance[]]) + { + $desiredValue = ConvertTo-HashTable -CimInstance $desiredValue + } + if ($currentValue -is [Microsoft.Management.Infrastructure.CimInstance] -or + $currentValue -is [Microsoft.Management.Infrastructure.CimInstance[]]) + { + $currentValue = ConvertTo-HashTable -CimInstance $currentValue + } + + if ($null -ne $desiredValue) + { + $desiredType = $desiredValue.GetType() + } + else + { + $desiredType = @{ + Name = 'Unknown' + } + } + + if ($null -ne $currentValue) + { + $currentType = $currentValue.GetType() + } + else + { + $currentType = @{ + Name = 'Unknown' + } + } + + if ($currentType.Name -ne 'Unknown' -and $desiredType.Name -eq 'PSCredential') + { + # This is a credential object. Compare only the user name + if ($currentType.Name -eq 'PSCredential' -and $currentValue.UserName -eq $desiredValue.UserName) + { + Write-Verbose -Message ($script:localizedData.MatchPsCredentialUsernameMessage -f $currentValue.UserName, $desiredValue.UserName) + continue + } + else + { + Write-Verbose -Message ($script:localizedData.NoMatchPsCredentialUsernameMessage -f $currentValue.UserName, $desiredValue.UserName) + $returnValue = $false + } + + # Assume the string is our username when the matching desired value is actually a credential + if ($currentType.Name -eq 'string' -and $currentValue -eq $desiredValue.UserName) + { + Write-Verbose -Message ($script:localizedData.MatchPsCredentialUsernameMessage -f $currentValue, $desiredValue.UserName) + continue + } + else + { + Write-Verbose -Message ($script:localizedData.NoMatchPsCredentialUsernameMessage -f $currentValue, $desiredValue.UserName) + $returnValue = $false + } + } + + if (-not $TurnOffTypeChecking) + { + if (($desiredType.Name -ne 'Unknown' -and $currentType.Name -ne 'Unknown') -and + $desiredType.FullName -ne $currentType.FullName) + { + Write-Verbose -Message ($script:localizedData.NoMatchTypeMismatchMessage -f $key, $currentType.FullName, $desiredType.FullName) + $returnValue = $false + continue + } + } + + if ($currentValue -eq $desiredValue -and -not $desiredType.IsArray) + { + Write-Verbose -Message ($script:localizedData.MatchValueMessage -f $desiredType.FullName, $key, $currentValue, $desiredValue) + continue + } + + if ($desiredValuesClean.GetType().Name -in 'HashTable', 'PSBoundParametersDictionary') + { + $checkDesiredValue = $desiredValuesClean.ContainsKey($key) + } + else + { + $checkDesiredValue = Test-DscObjectHasProperty -Object $desiredValuesClean -PropertyName $key + } + + if (-not $checkDesiredValue) + { + Write-Verbose -Message ($script:localizedData.MatchValueMessage -f $desiredType.FullName, $key, $currentValue, $desiredValue) + continue + } + + if ($desiredType.IsArray) + { + Write-Verbose -Message ($script:localizedData.TestDscParameterCompareMessage -f $key, $desiredType.FullName) + + if (-not $currentValue -and -not $desiredValue) + { + Write-Verbose -Message ($script:localizedData.MatchValueMessage -f $desiredType.FullName, $key, 'empty array', 'empty array') + continue + } + elseif (-not $currentValue) + { + Write-Verbose -Message ($script:localizedData.NoMatchValueMessage -f $desiredType.FullName, $key, $currentValue, $desiredValue) + $returnValue = $false + continue + } + elseif ($currentValue.Count -ne $desiredValue.Count) + { + Write-Verbose -Message ($script:localizedData.NoMatchValueDifferentCountMessage -f $desiredType.FullName, $key, $currentValue.Count, $desiredValue.Count) + $returnValue = $false + continue + } + else + { + $desiredArrayValues = $desiredValue + $currentArrayValues = $currentValue + + if ($SortArrayValues) + { + $desiredArrayValues = @($desiredArrayValues | Sort-Object) + $currentArrayValues = @($currentArrayValues | Sort-Object) + } + + for ($i = 0; $i -lt $desiredArrayValues.Count; $i++) + { + if ($desiredArrayValues[$i]) + { + $desiredType = $desiredArrayValues[$i].GetType() + } + else + { + $desiredType = @{ + Name = 'Unknown' + } + } + + if ($currentArrayValues[$i]) + { + $currentType = $currentArrayValues[$i].GetType() + } + else + { + $currentType = @{ + Name = 'Unknown' + } + } + + if (-not $TurnOffTypeChecking) + { + if (($desiredType.Name -ne 'Unknown' -and $currentType.Name -ne 'Unknown') -and + $desiredType.FullName -ne $currentType.FullName) + { + Write-Verbose -Message ($script:localizedData.NoMatchElementTypeMismatchMessage -f $key, $i, $currentType.FullName, $desiredType.FullName) + $returnValue = $false + continue + } + } + + #Convert a scriptblock into a string as scriptblocks are not comparable + $wasCurrentArrayValuesConverted = $false + if ($currentArrayValues[$i] -is [scriptblock]) + { + $currentArrayValues[$i] = if ($desiredArrayValues[$i] -is [string]) + { + $currentArrayValues[$i] = $currentArrayValues[$i].Invoke() + } + else + { + $currentArrayValues[$i].ToString() + } + $wasCurrentArrayValuesConverted = $true + } + if ($desiredArrayValues[$i] -is [scriptblock]) + { + $desiredArrayValues[$i] = if ($currentArrayValues[$i] -is [string] -and -not $wasCurrentArrayValuesConverted) + { + $desiredArrayValues[$i].Invoke() + } + else + { + $desiredArrayValues[$i].ToString() + } + } + + if ($desiredType -eq [System.Collections.Hashtable] -and $currentType -eq [System.Collections.Hashtable]) + { + $param = $PSBoundParameters + $param.CurrentValues = $currentArrayValues[$i] + $param.DesiredValues = $desiredArrayValues[$i] + + if ($returnValue) + { + $returnValue = Test-DscParameterState @param + } + else + { + Test-DscParameterState @param | Out-Null + } + continue + } + + if ($desiredArrayValues[$i] -ne $currentArrayValues[$i]) + { + Write-Verbose -Message ($script:localizedData.NoMatchElementValueMismatchMessage -f $i, $desiredType.FullName, $key, $currentArrayValues[$i], $desiredArrayValues[$i]) + $returnValue = $false + continue + } + else + { + Write-Verbose -Message ($script:localizedData.MatchElementValueMessage -f $i, $desiredType.FullName, $key, $currentArrayValues[$i], $desiredArrayValues[$i]) + continue + } + } + + } + } + elseif ($desiredType -eq [System.Collections.Hashtable] -and $currentType -eq [System.Collections.Hashtable]) + { + $param = $PSBoundParameters + $param.CurrentValues = $currentValue + $param.DesiredValues = $desiredValue + + if ($returnValue) + { + $returnValue = Test-DscParameterState @param + } + else + { + Test-DscParameterState @param | Out-Null + } + continue + } + else + { + #Convert a scriptblock into a string as scriptblocks are not comparable + $wasCurrentValue = $false + if ($currentValue -is [scriptblock]) + { + $currentValue = if ($desiredValue -is [string]) + { + $currentValue = $currentValue.Invoke() + } + else + { + $currentValue.ToString() + } + $wasCurrentValue = $true + } + if ($desiredValue -is [scriptblock]) + { + $desiredValue = if ($currentValue -is [string] -and -not $wasCurrentValue) + { + $desiredValue.Invoke() + } + else + { + $desiredValue.ToString() + } + } + + if ($desiredValue -ne $currentValue) + { + Write-Verbose -Message ($script:localizedData.NoMatchValueMessage -f $desiredType.FullName, $key, $currentValue, $desiredValue) + $returnValue = $false + } + } + } + + if ($ReverseCheck) + { + Write-Verbose -Message $script:localizedData.StartingReverseCheck + $reverseCheckParameters = $PSBoundParameters + $reverseCheckParameters.CurrentValues = $DesiredValues + $reverseCheckParameters.DesiredValues = $CurrentValues + $null = $reverseCheckParameters.Remove('ReverseCheck') + + if ($returnValue) + { + $returnValue = Test-DscParameterState @reverseCheckParameters + } + else + { + $null = Test-DscParameterState @reverseCheckParameters + } + } + + Write-Verbose -Message ($script:localizedData.TestDscParameterResultMessage -f $returnValue) + return $returnValue +} +#EndRegion './Public/Test-DscParameterState.ps1' 464 +#Region './Public/Test-IsNanoServer.ps1' 0 +<# + .SYNOPSIS + Tests if the current OS is a Nano server. + + .DESCRIPTION + Tests if the current OS is a Nano server. + + .EXAMPLE + Test-IsNanoServer + + Returns $true if the current operating system is Nano Server, if not $false + is returned. +#> +function Test-IsNanoServer +{ + [OutputType([System.Boolean])] + [CmdletBinding()] + param () + + $productDatacenterNanoServer = 143 + $productStandardNanoServer = 144 + + $operatingSystemSKU = (Get-CimInstance -ClassName Win32_OperatingSystem).OperatingSystemSKU + + Write-Verbose -Message ($script:localizedData.TestIsNanoServerOperatingSystemSku -f $operatingSystemSKU) + + return ($operatingSystemSKU -in ($productDatacenterNanoServer, $productStandardNanoServer)) +} +#EndRegion './Public/Test-IsNanoServer.ps1' 28 +#Region './suffix.ps1' 0 +$script:localizedData = Get-LocalizedData -DefaultUICulture 'en-US' +#EndRegion './suffix.ps1' 1 diff --git a/deployment/dsc/azshcihost/StorageDsc/5.0.1/Modules/DscResource.Common/0.9.3/en-US/DscResource.Common.strings.psd1 b/deployment/dsc/azshcihost/StorageDsc/5.0.1/Modules/DscResource.Common/0.9.3/en-US/DscResource.Common.strings.psd1 new file mode 100644 index 0000000..fd8440b --- /dev/null +++ b/deployment/dsc/azshcihost/StorageDsc/5.0.1/Modules/DscResource.Common/0.9.3/en-US/DscResource.Common.strings.psd1 @@ -0,0 +1,37 @@ +# Localized English (en-US) strings. + +ConvertFrom-StringData @' + TestIsNanoServerOperatingSystemSku = OperatingSystemSKU {0} was returned by Win32_OperatingSystem when detecting if operating system is Nano Server. (DRC0008) + ModuleNotFound = Please ensure that the PowerShell module '{0}' is installed. (DRC0009) + ParameterUsageWrong = None of the parameter(s) '{0}' may be used at the same time as any of the parameter(s) '{1}'. (DRC0010) + AddressFormatError = Address '{0}' is not in the correct format. Please correct the Address parameter in the configuration and try again. (DRC0011) + AddressIPv4MismatchError = Address '{0}' is in IPv4 format, which does not match server address family {1}. Please correct either of them in the configuration and try again. (DRC0012) + AddressIPv6MismatchError = Address '{0}' is in IPv6 format, which does not match server address family {1}. Please correct either of them in the configuration and try again. (DRC0013) + InvalidDesiredValuesError = Property 'DesiredValues' in Test-DscParameterState must be either a Hashtable or CimInstance. Type detected was '{0}'. (DRC0014) + InvalidCurrentValuesError = Property 'CurrentValues' in Test-DscParameterState must be either a Hashtable, CimInstance, or CimIntance[]. Type detected was '{0}'. (DRC0015) + InvalidPropertiesError = If 'DesiredValues' is a CimInstance then property 'Properties' must contain a value. (DRC0016) + MatchPsCredentialUsernameMessage = MATCH: PSCredential username match. Current state is '{0}' and desired state is '{1}'. (DRC0017) + NoMatchPsCredentialUsernameMessage = NOTMATCH: PSCredential username mismatch. Current state is '{0}' and desired state is '{1}'. (DRC0018) + NoMatchTypeMismatchMessage = NOTMATCH: Type mismatch for property '{0}' Current state type is '{1}' and desired type is '{2}'. (DRC0019) + MatchValueMessage = MATCH: Value (type '{0}') for property '{1}' does match. Current state is '{2}' and desired state is '{3}'. (DRC0020) + NoMatchValueMessage = NOTMATCH: Value (type '{0}') for property '{1}' does not match. Current state is '{2}' and desired state is '{3}'. (DRC0021) + NoMatchValueDifferentCountMessage = NOTMATCH: Value (type '{0}') for property '{1}' does have a different count. Current state count is '{2}' and desired state count is '{3}'. (DRC0022) + NoMatchElementTypeMismatchMessage = NOTMATCH: Type mismatch for property '{0}' Current state type of element [{1}] is '{2}' and desired type is '{3}'. (DRC0023) + NoMatchElementValueMismatchMessage = NOTMATCH: Value [{0}] (type '{1}') for property '{2}' does match. Current state is '{3}' and desired state is '{4}'. (DRC0024) + MatchElementValueMessage = MATCH: Value [{0}] (type '{1}') for property '{2}' does match. Current state is '{3}' and desired state is '{4}'. (DRC0025) + TestDscParameterResultMessage = Test-DscParameter result is '{0}'. (DRC0026) + StartingReverseCheck = Starting with a reverse check. (DRC0027) + TestDscParameterCompareMessage = Comparing values in property '{0}'. (DRC0028) + TooManyCimInstances = More than one CIM instance was returned from the current state. (DRC0029) + TestingCimInstance = Testing CIM instance '{0}' with the key properties '{1}'. (DRC0030) + MissingCimInstance = The CIM instance '{0}' with the key properties '{1}' is missing. (DRC0031) + ArrayValueIsAbsent = The array value '{0}' is absent. (DRC0032) + ArrayValueIsPresent = The array value '{0}' is present. (DRC0033) + KeyPropertiesMissing = The hashtable passed to function Test-DscPropertyState is missing the key 'KeyProperties'. This must be set to the property names that makes each instance in the CIM instance collection unique. (DRC0034) + ArrayDoesNotMatch = One or more values in an array does not match the desired state. Details of the changes are below. (DRC0035) + PropertyValueOfTypeDoesNotMatch = {0} value does not match. Current value is '{1}', but expected the value '{2}'. (DRC0036) + UnableToCompareType = Unable to compare the type {0} as it is not handled by the Test-DscPropertyState cmdlet. (DRC0037) + EvaluatePropertyState = Evaluating the state of the property '{0}'. (DRC0038) + PropertyInDesiredState = The parameter '{0}' is in desired state. (DRC0039) + PropertyNotInDesiredState = The parameter '{0}' is not in desired state. (DRC0040) +'@ diff --git a/deployment/dsc/azshcihost/StorageDsc/5.0.1/Modules/DscResource.Common/0.9.3/en-US/about_DscResource.Common.help.txt b/deployment/dsc/azshcihost/StorageDsc/5.0.1/Modules/DscResource.Common/0.9.3/en-US/about_DscResource.Common.help.txt new file mode 100644 index 0000000..2a99677 --- /dev/null +++ b/deployment/dsc/azshcihost/StorageDsc/5.0.1/Modules/DscResource.Common/0.9.3/en-US/about_DscResource.Common.help.txt @@ -0,0 +1,26 @@ +TOPIC + about_DscResource.Common + +SHORT DESCRIPTION + Common functions used in DSC tesources. + +LONG DESCRIPTION + This module contains common functions that are used in DSC resources. + +EXAMPLES + PS C:\> Get-Command -Module DscResource.Common + +NOTE: + Thank you to the DSC Community contributors who contributed to this module by + writing code, sharing opinions, and provided feedback. + +TROUBLESHOOTING NOTE: + Go to the Github repository for read about issues, submit a new issue, and read + about new releases. https://github.com/dsccommunity/DscResource.Common + +SEE ALSO + - https://github.com/dsccommunity/DscResource.Common + +KEYWORDS + DSC, Localization + diff --git a/deployment/dsc/azshcihost/StorageDsc/5.0.1/Modules/StorageDsc.Common/StorageDsc.Common.psm1 b/deployment/dsc/azshcihost/StorageDsc/5.0.1/Modules/StorageDsc.Common/StorageDsc.Common.psm1 new file mode 100644 index 0000000..d04dc93 --- /dev/null +++ b/deployment/dsc/azshcihost/StorageDsc/5.0.1/Modules/StorageDsc.Common/StorageDsc.Common.psm1 @@ -0,0 +1,231 @@ +$modulePath = Join-Path -Path (Split-Path -Path (Split-Path -Path $PSScriptRoot -Parent) -Parent) -ChildPath 'Modules' + +Import-Module -Name (Join-Path -Path $modulePath -ChildPath 'DscResource.Common') + +# Import Localization Strings +$script:localizedData = Get-LocalizedData -DefaultUICulture 'en-US' + +<# + .SYNOPSIS + Restarts a System Service + + .PARAMETER Name + Name of the service to be restarted. +#> +function Restart-ServiceIfExists +{ + [CmdletBinding()] + param + ( + [Parameter()] + [System.String] + $Name + ) + + Write-Verbose -Message ($script:localizedData.GetServiceInformation -f $Name) -Verbose + $servicesService = Get-Service @PSBoundParameters -ErrorAction Continue + + if ($servicesService) + { + Write-Verbose -Message ($script:localizedData.RestartService -f $Name) -Verbose + $servicesService | Restart-Service -Force -ErrorAction Stop -Verbose + } + else + { + Write-Verbose -Message ($script:localizedData.UnknownService -f $Name) -Verbose + } +} + +<# + .SYNOPSIS + Validates a Drive Letter, removing or adding the trailing colon if required. + + .PARAMETER DriveLetter + The Drive Letter string to validate. + + .PARAMETER Colon + Will ensure the returned string will include or exclude a colon. +#> +function Assert-DriveLetterValid +{ + [CmdletBinding()] + [OutputType([System.String])] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $DriveLetter, + + [Parameter()] + [Switch] + $Colon + ) + + $matches = @([regex]::matches($DriveLetter, '^([A-Za-z]):?$', 'IgnoreCase')) + + if (-not $matches) + { + # DriveLetter format is invalid + New-InvalidArgumentException ` + -Message $($script:localizedData.InvalidDriveLetterFormatError -f $DriveLetter) ` + -ArgumentName 'DriveLetter' + } + + # This is the drive letter without a colon + $DriveLetter = $matches.Groups[1].Value + + if ($Colon) + { + $DriveLetter = $DriveLetter + ':' + } # if + + return $DriveLetter +} # end function Assert-DriveLetterValid + +<# + .SYNOPSIS + Validates an Access Path, removing or adding the trailing slash if required. + If the Access Path does not exist or is not a folder then an exception will + be thrown. + + .PARAMETER AccessPath + The Access Path string to validate. + + .PARAMETER Slash + Will ensure the returned path will include or exclude a slash. +#> +function Assert-AccessPathValid +{ + [CmdletBinding()] + [OutputType([System.String])] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $AccessPath, + + [Parameter()] + [Switch] + $Slash + ) + + if (-not (Test-Path -Path $AccessPath -PathType Container)) + { + # AccessPath is invalid + New-InvalidArgumentException ` + -Message $($script:localizedData.InvalidAccessPathError -f $AccessPath) ` + -ArgumentName 'AccessPath' + } # if + + # Remove or Add the trailing slash + if ($AccessPath.EndsWith('\')) + { + if (-not $Slash) + { + $AccessPath = $AccessPath.TrimEnd('\') + } # if + } + else + { + if ($Slash) + { + $AccessPath = "$AccessPath\" + } # if + } # if + + return $AccessPath +} # end function Assert-AccessPathValid + +<# + .SYNOPSIS + Retrieves a Disk object matching the disk Id and Id type + provided. + + .PARAMETER DiskId + Specifies the disk identifier for the disk to retrieve. + + .PARAMETER DiskIdType + Specifies the identifier type the DiskId contains. Defaults to Number. +#> +function Get-DiskByIdentifier +{ + [CmdletBinding()] + [OutputType([Microsoft.Management.Infrastructure.CimInstance])] + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $DiskId, + + [Parameter()] + [ValidateSet('Number','UniqueId','Guid','Location')] + [System.String] + $DiskIdType = 'Number' + ) + + switch -regex ($DiskIdType) + { + 'Number|UniqueId' # for filters supported by the Get-Disk CmdLet + { + $diskIdParameter = @{ + $DiskIdType = $DiskId + } + + $disk = Get-Disk ` + @diskIdParameter ` + -ErrorAction SilentlyContinue + break + } + + default # for filters requiring Where-Object + { + $disk = Get-Disk -ErrorAction SilentlyContinue | + Where-Object -Property $DiskIdType -EQ $DiskId + } + } + + return $disk +} # end function Get-DiskByIdentifier + +<# + .SYNOPSIS + Tests if any of the access paths from a partition are assigned + to a local path. + + .PARAMETER AccessPath + Specifies the access paths that are assigned to the partition. +#> +function Test-AccessPathAssignedToLocal +{ + [CmdletBinding()] + [OutputType([System.Boolean])] + param + ( + [Parameter(Mandatory = $true)] + [System.String[]] + $AccessPath + ) + + $accessPathAssigned = $false + + foreach ($path in $AccessPath) + { + if ($path -match '[a-zA-Z]:\\') + { + $accessPathAssigned = $true + break + } + } + + return $accessPathAssigned +} # end function Test-AccessPathLocal + +Export-ModuleMember -Function @( + 'Restart-ServiceIfExists', + 'Assert-DriveLetterValid', + 'Assert-AccessPathValid', + 'Get-DiskByIdentifier', + 'Test-AccessPathAssignedToLocal' +) diff --git a/deployment/dsc/azshcihost/StorageDsc/5.0.1/Modules/StorageDsc.Common/en-US/StorageDsc.Common.strings.psd1 b/deployment/dsc/azshcihost/StorageDsc/5.0.1/Modules/StorageDsc.Common/en-US/StorageDsc.Common.strings.psd1 new file mode 100644 index 0000000..bb3dde1 --- /dev/null +++ b/deployment/dsc/azshcihost/StorageDsc/5.0.1/Modules/StorageDsc.Common/en-US/StorageDsc.Common.strings.psd1 @@ -0,0 +1,7 @@ +ConvertFrom-StringData @' + GetServiceInformation = Retrieving {0} service information. + RestartService = Restarting the {0} service. + UnknownService = Unable to find the desired service. + InvalidDriveLetterFormatError = Drive Letter format '{0}' is not valid. + InvalidAccessPathError = Access Path '{0}' is not found. +'@ diff --git a/deployment/dsc/azshcihost/StorageDsc/5.0.1/PSGetModuleInfo.xml b/deployment/dsc/azshcihost/StorageDsc/5.0.1/PSGetModuleInfo.xml new file mode 100644 index 0000000..fdb2ecb Binary files /dev/null and b/deployment/dsc/azshcihost/StorageDsc/5.0.1/PSGetModuleInfo.xml differ diff --git a/deployment/dsc/azshcihost/StorageDsc/5.0.1/StorageDsc.psd1 b/deployment/dsc/azshcihost/StorageDsc/5.0.1/StorageDsc.psd1 new file mode 100644 index 0000000..32ea4fb --- /dev/null +++ b/deployment/dsc/azshcihost/StorageDsc/5.0.1/StorageDsc.psd1 @@ -0,0 +1,95 @@ +@{ + # Version number of this module. + moduleVersion = '5.0.1' + + # ID used to uniquely identify this module + GUID = '00d73ca1-58b5-46b7-ac1a-5bfcf5814faf' + + # Author of this module + Author = 'DSC Community' + + # Company or vendor of this module + CompanyName = 'DSC Community' + + # Copyright statement for this module + Copyright = 'Copyright the DSC Community contributors. All rights reserved.' + + # Description of the functionality provided by this module + Description = 'DSC resources for managing storage on Windows Servers.' + + # Minimum version of the Windows PowerShell engine required by this module + PowerShellVersion = '4.0' + + # Minimum version of the common language runtime (CLR) required by this module + CLRVersion = '4.0' + + # Functions to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no functions to export. + FunctionsToExport = @() + + # Cmdlets to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no cmdlets to export. + CmdletsToExport = @() + + # Variables to export from this module + VariablesToExport = @() + + # Aliases to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no aliases to export. + AliasesToExport = @() + + # DSC resources to export from this module + DscResourcesToExport = @( + 'DiskAccessPath', + 'MountImage', + 'OpticalDiskDriveLetter', + 'WaitForDisk', + 'WaitForVolume', + 'Disk' + ) + + # Private data to pass to the module specified in RootModule/ModuleToProcess. This may also contain a PSData hashtable with additional module metadata used by PowerShell. + PrivateData = @{ + PSData = @{ + # Set to a prerelease string value if the release should be a prerelease. + Prerelease = '' + + # Tags applied to this module. These help with module discovery in online galleries. + Tags = @('DesiredStateConfiguration', 'DSC', 'DSCResource', 'Disk', 'Storage', 'Partition', 'Volume') + + # A URL to the license for this module. + LicenseUri = 'https://github.com/dsccommunity/StorageDsc/blob/master/LICENSE' + + # A URL to the main website for this project. + ProjectUri = 'https://github.com/dsccommunity/StorageDsc' + + # A URL to an icon representing this module. + IconUri = 'https://dsccommunity.org/images/DSC_Logo_300p.png' + + # ReleaseNotes of this module + ReleaseNotes = '## [5.0.1] - 2020-08-03 + +### Changed + +- Fixed build failures caused by changes in `ModuleBuilder` module v1.7.0 + by changing `CopyDirectories` to `CopyPaths` - Fixes [Issue #237](https://github.com/dsccommunity/StorageDsc/issues/237). +- Updated to use the common module _DscResource.Common_ - Fixes [Issue #234](https://github.com/dsccommunity/StorageDsc/issues/234). +- Pin `Pester` module to 4.10.1 because Pester 5.0 is missing code + coverage - Fixes [Issue #238](https://github.com/dsccommunity/StorageDsc/issues/238). +- OpticalDiskDriveLetter: + - Removed integration test that tests when a disk is not in the + system as it is not a useful test, does not work correctly + and is covered by unit tests - Fixes [Issue #240](https://github.com/dsccommunity/StorageDsc/issues/240). +- StorageDsc + - Automatically publish documentation to GitHub Wiki - Fixes [Issue #241](https://github.com/dsccommunity/StorageDsc/issues/241). + +### Fixed + +- Disk: + - Fix bug when multiple partitions with the same drive letter are + reported by the disk subsystem - Fixes [Issue #210](https://github.com/dsccommunity/StorageDsc/issues/210). + +' + } # End of PSData hashtable + } # End of PrivateData hashtable +} + + + diff --git a/deployment/dsc/azshcihost/StorageDsc/5.0.1/en-US/about_StorageDsc.help.txt b/deployment/dsc/azshcihost/StorageDsc/5.0.1/en-US/about_StorageDsc.help.txt new file mode 100644 index 0000000..db25595 --- /dev/null +++ b/deployment/dsc/azshcihost/StorageDsc/5.0.1/en-US/about_StorageDsc.help.txt @@ -0,0 +1,25 @@ +TOPIC + about_StorageDsc + +SHORT DESCRIPTION + DSC resources for managing storage on Windows Servers. + +LONG DESCRIPTION + This module contains DSC resources for managing storage on Windows Servers. + +EXAMPLES + PS C:\> Get-DscResource -Module StorageDsc + +NOTE: + Thank you to the DSC Community contributors who contributed to this module by + writing code, sharing opinions, and provided feedback. + +TROUBLESHOOTING NOTE: + Go to the Github repository for read about issues, submit a new issue, and read + about new releases. https://github.com/dsccommunity/StorageDsc + +SEE ALSO + - https://github.com/dsccommunity/StorageDsc + +KEYWORDS + DSC, DscResource, Storage, Disk, Partition, Volume diff --git a/deployment/dsc/azshcihost/WindowsDeploymentHelper/0.0.1/WindowsDeploymentHelper.psd1 b/deployment/dsc/azshcihost/WindowsDeploymentHelper/0.0.1/WindowsDeploymentHelper.psd1 new file mode 100644 index 0000000..7cb5d97 Binary files /dev/null and b/deployment/dsc/azshcihost/WindowsDeploymentHelper/0.0.1/WindowsDeploymentHelper.psd1 differ diff --git a/deployment/dsc/azshcihost/WindowsDeploymentHelper/0.0.1/WindowsDeploymentHelper.psm1 b/deployment/dsc/azshcihost/WindowsDeploymentHelper/0.0.1/WindowsDeploymentHelper.psm1 new file mode 100644 index 0000000..5b98126 --- /dev/null +++ b/deployment/dsc/azshcihost/WindowsDeploymentHelper/0.0.1/WindowsDeploymentHelper.psm1 @@ -0,0 +1,351 @@ +function New-BasicUnattendXML +{ + +[CmdletBinding(DefaultParameterSetName='basic')] + +Param ( + + [Parameter(Mandatory=$true, ParameterSetName='print')] + [Parameter(Mandatory=$true, ParameterSetName='basic')] + [Parameter(Mandatory=$true, ParameterSetName='Join Domain')] + [String]$ComputerName, + + [Parameter(Mandatory=$false, ParameterSetName='print')] + [Parameter(Mandatory=$true, ParameterSetName='Join Domain')] + [String]$Domain, + + [Parameter(Mandatory=$false, ParameterSetName='print')] + [Parameter(Mandatory=$true, ParameterSetName='Join Domain')] + [String]$Username, + + [Parameter(Mandatory=$false, ParameterSetName='print')] + [Parameter(Mandatory=$true, ParameterSetName='Join Domain')] + [SecureString]$Password, + + [Parameter(Mandatory=$false, ParameterSetName='print')] + [Parameter(Mandatory=$true, ParameterSetName='basic')] + [Parameter(Mandatory=$true, ParameterSetName='Join Domain')] + [SecureString]$LocalAdministratorPassword, + + [Parameter(Mandatory=$false, ParameterSetName='print')] + [Parameter(Mandatory=$true, ParameterSetName='Join Domain')] + [String]$JoinDomain, + + [Parameter(Mandatory=$false, ParameterSetName='print')] + [Parameter(Mandatory=$false, ParameterSetName='basic')] + [Parameter(Mandatory=$false, ParameterSetName='Join Domain')] + [validatescript({(([system.net.ipaddress]($_ -split '/' | Select-Object -First 1)).AddressFamily -match 'InterNetwork') -and (0..32 -contains ([int]($_ -split '/' | Select-Object -Last 1) )) })] + [String]$IpCidr, + + [Parameter(Mandatory=$false, ParameterSetName='print')] + [Parameter(Mandatory=$false, ParameterSetName='basic')] + [Parameter(Mandatory=$false, ParameterSetName='Join Domain')] + [String]$DefaultGateway, + + [Parameter(Mandatory=$false, ParameterSetName='print')] + [Parameter(Mandatory=$false, ParameterSetName='basic')] + [Parameter(Mandatory=$false, ParameterSetName='Join Domain')] + [String]$DnsServer, + + [Parameter(Mandatory=$false, ParameterSetName='print')] + [Parameter(Mandatory=$false, ParameterSetName='basic')] + [Parameter(Mandatory=$false, ParameterSetName='Join Domain')] + [String]$NicNameForIPandDNSAssignments, + + [Parameter(Mandatory=$false, ParameterSetName='print')] + [Parameter(Mandatory=$true, ParameterSetName='basic')] + [Parameter(Mandatory=$true, ParameterSetName='Join Domain')] + [String]$OutputPath, + + [Parameter(Mandatory=$false, ParameterSetName='basic')] + [Parameter(Mandatory=$false, ParameterSetName='Join Domain')] + [Switch]$Force, + + [Parameter(Mandatory=$false, ParameterSetName='print')] + [Parameter(Mandatory=$false, ParameterSetName='basic')] + [Parameter(Mandatory=$false, ParameterSetName='Join Domain')] + [System.Globalization.CultureInfo]$InputLocale = 'en-us', + + [Parameter(Mandatory=$false, ParameterSetName='print')] + [Parameter(Mandatory=$false, ParameterSetName='basic')] + [Parameter(Mandatory=$false, ParameterSetName='Join Domain')] + [System.Globalization.CultureInfo]$SystemLocale = 'en-us', + + [Parameter(Mandatory=$false, ParameterSetName='print')] + [Parameter(Mandatory=$false, ParameterSetName='basic')] + [Parameter(Mandatory=$false, ParameterSetName='Join Domain')] + [System.Globalization.CultureInfo]$UILanguage = 'en-us', + + [Parameter(Mandatory=$false, ParameterSetName='print')] + [Parameter(Mandatory=$false, ParameterSetName='basic')] + [Parameter(Mandatory=$false, ParameterSetName='Join Domain')] + [System.Globalization.CultureInfo]$UserLocale = 'en-us', + + [Parameter(Mandatory=$false, ParameterSetName='print')] + [Parameter(Mandatory=$false, ParameterSetName='basic')] + [Parameter(Mandatory=$false, ParameterSetName='Join Domain')] + [bool]$HideEULAPage = $true, + + [Parameter(Mandatory=$false, ParameterSetName='print')] + [Parameter(Mandatory=$false, ParameterSetName='basic')] + [Parameter(Mandatory=$false, ParameterSetName='Join Domain')] + [ValidateScript({$_ -in (Get-TimeZone -ListAvailable | Select-Object -ExpandProperty standardname)})] + [String]$TimeZone = 'GMT Standard Time', + + [Parameter(Mandatory=$false, ParameterSetName='print')] + [Parameter(Mandatory=$false, ParameterSetName='basic')] + [Parameter(Mandatory=$false, ParameterSetName='Join Domain')] + [ValidateRange(0,100)] + [int]$AutoLogonCount = 0, + + [Parameter(Mandatory=$false, ParameterSetName='print')] + [Parameter(Mandatory=$false, ParameterSetName='basic')] + [Parameter(Mandatory=$false, ParameterSetName='Join Domain')] + [String]$RegisteredOrganization = 'Azure Stack HCI on Azure VM', + + [Parameter(Mandatory=$false, ParameterSetName='print')] + [Parameter(Mandatory=$false, ParameterSetName='basic')] + [Parameter(Mandatory=$false, ParameterSetName='Join Domain')] + [String]$RegisteredOwner = 'Azure Stack HCI on Azure VM', + + [Parameter(Mandatory=$false, ParameterSetName='print')] + [Parameter(Mandatory=$false, ParameterSetName='basic')] + [Parameter(Mandatory=$false, ParameterSetName='Join Domain')] + [String]$PowerShellScriptFullPath, + + [Parameter(Mandatory=$false, ParameterSetName='print')] + [Parameter(Mandatory=$false, ParameterSetName='basic')] + [Parameter(Mandatory=$false, ParameterSetName='Join Domain')] + [Switch]$PrintScreenOnly +) + + $localAdministratorCreds = New-Object pscredential -ArgumentList Administrator, $LocalAdministratorPassword + $LocalAdministratorPasswordClearText = $localAdministratorCreds.GetNetworkCredential().password + $encodedAdministratorPassword = [System.Convert]::ToBase64String([System.Text.Encoding]::Unicode.GetBytes(('{0}AdministratorPassword' -f $LocalAdministratorPasswordClearText))) + + if ($JoinDomain) + { + $domainJoinerCreds = New-Object pscredential -ArgumentList Administrator, $LocalAdministratorPassword + $domainJoinerPasswordClearText = $domainJoinerCreds.GetNetworkCredential().password + $domainJoinXMLString = @" + + + + + $Domain + $domainJoinerPasswordClearText + $username + + $joinDomain + + +"@ + } + else + { + $domainJoinXMLString = $null + } + + $PowerShellStartupCmd = "%SystemRoot%\System32\WindowsPowerShell\v1.0\powershell.exe -NoProfile -ExecutionPolicy Bypass -File $PowerShellScriptFullPath" + + if ($AutoLogonCount -gt 0) + { + Write-Warning -Message '-AutoLogonCount places the Administrator password in plain txt' + $autoLogonXMLString = @" + + + + $LocalAdministratorPasswordClearText + + $AutoLogonCount + Administrator + true + +"@ + } + else + { + $autoLogonXMLString = $null + } + if ($IpCidr){ + $IPAddressXMLString = @" + + + + + $NicNameForIPandDNSAssignments + + $IpCidr + + + + 1 + 0.0.0.0/0 + 10 + $DefaultGateway + + + + false + + + + +"@ + } + else + { + $IPAddressXMLString = $null + } + if ($DnsServer) + { + $DnsAddressXMLString = @" + + + + + $NicNameForIPandDNSAssignments + + $DnsServer + + true + false + + + +"@ + } + else + { + $DnsAddressXMLString = $null + } + + if ($PowerShellScriptFullPath) + { + $logonScriptXMLString = @" + + + + PowerShell First logon script + 1 + $PowerShellStartupCmd + false + + +"@ + } + else + { + $logonScriptXMLString = $null + } + + $unattend = @" + + + + + + + + + $computerName + + + $computerName + $IPAddressXMLString$DnsAddressXMLString$domainJoinXMLString + + + + $InputLocale + $SystemLocale + $UILanguage + $UserLocale + + + $InputLocale + $SystemLocale + $UILanguage + $UserLocale + + + + $HideEULAPage + true + Work + 1 + true + true + + $TimeZone + + + $encodedAdministratorPassword + false</PlainText> + </AdministratorPassword> + </UserAccounts> + <RegisteredOrganization>$RegisteredOrganization</RegisteredOrganization> + <RegisteredOwner>$RegisteredOrganization</RegisteredOwner>$autoLogonXMLString$logonScriptXMLString + </component> + <component name="Microsoft-Windows-Shell-Setup" processorArchitecture="x86" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS" xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> + <OOBE> + <HideEULAPage>$HideEULAPage</HideEULAPage> + <HideWirelessSetupInOOBE>true</HideWirelessSetupInOOBE> + <NetworkLocation>Work</NetworkLocation> + <ProtectYourPC>1</ProtectYourPC> + <SkipUserOOBE>true</SkipUserOOBE> + <SkipMachineOOBE>true</SkipMachineOOBE> + </OOBE> + <TimeZone>$TimeZone</TimeZone> + <UserAccounts> + <AdministratorPassword> + <Value>$encodedAdministratorPassword</Value> + <PlainText>false</PlainText> + </AdministratorPassword> + </UserAccounts> + <RegisteredOrganization>$RegisteredOrganization</RegisteredOrganization> + <RegisteredOwner>$RegisteredOrganization</RegisteredOwner>$autoLogonXMLString$logonScriptXMLString + </component> + </settings> +</unattend> +"@ + + + try + { + $path = Resolve-Path -Path $OutputPath -ErrorAction Stop + $file = (Join-Path -Path $path -ChildPath 'Unattend.xml') + + $fileExist = Test-Path -Path $file + if ($fileExist -and $Force) + { + Write-Verbose -Message "Overwriting $file, Force switch was enabled." + $confirm = $false + $operation = 'Overridden' + } + elseif ($fileExist) + { + Write-Verbose -Message "$file deletion will be prompted." + $confirm = $true + $operation = 'Overridden' + } + else + { + Write-Verbose -Message "Creating Unattend.xml file in $OutputPath" + $confirm = $false + $operation = 'created' + } + if ($PrintScreenOnly) + { + return $unattend + } + Remove-Item -Path $file -Confirm:$confirm -ErrorAction SilentlyContinue + Set-Content -Path $file -Value $unattend + Write-Output "File $($operation): $file" + } + finally + { + Remove-Variable -Name unattend, LocalAdministratorPasswordClearText, encodedAdministratorPassword, domainJoinerPasswordClearText -ErrorAction SilentlyContinue + } + +} +Export-ModuleMember -Function New-BasicUnattendXML \ No newline at end of file diff --git a/deployment/dsc/azshcihost/WindowsImageTools/1.0.7/Functions/Add-UpdateImage.ps1 b/deployment/dsc/azshcihost/WindowsImageTools/1.0.7/Functions/Add-UpdateImage.ps1 new file mode 100644 index 0000000..ae2acc8 --- /dev/null +++ b/deployment/dsc/azshcihost/WindowsImageTools/1.0.7/Functions/Add-UpdateImage.ps1 @@ -0,0 +1,277 @@ +function Add-UpdateImage +{ + <# + .Synopsis + Add a Windows Image to a Windows Image Tools Update Directory + .DESCRIPTION + This command will convert a .ISO or .WIM into a VHD populated with an unattend.xml and first boot script + .EXAMPLE + Add-WitUpdateImage -Path c:\WitTools + .EXAMPLE + Another example of how to use this cmdlet + .INPUTS + System.IO.DirectoryInfo + .OUTPUTS + Custom object containing String -Path and String -Name + #> + [CmdletBinding(SupportsShouldProcess = $true)] + #[OutputType([String])] + Param + ( + # Path to the Windows Image Tools Update Folders (created via New-WindowsImageToolsExample) + [Parameter(Mandatory = $true, + ValueFromPipelineByPropertyName = $true)] + [ValidateNotNull()] + [ValidateNotNullOrEmpty()] + [ValidateScript({ + if (Test-Path $_) + { + $true + } + else + { + throw "Path $_ does not exist" + } + })] + [Alias('FullName')] + $Path, + + # Friendly name for for Base VHD used for filenames and targeting in Invoke-WindwosImageUpdate + [Parameter(Mandatory = $true)] + [ValidateNotNull()] + [ValidateNotNullOrEmpty()] + [string] + $FriendlyName, + + # Administrator Password for Base VHD + [Parameter(Mandatory = $true)] + [ValidateNotNull()] + [ValidateNotNullOrEmpty()] + [PSCredential] + $AdminCredential, + + # Product Key for sorce image (Not required for volume licence media) + [ValidateNotNullOrEmpty()] + [ValidateScript({ + if ($_ -imatch '^[a-z0-9]{5}-[a-z0-9]{5}-[a-z0-9]{5}-[a-z0-9]{5}-[a-z0-9]{5}$') + { + $true + } + else + { + throw "$_ not a valid key format" + } + })] + [String] + $ProductKey, + + # Size in Bytes (Default 40B) + [ValidateRange(25GB,64TB)] + [uint64]$Size = 40GB, + + # Create Dynamic disk + [switch]$Dynamic, + + # Specifies whether to build the image for BIOS (MBR), UEFI (GPT), or WindowsToGo (MBR). + # Generation 1 VMs require BIOS (MBR) images. Generation 2 VMs require UEFI (GPT) images. + # Windows To Go images will boot in UEFI or BIOS + [Parameter(Mandatory = $true)] + [Alias('Layout')] + [string] + [ValidateNotNullOrEmpty()] + [ValidateSet('BIOS', 'UEFI', 'WindowsToGo')] + $DiskLayout, + + # Path to WIM or ISO used to populate VHDX + [parameter(Position = 1,Mandatory = $true, + HelpMessage = 'Enter the path to the WIM/ISO file')] + [ValidateScript({ + Test-Path -Path (Get-FullFilePath -Path $_ ) + })] + [string]$SourcePath, + + # Index of image inside of WIM (Default 1) + [int]$Index = 1, + + # Add payload for all removed features + [switch]$AddPayloadForRemovedFeature, + + # Features to turn on (in DISM format) + [ValidateNotNullOrEmpty()] + [string[]]$Feature, + + # Feature to remove (in DISM format) + [ValidateNotNullOrEmpty()] + [string[]]$RemoveFeature, + + # Feature Source path. If not provided, all ISO and WIM images in $sourcePath searched + [ValidateNotNullOrEmpty()] + [ValidateScript({ + Test-Path -Path $(Resolve-Path $_) + })] + [string[]]$FeatureSource, + + # Feature Source index. If the source is a .wim provide an index Default =1 + [int]$FeatureSourceIndex, + + # Path to drivers to inject + [ValidateNotNullOrEmpty()] + [ValidateScript({ + Test-Path -Path $(Resolve-Path $_) + })] + [string[]]$Driver, + + # Path of packages to install via DSIM + [ValidateNotNullOrEmpty()] + [ValidateScript({ + Test-Path -Path $(Resolve-Path $_) + })] + [string[]]$Package, + + # Files/Folders to copy to root of Winodws Drive (to place files in directories mimic the direcotry structure off of C:\) + [ValidateNotNullOrEmpty()] + [ValidateScript({ + foreach ($Path in $_) + { + Test-Path -Path $(Resolve-Path $Path) + } + })] + [string[]]$filesToInject, + + # Force the overwrite of existing Image + [switch]$force + + ) + + $target = "$Path\BaseImage\$($FriendlyName)_base.vhdx" + + if ($pscmdlet.ShouldProcess("$target", 'Add Windows Image Tools Update Image')) + { + $ParametersToPass = @{} + foreach ($key in ('Whatif', 'Verbose', 'Debug')) + { + if ($PSBoundParameters.ContainsKey($key)) + { + $ParametersToPass[$key] = $PSBoundParameters[$key] + } + } + + #region Validate Input + try + { + $null = Test-Path -Path "$Path\BaseImage" -ErrorAction Stop + $null = Test-Path -Path "$Path\Resource" -ErrorAction Stop + } + catch + { + Throw "$Path missing required folder structure use New-WindowsImagetoolsExample to create example" + } + if ((Test-Path -Path "$Path\BaseImage\$($FriendlyName)_Base.vhdx") -and (-not ($force))) + { + Throw "BaseImage $Path\BaseImage\$($FriendlyName)_Base.vhdx allready exists. use -force to overwrite " + } + #endregion + + #region Unattend + $unattentParam = @{ + FirstBootScriptPath = 'c:\pstemp\FirstBoot.ps1' + AdminCredential = $AdminCredential + EnableAdministrator = $true + } + if ($ProductKey) + { + $unattentParam.add('ProductKey',$ProductKey) + } + + $UnattendPath = New-UnattendXml @unattentParam @ParametersToPass + #endregion + + + #region Create Base VHD + $convertParm = @{ + DiskLayout = $DiskLayout + SourcePath = $SourcePath + Index = $Index + Unattend = $UnattendPath + Path = $target + } + if ($Dynamic) + { + $convertParm.add('Dynamic',$Dynamic) + } + if ($AddPayloadForRemovedFeature) + { + $convertParm.add('AddPayloadForRemovedFeature', $AddPayloadForRemovedFeature) + } + if ($Feature) + { + $convertParm.add('Feature',$Feature) + } + if ($RemoveFeature) + { + $SetVHDPartitionParam.add('RemoveFeature', $RemoveFeature) + } + if ($FeatureSource) + { + $SetVHDPartitionParam.add('FeatureSource', $FeatureSource) + } + if ($FeatureSourceIndex) + { + $SetVHDPartitionParam.add('FeatureSourceIndex', $FeatureSourceIndex) + } + if ($Driver) + { + $convertParm.add('Driver',$Driver) + } + if ($Package) + { + $convertParm.add('Package',$Package) + } + if ($filesToInject) + { + $convertParm.add('filesToInject',$filesToInject) + } + if ($force) + { + $convertParm.add('force',$true) + } + + Write-Verbose -Message "[$($MyInvocation.MyCommand)] : $target : Creating " + Convert-Wim2VHD @convertParm @ParametersToPass + #endregion + + #region add firstboot script + $FirstBootContent = { + Start-Transcript -Path $PSScriptRoot\FirstBoot.log + + Get-Service Schedule | Start-Service + Start-Sleep -Seconds 20 + schtasks.exe /Create /TN 'AtStartup' /RU 'SYSTEM' /SC ONSTART /TR "'%SystemRoot%\System32\WindowsPowerShell\v1.0\powershell.exe' -NoProfile -ExecutionPolicy Bypass -File C:\PsTemp\AtStartup.ps1" + Start-Sleep -Seconds 20 + + # Restart-Computer does not have -force in 2008/win7 WMF2 + if ((Get-Command Restart-Computer -Syntax) -like '*[force]*') + { + Restart-Computer -Verbose -Force + } + else + { + shutdown.exe /r /t 0 /f + } + Stop-Transcript + } + + $AddScriptFilesBlock = { + if (-not (Test-Path "$($driveLetter):\PsTemp")) + { + $null = mkdir "$($driveLetter):\PsTemp" -ErrorAction SilentlyContinue + } + $null = New-Item -Path "$($driveLetter):\PsTemp" -Name FirstBoot.ps1 -ItemType 'file' -Value $FirstBootContent + } + + Write-Verbose -Message "[$($MyInvocation.MyCommand)] : $target : Adding First Boot Script " + MountVHDandRunBlock -vhd $target -block $AddScriptFilesBlock @ParametersToPass + Write-Verbose -Message "[$($MyInvocation.MyCommand)] : $target : Finished " + #endregion + } +} diff --git a/deployment/dsc/azshcihost/WindowsImageTools/1.0.7/Functions/Convert-Wim2VHD.ps1 b/deployment/dsc/azshcihost/WindowsImageTools/1.0.7/Functions/Convert-Wim2VHD.ps1 new file mode 100644 index 0000000..edda387 --- /dev/null +++ b/deployment/dsc/azshcihost/WindowsImageTools/1.0.7/Functions/Convert-Wim2VHD.ps1 @@ -0,0 +1,242 @@ +function Convert-Wim2VHD +{ + <# + .Synopsis + Create a VHDX and populate it from a WIM + .DESCRIPTION + This command will create a VHD or VHDX formated for UEFI (Gen 2/GPT) or BIOS (Gen 1/MBR) + You must supply the path to the VHD/VHDX file and a valid WIM/ISO. You should also + include the index number for the Windows Edition to install. + .EXAMPLE + Convert-WIM2VHDX -Path c:\windows8.vhdx -WimPath d:\Source\install.wim -Recovery -DiskLayout UEFI + .EXAMPLE + Convert-WIM2VHDX -Path c:\windowsServer.vhdx -WimPath d:\Source\install.wim -index 3 -Size 40GB -force -DiskLayout UEFI + #> + [CmdletBinding(SupportsShouldProcess = $true, + PositionalBinding = $false, + ConfirmImpact = 'Medium')] + Param + ( + # Path to the new VHDX file (Must end in .vhdx) + [Parameter(Position = 0,Mandatory = $true, + HelpMessage = 'Enter the path for the new VHDX file')] + [ValidateNotNullorEmpty()] + [ValidatePattern(".\.vhdx?$")] + [ValidateScript({ + if (Get-FullFilePath -Path $_ | + Split-Path | + Resolve-Path ) + { + $true + } + else + { + Throw "Parent folder for $_ does not exist." + } + })] + [string]$Path, + + # Size in Bytes (Default 40B) + [ValidateRange(25GB,64TB)] + [long]$Size = 40GB, + + # Create Dynamic disk + [switch]$Dynamic, + + # Specifies whether to build the image for BIOS (MBR), UEFI (GPT), or WindowsToGo (MBR). + # Generation 1 VMs require BIOS (MBR) images. Generation 2 VMs require UEFI (GPT) images. + # Windows To Go images will boot in UEFI or BIOS + [Parameter(Mandatory = $true)] + [Alias('Layout')] + [string] + [ValidateNotNullOrEmpty()] + [ValidateSet('BIOS', 'UEFI', 'WindowsToGo')] + $DiskLayout, + + # Create the Recovery Environment Tools Partition. Only valid on UEFI layout + [switch]$RecoveryTools, + + # Create the Recovery Environment Tools and Recovery Image Partitions. Only valid on UEFI layout + [switch]$RecoveryImage, + + # Force the overwrite of existing files + [switch]$force, + + # Path to WIM or ISO used to populate VHDX + [parameter(Position = 1,Mandatory = $true, + HelpMessage = 'Enter the path to the WIM/ISO file')] + [ValidateScript({ + Test-Path -Path (Get-FullFilePath -Path $_ ) + })] + [string]$SourcePath, + + # Index of image inside of WIM (Default 1) + [int]$Index = 1, + + # Path to file to copy inside of VHD(X) as C:\unattent.xml + [ValidateScript({ + if ($_) + { + Test-Path -Path $_ + } + else + { + $true + } + })] + [string]$Unattend, + + # Native Boot does not have the boot code inside the VHD(x) it must exist on the physical disk. + [switch]$NativeBoot, + + # Features to turn on (in DISM format) + [ValidateNotNullOrEmpty()] + [string[]]$Feature, + + # Feature to remove (in DISM format) + [ValidateNotNullOrEmpty()] + [string[]]$RemoveFeature, + + # Feature Source path. If not provided, all ISO and WIM images in $sourcePath searched + [ValidateNotNullOrEmpty()] + [ValidateScript({ + Test-Path -Path $(Resolve-Path $_) + })] + [string]$FeatureSource, + + # Feature Source index. If the source is a .wim provide an index Default =1 + [int]$FeatureSourceIndex = 1, + + # Path to drivers to inject + [ValidateNotNullOrEmpty()] + [ValidateScript({ + Test-Path -Path $(Resolve-Path $_) + })] + [string[]]$Driver, + + # Add payload for all removed features + [switch]$AddPayloadForRemovedFeature, + + # Path of packages to install via DSIM + [ValidateNotNullOrEmpty()] + [ValidateScript({ + Test-Path -Path $(Resolve-Path $_) + })] + [string[]]$Package, + # Files/Folders to copy to root of Winodws Drive (to place files in directories mimic the direcotry structure off of C:\) + [ValidateNotNullOrEmpty()] + [ValidateScript({ + foreach ($Path in $_) + { + Test-Path -Path $(Resolve-Path $Path) + } + })] + [string[]]$filesToInject + + ) + $Path = $Path | Get-FullFilePath + $SourcePath = $SourcePath | Get-FullFilePath + + $VhdxFileName = Split-Path -Leaf -Path $Path + + if ($pscmdlet.ShouldProcess("[$($MyInvocation.MyCommand)] : Overwrite partitions inside [$Path] with content of [$SourcePath]", + "Overwrite partitions inside [$Path] with contentce of [$SourcePath]? ", + 'Overwrite WARNING!')) + { + if((-not (Test-Path $Path)) -Or $force -Or $pscmdlet.ShouldContinue('Are you sure? Any existin data will be lost!', 'Warning')) + { + $ParametersToPass = @{} + foreach ($key in ('Whatif', 'Verbose', 'Debug')) + { + if ($PSBoundParameters.ContainsKey($key)) + { + $ParametersToPass[$key] = $PSBoundParameters[$key] + } + } + + $InitializeVHDPartitionParam = @{ + 'Size' = $Size + 'Path' = $Path + 'force' = $true + 'DiskLayout' = $DiskLayout + } + if ($RecoveryTools) + { + $InitializeVHDPartitionParam.add('RecoveryTools', $true) + } + if ($RecoveryImage) + { + $InitializeVHDPartitionParam.add('RecoveryImage', $true) + } + if ($Dynamic) + { + $InitializeVHDPartitionParam.add('Dynamic', $true) + } + $SetVHDPartitionParam = @{ + 'SourcePath' = $SourcePath + 'Path' = $Path + 'Index' = $Index + 'force' = $true + 'Confirm' = $false + } + if ($Unattend) + { + $SetVHDPartitionParam.add('Unattend', $Unattend) + } + if ($NativeBoot) + { + $SetVHDPartitionParam.add('NativeBoot', $NativeBoot) + } + if ($Feature) + { + $SetVHDPartitionParam.add('Feature', $Feature) + } + if ($RemoveFeature) + { + $SetVHDPartitionParam.add('RemoveFeature', $RemoveFeature) + } + if ($FeatureSource) + { + $SetVHDPartitionParam.add('FeatureSource', $FeatureSource) + } + if ($FeatureSourceIndex) + { + $SetVHDPartitionParam.add('FeatureSourceIndex', $FeatureSourceIndex) + } + if ($AddPayloadForRemovedFeature) + { + $SetVHDPartitionParam.add('AddPayloadForRemovedFeature', $AddPayloadForRemovedFeature) + } + if ($Driver) + { + $SetVHDPartitionParam.add('Driver', $Driver) + } + if ($Package) + { + $SetVHDPartitionParam.add('Package', $Package) + } + if ($filesToInject) + { + $SetVHDPartitionParam.add('filesToInject', $filesToInject) + } + Write-Verbose -Message "[$($MyInvocation.MyCommand)] : InitializeVHDPartitionParam" + Write-Verbose -Message ($InitializeVHDPartitionParam | Out-String) + Write-Verbose -Message "[$($MyInvocation.MyCommand)] : SetVHDPartitionParam" + Write-Verbose -Message ($SetVHDPartitionParam | Out-String) + Write-Verbose -Message "[$($MyInvocation.MyCommand)] : ParametersToPass" + Write-Verbose -Message ($ParametersToPass | Out-String) + + Try + { + Initialize-VHDPartition @InitializeVHDPartitionParam @ParametersToPass + Set-VHDPartition @SetVHDPartitionParam @ParametersToPass + } + Catch + { + throw "$($_.Exception.Message) at $($_.Exception.InvocationInfo.ScriptLineNumber)" + } + } + } +} + + diff --git a/deployment/dsc/azshcihost/WindowsImageTools/1.0.7/Functions/Get-VhdPartitionStyle.ps1 b/deployment/dsc/azshcihost/WindowsImageTools/1.0.7/Functions/Get-VhdPartitionStyle.ps1 new file mode 100644 index 0000000..d5487bb --- /dev/null +++ b/deployment/dsc/azshcihost/WindowsImageTools/1.0.7/Functions/Get-VhdPartitionStyle.ps1 @@ -0,0 +1,24 @@ +Function Get-VhdPartitionStyle +{ + <# + .Synopsis + Gets partition style of a VHD(x) + .DESCRIPTION + Returns the partition Style of the provided VHD(x) ei. GPT or MBR + .EXAMPLE + $partitionStyle = Get-VhdPartitionStyle -Vhd C:\win10.vhdx + #> + param + ( + # Path to VHD(x) file + [Parameter(Mandatory,HelpMessage = 'Path to VHD(x)')] + [ValidateNotNull()] + [ValidateNotNullOrEmpty()] + [string] + $vhd + ) + $PartitionStyle = (Mount-VHD -Path $vhd -ReadOnly -Passthru | Get-Disk).PartitionStyle + Dismount-VHD $vhd + Start-Sleep -Seconds 2 + return $PartitionStyle +} diff --git a/deployment/dsc/azshcihost/WindowsImageTools/1.0.7/Functions/HelperFunctions.ps1 b/deployment/dsc/azshcihost/WindowsImageTools/1.0.7/Functions/HelperFunctions.ps1 new file mode 100644 index 0000000..e4d3005 --- /dev/null +++ b/deployment/dsc/azshcihost/WindowsImageTools/1.0.7/Functions/HelperFunctions.ps1 @@ -0,0 +1,321 @@ +function Get-FullFilePath +{ + <# + .Synopsis + Get Absolute path from relative path + .DESCRIPTION + Takes a relative path like .\file.txt and returns the full path. + Parent folder must exist, but target file does not. + The target file does not have to exist, but the parent folder must exist + .EXAMPLE + $path = Get-AbsoluteFilePath -Path .\file.txt + #> + [CmdletBinding()] + [OutputType([string])] + Param + ( + # Path to file + [Parameter(Mandatory,HelpMessage = 'Path to file', + ValueFromPipeline, + Position = 0)] + [String]$Path + ) + + if (-not (Test-Path -Path $Path)) + { + if (Test-Path -Path (Split-Path -Path $Path -Parent )) + { + $Parent = Resolve-Path -Path (Split-Path -Path $Path -Parent ) + $Leaf = Split-Path -Path $Path -Leaf + + if ($Parent.path[-1] -eq '\') + { + $Path = "$Parent" + "$Leaf" + } + else + { + $Path = "$Parent" + "\$Leaf" + } + } + else + { + throw "Parent [$(Split-Path -Path $Path -Parent)] does not exist" + } + } + else + { + $Path = Resolve-Path -Path $Path + } + + return $Path +} + +function +Test-Admin +{ + <# + .SYNOPSIS + Short function to determine whether the logged-on user is an administrator. + + .EXAMPLE + Do you honestly need one? There are no parameters! + + .OUTPUTS + $true if user is admin. + $false if user is not an admin. + #> + [CmdletBinding()] + param() + + $currentUser = New-Object -TypeName Security.Principal.WindowsPrincipal -ArgumentList $([Security.Principal.WindowsIdentity]::GetCurrent()) + $isAdmin = $currentUser.IsInRole([Security.Principal.WindowsBuiltinRole]::Administrator) + Write-Verbose -Message "[$($MyInvocation.MyCommand)] : is User Admin? [$isAdmin]" + + return $isAdmin +} + + +function +Run-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,HelpMessage = 'Path to Executable')] + [string] + [ValidateNotNullOrEmpty()] + $Executable, + + [Parameter(Mandatory,HelpMessage = 'aray of arguments to pass to executable')] + [string[]] + [ValidateNotNullOrEmpty()] + $Arguments, + + [Parameter()] + [int] + $SuccessfulErrorCode = 0 + + ) + + $exeName = Split-Path -Path $Executable -Leaf + Write-Verbose -Message "[$($MyInvocation.MyCommand)] : Running [$Executable] [$Arguments]" + $Params = @{ + 'FilePath' = $Executable + 'ArgumentList' = $Arguments + 'NoNewWindow' = $true + 'Wait' = $true + 'RedirectStandardOutput' = "$($env:temp)\$($exeName)-StandardOutput.txt" + 'RedirectStandardError' = "$($env:temp)\$($exeName)-StandardError.txt" + 'PassThru' = $true + } + + Write-Verbose -Message ($Params | Out-String) + $ret = Start-Process @Params -ErrorAction SilentlyContinue + + Write-Verbose -Message "[$($MyInvocation.MyCommand)] : Return code was [$($ret.ExitCode)]" + + if ($ret.ExitCode -ne $SuccessfulErrorCode) + { + throw "$Executable failed with code $($ret.ExitCode)!" + } +} + +Function Test-IsNetworkLocation +{ + <# + .SYNOPSIS + Determines whether or not a given path is a network location or a local drive. + + .DESCRIPTION + Function to determine whether or not a specified path is a local path, a UNC path, + or a mapped network drive. + + .PARAMETER Path + The path that we need to figure stuff out about, + #> + + [CmdletBinding()] + param( + [Parameter(ValueFromPipeLine)] + [string] + [ValidateNotNullOrEmpty()] + $Path + ) + + $result = $false + + if ([bool]([URI]$Path).IsUNC) + { + $result = $true + } + else + { + $driveInfo = [IO.DriveInfo]((Resolve-Path -Path $Path).Path) + + if ($driveInfo.DriveType -eq 'Network') + { + $result = $true + } + } + + return $result +} + +function New-TemporaryDirectory +{ + <# + .Synopsis + Create a new Temporary Directory + .DESCRIPTION + Creates a new Directory in the $env:temp and returns the System.IO.DirectoryInfo (dir) + .EXAMPLE + $TempDirPath = NewTemporaryDirectory + #> + [CmdletBinding(SupportsShouldProcess)] + [OutputType([System.IO.DirectoryInfo])] + Param + ( + ) + + #return [System.IO.Directory]::CreateDirectory((Join-Path $env:Temp -Ch ([System.IO.Path]::GetRandomFileName().split('.')[0]))) + + Begin + { + try + { + if($PSCmdlet.ShouldProcess($env:temp)) + { + $tempDirPath = [System.IO.Directory]::CreateDirectory((Join-Path -Path $env:temp -ChildPath ([System.IO.Path]::GetRandomFileName().split('.')[0]))) + } + } + catch + { + $errorRecord = [System.Management.Automation.ErrorRecord]::new($_.Exception,'NewTemporaryDirectoryWriteError', 'WriteError', $env:temp) + Write-Error -ErrorRecord $errorRecord + return + } + + if($tempDirPath) + { + Get-Item -Path $env:temp\$tempDirPath + } + } +} + +function MountVHDandRunBlock +{ + param + ( + [string]$vhd, + [scriptblock]$block, + [switch]$ReadOnly + ) + + # This function mounts a VHD, runs a script block and unmounts the VHD. + # Drive letter of the mounted VHD is stored in $driveLetter - can be used by script blocks + if($ReadOnly) + { + $virtualDisk = Mount-VHD -Path $vhd -ReadOnly -Passthru + } + else + { + $virtualDisk = Mount-VHD -Path $vhd -Passthru + } + # Workarround for new drive letters in script modules + $null = Get-PSDrive + $driveLetter = ($virtualDisk | + Get-Disk | + Get-Partition | + Get-Volume).DriveLetter + & $block + + Dismount-VHD -Path $vhd + + # Wait 2 seconds for activity to clean up + Start-Sleep -Seconds 2 +} + +Function GetVHDPartitionStyle +{ + param + ( + [string]$vhd + ) + $PartitionStyle = (Mount-VHD -Path $vhd -ReadOnly -Passthru | Get-Disk).PartitionStyle + Dismount-VHD -Path $vhd + Start-Sleep -Seconds 2 + return $PartitionStyle +} + +function createRunAndWaitVM +{ + [CmdletBinding()] + param + ( + [string] $vhdPath, + [string] $vmGeneration, + [Hashtable] $configData + ) + + $vmName = [System.IO.Path]::GetRandomFileName().split('.')[0] + + Write-Verbose -Message "[$($MyInvocation.MyCommand)] : Creating VM $vmName at $(Get-Date)" + $null = New-VM -Name $vmName -MemoryStartupBytes 2048mb -VHDPath $vhdPath -Generation $vmGeneration -SwitchName $configData.vmSwitch -ErrorAction Stop + + If($configData.vLan -ne 0) + { + Get-VMNetworkAdapter -VMName $vmName | Set-VMNetworkAdapterVlan -Access -VlanId $configData.vLan + } + + Set-VM -Name $vmName -ProcessorCount 2 + Start-VM -Name $vmName + + # Give the VM a moment to start before we start checking for it to stop + Start-Sleep -Seconds 10 + + # Wait for the VM to be stopped for a good solid 5 seconds + do + { + $state1 = (Get-VM | Where-Object name -EQ -Value $vmName).State + Start-Sleep -Seconds 5 + + $state2 = (Get-VM | Where-Object name -EQ -Value $vmName).State + Start-Sleep -Seconds 5 + } + until (($state1 -eq 'Off') -and ($state2 -eq 'Off')) + + # Clean up the VM + Write-Verbose -Message "[$($MyInvocation.MyCommand)] : VM $vmName Stoped" + Remove-VM -Name $vmName -Force + Write-Verbose -Message "[$($MyInvocation.MyCommand)] : VM $vmName Deleted at $(Get-Date)" +} + +function cleanupFile +{ + param + ( + [string[]] $file + ) + + foreach ($target in $file) + { + if (Test-Path -Path $target) + { + Remove-Item -Path $target -Recurse -Force + } + } +} diff --git a/deployment/dsc/azshcihost/WindowsImageTools/1.0.7/Functions/Initialize-VHDPartition.ps1 b/deployment/dsc/azshcihost/WindowsImageTools/1.0.7/Functions/Initialize-VHDPartition.ps1 new file mode 100644 index 0000000..ed02a26 --- /dev/null +++ b/deployment/dsc/azshcihost/WindowsImageTools/1.0.7/Functions/Initialize-VHDPartition.ps1 @@ -0,0 +1,346 @@ +function Initialize-VHDPartition +{ + <# + .Synopsis + Create VHD(X) with partitions needed to be bootable + .DESCRIPTION + This command will create a VHD or VHDX file. Supported layours are: BIOS, UEFO or WindowsToGo. + + To create a recovery partitions use -RecoveryTools and -RecoveryImage + + .EXAMPLE + Initialize-VHDPartition d:\disks\disk001.vhdx -dynamic -size 30GB -DiskLayout BIOS + .EXAMPLE + Initialize-VHDPartition d:\disks\disk001.vhdx -dynamic -size 40GB -DiskLayout UEFI -RecoveryTools + .NOTES + General notes + #> + [CmdletBinding(SupportsShouldProcess, + PositionalBinding = $false, + ConfirmImpact = 'Medium')] + Param + ( + # Path to the new VHDX file (Must end in .vhdx) + [Parameter(Position = 0,Mandatory, + HelpMessage = 'Enter the path for the new VHD/VHDX file')] + [ValidateNotNullorEmpty()] + [ValidatePattern(".\.vhdx?$")] + [ValidateScript({ + if (Get-FullFilePath -Path $_ | + Split-Path | + Resolve-Path ) + { + $true + } + else + { + Throw "Parent folder for $_ does not exist." + } + })] + [string]$Path, + + # Size in Bytes (Default 40B) + [ValidateRange(25GB,64TB)] + [uint64]$Size = 40GB, + + # Create Dynamic disk + [switch]$Dynamic, + + # Specifies whether to build the image for BIOS (MBR), UEFI (GPT), or WindowsToGo (MBR). + # Generation 1 VMs require BIOS (MBR) images. Generation 2 VMs require UEFI (GPT) images. + # Windows To Go images will boot in UEFI or BIOS + [Parameter(Mandatory)] + [Alias('Layout')] + [string] + [ValidateNotNullOrEmpty()] + [ValidateSet('BIOS', 'UEFI', 'WindowsToGo')] + $DiskLayout, + + # Output the disk image object + [switch]$Passthru, + + # Create the Recovery Environment Tools Partition. Only valid on UEFI layout + [switch]$RecoveryTools, + + # Create the Recovery Environment Tools and Recovery Image Partitions. Only valid on UEFI layout + [switch]$RecoveryImage, + + # Force the overwrite of existing files + [switch]$force + ) + Begin { + + + if ($pscmdlet.ShouldProcess("[$($MyInvocation.MyCommand)] Create partition structure for Bootable vhd(x) on [$Path]", + "Replace existing file [$Path] ? ", + 'Overwrite WARNING!')) + { + if((-not (Test-Path $Path)) -Or + $force -Or + ((Test-Path $Path) -and $pscmdlet.ShouldContinue("TargetFile [$Path] exists! Any existin data will be lost!", 'Warning'))) + { + #region Validate input + + # Recovery Image requires the Recovery Tools + if ($RecoveryImage) + { + $RecoveryTools = $true + } + + $VHDFormat = ([IO.FileInfo]$Path).Extension.split('.')[-1] + + if (($DiskLayout -eq 'UEFI')-and ($VHDFormat -eq 'VHD')) + { + throw 'UEFI disks must be in VHDX format. Please change the path to end in VHDX' + } + + # Choose smallest supported block size for dynamic VHD(X) + $BlockSize = 1MB + + # Enforce max VHD size. + if ('VHD' -ilike $VHDFormat) + { + if ($Size -gt 2040GB) + { + Write-Warning -Message 'For the VHD file format, the maximum file size is ~2040GB. Reseting size to 2040GB.' + $Size = 2040GB + } + + $BlockSize = 512KB + } + + $SysSize = 200MB + $MSRSize = 128MB + $RESize = 0 + $RecoverySize = 0 + if ($RecoveryTools) + { + $RESize = 350MB + } + if ($RecoveryImage) + { + $RecoverySize = 15GB + } + $fileName = Split-Path -Leaf -Path $Path + + # make paths absolute + $Path = $Path | Get-FullFilePath + #endregion + + # if we get this far it's ok to delete existing files + if (Test-Path -Path $Path) + { + Remove-Item -Path $Path + } + Write-Verbose -Message "[$($MyInvocation.MyCommand)] [$fileName] : Creating" + + #region Create VHD + Try + { + if ($VHDCmdlets) + { + $vhdParams = @{ + ErrorAction = 'Stop' + Path = $Path + SizeBytes = $Size + Dynamic = $Dynamic + BlockSizeBytes = $BlockSize + } + Write-Verbose -Message "[$($MyInvocation.MyCommand)] [$fileName] : @vhdParms" + Write-Verbose -Message ($vhdParams | Out-String) + $null = New-VHD @vhdParams + } + else + { + $vhdParams = @{ + VHDFormat = $VHDFormat + Path = $Path + SizeBytes = $Size + } + + Write-Verbose -Message "[$($MyInvocation.MyCommand)] [$fileName] : Params for [WIM2VHD.VirtualHardDisk]::CreateSparseDisk()" + Write-Verbose -Message ($vhdParams | Out-String) + + [WIM2VHD.VirtualHardDisk]::CreateSparseDisk( + $VHDFormat, + $Path, + $Size, + $true + ) + } + } + catch + { + Throw "Failed to create $Path. $($_.Exception.Message)" + } + + #endregion + + if (Test-Path -Path $Path) + { + #region Mount Image + try + { + Write-Verbose -Message "[$($MyInvocation.MyCommand)] [$fileName] : Mounting disk image" + $disk = Mount-DiskImage -ImagePath $Path -PassThru | + Get-DiskImage | + Get-Disk + } + catch + { + throw $_.Exception.Message + } + #endregion + + #region create partitions + try + { + $disknumber = $disk.Number + + switch ($DiskLayout) + { + 'BIOS' + { + Write-Verbose -Message "[$($MyInvocation.MyCommand)] [$fileName] : Initializing disk [$disknumber] as MBR" + Initialize-Disk -Number $disknumber -PartitionStyle MBR + + Write-Verbose -Message "[$($MyInvocation.MyCommand)] [$fileName] : Clearing disk partitions to start all over" + Get-Disk -Number $disknumber -ErrorAction Stop | + Get-Partition -ErrorAction Stop | + Remove-Partition -Confirm:$false -ErrorAction Stop + + # Create the Windows/system partition + # Refresh $disk to update free space + $disk = Get-DiskImage -ImagePath $Path | Get-Disk + Write-Verbose -Message "[$($MyInvocation.MyCommand)] [$fileName] : Creating single partition of [$($disk.LargestFreeExtent)] bytes" + $windowsPartition = New-Partition -DiskNumber $disknumber -UseMaximumSize -MbrType IFS -IsActive #-Size $disk.LargestFreeExtent + $systemPartition = $windowsPartition + + Write-Verbose -Message "[$($MyInvocation.MyCommand)] [$fileName] : Formatting windows volume" + $null = Format-Volume -Partition $windowsPartition -FileSystem NTFS -Force -Confirm:$false + } + + 'UEFI' + { + Write-Verbose -Message "[$($MyInvocation.MyCommand)] [$fileName] : Initializing disk [$disknumber] as GPT" + Initialize-Disk -Number $disk.Number -PartitionStyle GPT + + Write-Verbose -Message "[$($MyInvocation.MyCommand)] [$fileName] : Clearing disk partitions to start all over" + Get-Disk -Number $disknumber -ErrorAction Stop | + Get-Partition -ErrorAction Stop | + Remove-Partition -Confirm:$false -ErrorAction Stop + + if ($RecoveryTools) + { + Write-Verbose "[$($MyInvocation.MyCommand)] [$fileName] : Recovery tools : Creating partition of [$RESize] bytes" + $recoveryToolsPartition = New-Partition -DiskNumber $disk.Number -Size $RESize -GptType '{de94bba4-06d1-4d40-a16a-bfd50179d6ac}' + Write-Verbose "[$($MyInvocation.MyCommand)] [$fileName] : Recovery tools : Formatting NTFS" + $null = Format-Volume -Partition $recoveryToolsPartition -FileSystem NTFS -NewFileSystemLabel 'Windows RE Tools' -Force -Confirm:$false + #run diskpart to set GPT attribute to prevent partition removal + #the here string must be left justified + $null = @" +select disk $($disk.Number) +select partition $($recoveryToolsPartition.partitionNumber) +gpt attributes=0x8000000000000001 +exit +"@ | + diskpart.exe + } + + + # Create the system partition. Create a data partition so we can format it, then change to ESP + Write-Verbose "[$($MyInvocation.MyCommand)] [$fileName] : EFI system : Creating partition of [$SysSize] bytes" + $systemPartition = New-Partition -DiskNumber $disk.Number -Size $SysSize -GptType '{ebd0a0a2-b9e5-4433-87c0-68b6b72699c7}' + + Write-Verbose "[$($MyInvocation.MyCommand)] [$fileName] : EFI system : Formatting FAT32" + $null = Format-Volume -Partition $systemPartition -FileSystem FAT32 -Force -Confirm:$false + + Write-Verbose "[$($MyInvocation.MyCommand)] [$fileName] : EFI system : Setting system partition as ESP" + $systemPartition | Set-Partition -GptType '{c12a7328-f81f-11d2-ba4b-00a0c93ec93b}' + + # Create the reserved partition + Write-Verbose "[$($MyInvocation.MyCommand)] [$fileName] : MSR : Creating partition of [$MSRSize] bytes" + $null = New-Partition -DiskNumber $disk.Number -Size $MSRSize -GptType '{e3c9e316-0b5c-4db8-817d-f92df00215ae}' + + + # Create the Windows partition + # Refresh $disk to update free space + $disk = Get-DiskImage -ImagePath $Path | Get-Disk + Write-Verbose "[$($MyInvocation.MyCommand)] [$fileName] : Windows : Creating partition of [$($disk.LargestFreeExtent - $RecoverySize)] bytes" + $windowsPartition = New-Partition -DiskNumber $disk.Number -Size ($disk.LargestFreeExtent - $RecoverySize) -GptType '{ebd0a0a2-b9e5-4433-87c0-68b6b72699c7}' + Write-Verbose "[$($MyInvocation.MyCommand)] [$fileName] : Windows : Formatting volume NTFS" + $null = Format-Volume -Partition $windowsPartition -NewFileSystemLabel 'OS' -FileSystem NTFS -Force -Confirm:$false + + if ($RecoveryImage) + { + Write-Verbose "[$($MyInvocation.MyCommand)] [$fileName] : Recovery Image : Creating partition using remaing free space" + $recoveryImagePartition = New-Partition -DiskNumber $disk.Number -UseMaximumSize -GptType '{de94bba4-06d1-4d40-a16a-bfd50179d6ac}' + Write-Verbose "[$($MyInvocation.MyCommand)] [$fileName] : Recovery Image : Formatting volume NTFS" + $null = Format-Volume -Partition $recoveryImagePartition -NewFileSystemLabel 'Windows Recovery' -FileSystem NTFS -Force -Confirm:$false + #run diskpart to set GPT attribute to prevent partition removal + #the here string must be left justified + $null = @" +select disk $($disk.Number) +select partition $($recoveryImagePartition.partitionNumber) +gpt attributes=0x8000000000000001 +exit +"@ | + diskpart.exe + } + } + + 'WindowsToGo' + { + Write-Verbose -Message "[$($MyInvocation.MyCommand)] [$fileName] : Initializing disk [$disknumber] as MBR" + Initialize-Disk -Number $disk.Number -PartitionStyle MBR + + Write-Verbose -Message "[$($MyInvocation.MyCommand)] [$fileName] : Clearing disk partitions to start all over" + Get-Disk -Number $disknumber -ErrorAction Stop | + Get-Partition -ErrorAction Stop | + Remove-Partition -Confirm:$false -ErrorAction Stop + + # Create the system partition + Write-Verbose "[$($MyInvocation.MyCommand)] [$fileName] : System : Creating partition of [$SysSize] bytes" + $systemPartition = New-Partition -DiskNumber $disk.Number -Size $SysSize -MbrType FAT32 -IsActive + + Write-Verbose "[$($MyInvocation.MyCommand)] [$fileName] : EFI system : Formatting FAT32" + $null = Format-Volume -Partition $systemPartition -FileSystem FAT32 -Force -Confirm:$false + + # Create the Windows partition + Write-Verbose "[$($MyInvocation.MyCommand)] [$fileName] : Windows : Creating partition useing remaning space" + $windowsPartition = New-Partition -DiskNumber $disk.Number -Size $disk.LargestFreeExtent -MbrType IFS + + Write-Verbose "[$($MyInvocation.MyCommand)] [$fileName] : Windows : Formatting volume NTFS" + $null = Format-Volume -Partition $windowsPartition -FileSystem NTFS -Force -Confirm:$false + } + } + } + catch + { + Write-Error -Message "[$($MyInvocation.MyCommand)] [$fileName] : Creating Partitions" + throw $_.Exception.Message + } + #endregion create partitions + + #region Dismount + finally + { + Write-Verbose -Message "[$($MyInvocation.MyCommand)] [$fileName] : Dismounting disk image" + Dismount-DiskImage -ImagePath $Path + } + #endregion + + if ($Passthru) + { + #write the new disk object to the pipeline + Get-DiskImage -ImagePath $Path + } + }#end if disk + else + { + throw "Unable to create or mount $Path" + } + } + } + } +} diff --git a/deployment/dsc/azshcihost/WindowsImageTools/1.0.7/Functions/Invoke-CreateVmRunAndWait.ps1 b/deployment/dsc/azshcihost/WindowsImageTools/1.0.7/Functions/Invoke-CreateVmRunAndWait.ps1 new file mode 100644 index 0000000..db7f820 --- /dev/null +++ b/deployment/dsc/azshcihost/WindowsImageTools/1.0.7/Functions/Invoke-CreateVmRunAndWait.ps1 @@ -0,0 +1,80 @@ +function Invoke-CreateVmRunAndWait +{ + <# + .Synopsis + Create a temp vm with a random name and wait for it to stop + .DESCRIPTION + This Command quickly test changes to a VHD by creating a temporary VM and ataching it to the network. VM is deleted when it enters a stoped state. + .EXAMPLE + Invoke-CreateVMRunAndWait -VhdPath c:\test.vhdx -VmGeneration 2 -VmSwitch 'testlab' + .EXAMPLE + Invoke-CreateVMRunAndWait -VhdPath c:\test.vhdx -VmGeneration 2 -VmSwitch 'testlab' -vLan 16023 -ProcessorCount 1 -MemorySTartupBytes 512mb + #> + [CmdletBinding()] + param + ( + # Path to VHD(x) + [Parameter(Mandatory = $true)] + [ValidateNotNull()] + [ValidateNotNullOrEmpty()] + [string] + $VhdPath, + + # VM Generation (1 = BIOS/MBR, 2 = uEFI/GPT) + [Parameter(Mandatory = $true)] + [ValidateSet(1, 2)] + [int] + $VmGeneration, + + # name of VM switch to attach to + [Parameter(Mandatory = $true)] + [ValidateNotNull()] + [ValidateNotNullOrEmpty()] + [string] + $VmSwitch, + + # vLAN to use default = 0 (dont use vLAN) + [int] + $vLan = 0, + + # ProcessorCount default = 2 + [int] + $ProcessorCount = 2, + + # MemoryStartupBytes default = 2Gig + [long] + $MemoryStartupBytess = 2GB + ) + + $vmName = [System.IO.Path]::GetRandomFileName().split('.')[0] + + Write-Verbose -Message "[$($MyInvocation.MyCommand)] : Creating VM $vmName at $(Get-Date)" + $null = New-VM -Name $vmName -MemoryStartupBytes $MemoryStartupBytess -VHDPath $VhdPath -Generation $VmGeneration -SwitchName $VmSwitch -ErrorAction Stop + + If($vLan -ne 0) + { + Get-VMNetworkAdapter -VMName $vmName | Set-VMNetworkAdapterVlan -Access -VlanId $vLan + } + + Set-VM -Name $vmName -ProcessorCount $ProcessorCount + Start-VM $vmName + + # Give the VM a moment to start before we start checking for it to stop + Start-Sleep -Seconds 10 + + # Wait for the VM to be stopped for a good solid 5 seconds + do + { + $state1 = (Get-VM | Where-Object -Property name -EQ -Value $vmName).State + Start-Sleep -Seconds 5 + + $state2 = (Get-VM | Where-Object -Property name -EQ -Value $vmName).State + Start-Sleep -Seconds 5 + } + until (($state1 -eq 'Off') -and ($state2 -eq 'Off')) + + # Clean up the VM + Write-Verbose -Message "[$($MyInvocation.MyCommand)] : VM $vmName Stoped" + Remove-VM $vmName -Force + Write-Verbose -Message "[$($MyInvocation.MyCommand)] : VM $vmName Deleted at $(Get-Date)" +} diff --git a/deployment/dsc/azshcihost/WindowsImageTools/1.0.7/Functions/Invoke-WindowsImageUpdate.ps1 b/deployment/dsc/azshcihost/WindowsImageTools/1.0.7/Functions/Invoke-WindowsImageUpdate.ps1 new file mode 100644 index 0000000..1e2e731 --- /dev/null +++ b/deployment/dsc/azshcihost/WindowsImageTools/1.0.7/Functions/Invoke-WindowsImageUpdate.ps1 @@ -0,0 +1,489 @@ +function Invoke-WindowsImageUpdate +{ + <# + .Synopsis + Starts the process of applying updates to all (or selected) images in a Windows Image Tools BaseImages Folder + .DESCRIPTION + This Command updates all (or selected) the images created via Add-UpdateImage in a Windows Image Tools BaseImages folder + New-WindowsImageToolsExample can be use to create the structrure + .EXAMPLE + Invoke-WindowsImageUpdate -Path C:\WITExample + Update all the Images created with Add-UpdateImage located in C:\WITExample\BaseImages and place the resulting VHD and WIM in c:\WITExample\UpdatedImageShare + .EXAMPLE + Invoke-WindowsImageUpdate -Path C:\WITExample -Name 2012r2Wmf5 + Update Image named 2012r2Wmf5_Base.vhdx in C:\WITExample\BaseImages and place the resulting VHD and WIM in c:\WITExample\UpdatedImageShare + #> + [CmdletBinding(SupportsShouldProcess = $true)] + [OutputType([bool])] + Param + ( + # Path to the Windows Image Tools Update Folders (created via New-WindowsImageToolsExample) + [Parameter(Mandatory = $true, + ValueFromPipelineByPropertyName = $true)] + [ValidateNotNull()] + [ValidateNotNullOrEmpty()] + [ValidateScript({ + if (Test-Path $_) + { + $true + } + else + { + throw "Path $_ does not exist" + } + })] + [Alias('FullName')] + $Path, + # Name of the Image to update + [ValidateNotNull()] + [ValidateNotNullOrEmpty()] + [Alias('FriendlyName')] + [string[]] + $ImageName, + + # Reduce output file by removing feature sources + [switch] + $ReduceImageSize, + + # what files to export if upates are added : NONE, WIM, Both (wim and vhdx) default = both + [ValidateSet('NONE', 'WIM', 'Both')] + [string] + $output = 'Both' + + ) + + $ParametersToPass = @{} + foreach ($key in ('Whatif', 'Verbose', 'Debug')) + { + if ($PSBoundParameters.ContainsKey($key)) + { + $ParametersToPass[$key] = $PSBoundParameters[$key] + } + } + + #region validate input + try + { + $null = Test-Path -Path "$Path\BaseImage" -ErrorAction Stop + $null = Test-Path -Path "$Path\Resource" -ErrorAction Stop + $null = Test-Path -Path "$Path\UpdatedImageShare" -ErrorAction Stop + $null = Test-Path -Path "$Path\config.xml" -ErrorAction Stop + } + catch + { + throw "$Path folder structure incorrect, see New-WindowsImageToolsExample for an example" + } + + if ($ImageName) + { + foreach ($testpath in $ImageName) + { + Write-Verbose -Message "[$($MyInvocation.MyCommand)] : Validateing [$testpath]" + if (-not (Test-Path -Path "$Path\BaseImage\$($testpath)_base.vhdx" )) + + { + throw "$Path\BaseImage\$($testpath)_base.vhdx" + } + } + $ImageList = $ImageName + } + else + { + Write-Verbose -Message "[$($MyInvocation.MyCommand)] : Colecting List of Images" + $ImageList = (Get-ChildItem -Path $Path\BaseImage\*_Base.vhdx).Name -replace '_Base.vhdx', '' + } + + $configData = Import-Clixml -Path "$Path\config.xml" + + try + { + Write-Verbose -Message "[$($MyInvocation.MyCommand)] : Validateing VM switch config" + $null = Get-VMSwitch -Name $configData.VmSwitch -ErrorAction Stop + } + catch + { + throw "VM Switch Configuration in $Path incorrect Set-UpdateConfig" + } + + #endregion + + #region update resorces folder + if ($pscmdlet.ShouldProcess('PowerShell Gallery', 'Download required Modules')) + { + if (-not (Test-Path -Path $Path\Resource\Modules)) + { + $null = mkdir -Path $Path\Resource\Modules + } + if (-not (Get-Command Save-Module)) + { + Write-Warning -Message 'PowerShellGet missing. you will need to download required modules from PowerShell Gallery manualy' + Write-Warning -Message 'Required Modules : PSWindowsUpdate' + } + else + { + Write-Verbose -Message "[$($MyInvocation.MyCommand)] : Geting latest PSWindowsUpdate" + try + { + # if nuget needs updating this prompts + ### To-Do find a way to silenty update nuget ### + $null = Save-Module -Name PSWindowsUpdate -Path $Path\Resource\Modules -Force -ErrorAction Stop @ParametersToPass + } + catch + { + if (Test-Path -Path $Path\Resource\Modules\PSWindowsUpdate) + { + Write-Warning -Message "[$($MyInvocation.MyCommand)] : PSwindowsUpdate present, but unable to download latest" + } + else + { + throw "unable to download PSWindowsUpdate from PowerShellGalary.com, download manualy and place in $Path\Resource\Modules " + } + } + } + } + #endregion + + #region Process Images + foreach ($TargetImage in $ImageList) + { + if ($pscmdlet.ShouldProcess($TargetImage, 'Invoke Windows Updates on Image')) + { + #region setup enviroment + $BaseImage = "$Path\BaseImage\$($TargetImage)_base.vhdx" + $UpdateImage = "$Path\BaseImage\$($TargetImage)_Update.vhdx" + $SysprepImage = "$Path\BaseImage\$($TargetImage)_Sysprep.vhdx" + $OutputVhd = "$Path\UpdatedImageShare\$($TargetImage).vhdx" + $OutputWim = "$Path\UpdatedImageShare\$($TargetImage).wim" + + $vmGeneration = 1 + $PartitionStyle = GetVHDPartitionStyle -vhd $BaseImage + if ($PartitionStyle -eq 'GPT') + { + $vmGeneration = 2 + } + $configData = Get-UpdateConfig -Path $Path + + $vhdData = Get-VHD -Path $BaseImage + #endregion + + #region create Diff disk + try + { + Write-Verbose -Message "[$($MyInvocation.MyCommand)] : Windows Update : New Diff Disk : Creating $UpdateImage from $BaseImage" + $null = New-VHD -Path $UpdateImage -ParentPath $BaseImage -ErrorAction Stop @ParametersToPass + } + catch + { + throw "error creating differencing disk $UpdateImage from $BaseImage" + } + #endregion + + #region Inject files + $RunWindowsUpdateAtStartup = { + Start-Transcript -Path $PSScriptRoot\AtStartup.log -Append + + $IpType = 'IPTYPEPLACEHOLDER' + $IPAddress = 'IPADDRESSPLACEHOLDER' + $SubnetMask = 'SUBNETMASKPLACEHOLDER' + $Gateway = 'GATEWAYPLACEHOLDER' + $DnsServer = 'DNSPLACEHOLDER' + + if (-not ($IpType -eq 'DHCP')) + { + Write-Verbose -Message 'Set Network : Getting network adaptor' -Verbose + $adapter = Get-NetAdapter | Where-Object -FilterScript { + $_.Status -eq 'up' + } + + Write-Verbose -Message "Set Network : removing existing config on $($adaptor.Name)" -Verbose + If (($adapter | Get-NetIPConfiguration).IPv4Address.IPAddress) + { + $adapter | Remove-NetIPAddress -AddressFamily $IpType -Confirm:$false + } + If (($adapter | Get-NetIPConfiguration).Ipv4DefaultGateway) + { + $adapter | Remove-NetRoute -AddressFamily $IpType -Confirm:$false + } + + $params = { + AddressFamily = $IpType + IPAddress = $IPAddress + PrefixLength = $SubnetMask + DefaultGateway = $Gateway + } + Write-Verbose -Message 'Set Network : Adding settings to adaptor' + Write-Verbose -Message $params -Verbose + $adapter | New-NetIPAddress @params + + Write-Verbose "Set Network : Set DNS to $DnsServer" -Verbose + $adapter | Set-DnsClientServerAddress -ServerAddresses $DnsServer + } + + try + { + Import-Module "$env:SystemDrive\PsTemp\Modules\PSWindowsUpdate" -Force -ErrorAction Stop + } + catch + { + Write-Error 'Unable to import update module' + Stop-Transcript + Stop-Computer -Force + } + + # Run pre-update script if it exists + if (Test-Path "$env:SystemDrive\PsTemp\PreUpdateScript.ps1") + { + Write-Verbose "Pre-Upate script : found $env:SystemDrive\PsTemp\PreUpdateScript.ps1" + & "$env:SystemDrive\PsTemp\PreUpdateScript.ps1" + } + + if ((Get-WUList -verbose -NotCategory 'Language packs').Count -gt 0) + { + Write-Verbose 'Windows updates : Updates needed, flaging drive as changed' -Verbose + Get-Date | Out-File $env:SystemDrive\PsTemp\changesMade.txt -Force + } + else + { + Write-Verbose 'Windows updates : No further updates' -Verbose + + if(-not ($IpType -eq 'DHCP')) + { + $adapter = Get-NetAdapter | Where-Object { + $_.Status -eq 'up' + } + $interface = $adapter | Get-NetIPInterface -AddressFamily $IpType + + Write-Verbose 'Set Network : Removing static config' -Verbose + If ($interface.Dhcp -eq 'Disabled') + { + If (($interface | Get-NetIPConfiguration).Ipv4DefaultGateway) + { + $interface | Remove-NetRoute -Confirm:$false + } + $interface | Set-NetIPInterface -Dhcp Enabled + $interface | Set-DnsClientServerAddress -ResetServerAddresses + } + } + Write-Verbose 'Shuting down' -Verbose + ## remove self so as to not triger updates if manual mantinance required + Remove-Item "$env:SystemDrive\PsTemp\AtStartup.ps1" + Stop-Transcript + Stop-Computer + } + + # Apply all non-language updates + Write-Verbose 'Windows updates : installing updates' -Verbose + Get-WUInstall -AcceptAll -IgnoreReboot -IgnoreUserInput -NotCategory 'Language packs' -Verbose + + # Run post-update script if it exists + if (Test-Path "$env:SystemDrive\PsTemp\PostUpdateScript.ps1") + { + Write-Verbose "Post-Update script : found $env:SystemDrive\PsTemp\PostUpdateScript.ps1" + & "$env:SystemDrive\PsTemp\PostUpdateScript.ps1" + } + + + if (Get-WURebootStatus -Silent) + { + Write-Verbose 'Windows updates : Reboot required to finish restarting' -Verbose + } + else + { + Write-Verbose 'Windows updates : Restarting to check for additional updates' -Verbose + } + Stop-Transcript + Restart-Computer -Force + } + + #region add configuration data into block + $block = $RunWindowsUpdateAtStartup | Out-String -Width 400 + + $block = $block.Replace('IPTYPEPLACEHOLDER', $configData.IpType) + $block = $block.Replace('IPADDRESSPLACEHOLDER', $configData.IPAddress) + $block = $block.Replace('SUBNETMASKPLACEHOLDER', $configData.SubnetMask) + $block = $block.Replace('GATEWAYPLACEHOLDER', $configData.Gateway) + $block = $block.Replace('DNSPLACEHOLDER', $configData.DnsServer) + + $RunWindowsUpdateAtStartup = [scriptblock]::Create($block) + #endregion + + $CopyInUpdateFilesBlock = { + if (-not (Test-Path -Path "$($driveLetter):\PsTemp")) + { + $null = mkdir -Path "$($driveLetter):\PsTemp" + } + if (-not (Test-Path -Path "$($driveLetter):\PsTemp\Modules")) + { + $null = mkdir -Path "$($driveLetter):\PsTemp\Modules" + } + $null = New-Item -Path "$($driveLetter):\PsTemp" -Name AtStartup.ps1 -ItemType 'file' -Value $RunWindowsUpdateAtStartup -Force + cleanupFile "$($driveLetter):\PsTemp\Modules\*" + $null = Copy-Item -Path "$Path\Resource\Modules\*" -Destination "$($driveLetter):\PsTemp\Modules\" -Recurse + + if ((Get-ChildItem "$($driveLetter):\PsTemp\Modules\PSWindowsUpdate" -File).count -eq 0) + { + Write-Verbose -Message 'Sidebyside detected in PSWindowsUpdate : switching to v4 compatability' + $newest = (Get-ChildItem "$($driveLetter):\PsTemp\Modules\PSWindowsUpdate" -Directory | Sort-Object LastWriteTime)[0] + Copy-Item -Path $newest.fullname -Destination "$($driveLetter):\PsTemp\Modules\PSWindowsUpdate_temp" -Recurse + cleanupFile "$($driveLetter):\PsTemp\Modules\PSWindowsUpdate" + Rename-Item -Path "$($driveLetter):\PsTemp\Modules\PSWindowsUpdate_temp" -NewName "$($driveLetter):\PsTemp\Modules\PSWindowsUpdate" + } + } + Write-Verbose -Message "[$($MyInvocation.MyCommand)] : Windows Update : Adding PSWindowsUpdate Module to $UpdateImage" + Write-Verbose -Message "[$($MyInvocation.MyCommand)] : Windows Update : updateting AtStartup script" + MountVHDandRunBlock -vhd $UpdateImage -block $CopyInUpdateFilesBlock + #endregion + + #region create vm and run updates + createRunAndWaitVM -vhdPath $UpdateImage -vmGeneration $vmGeneration -configData $configData @ParametersToPass + #endregion + + #region Detect results - Merge or discard. + $checkresultsBlock = { + Test-Path -Path "$($driveLetter):\PsTemp\ChangesMade.txt" + Remove-Item "$($driveLetter):\PsTemp\ChangesMade.txt" -ErrorAction SilentlyContinue + } + $ChangesMade = MountVHDandRunBlock -vhd $UpdateImage -block $checkresultsBlock + if ($ChangesMade) + { + Write-Verbose -Message "[$($MyInvocation.MyCommand)] : Windows Update : Changes detected : Merging $UpdateImage into $BaseImage" + Merge-VHD -Path $UpdateImage -DestinationPath $BaseImage @ParametersToPass + } + else + { + Write-Verbose -Message "[$($MyInvocation.MyCommand)] : Windows Update : No changes, discarding $UpdateImage" + cleanupFile $UpdateImage + } + #endregion + + if ($output -ne 'none') + { + #region Sysprep if changes or missing output vhd + if (($ChangesMade) -or (-not (Test-Path $OutputVhd))) + { + try + { + Write-Verbose -Message "[$($MyInvocation.MyCommand)] : SysPrep : New Diff Disk : Creating $SysprepImage from $BaseImage" + cleanupFile $SysprepImage + $null = New-VHD -Path $SysprepImage -ParentPath $BaseImage -ErrorAction Stop @ParametersToPass + } + catch + { + throw "error creating differencing disk $SysprepImage from $BaseImage" + } + + + $sysprepAtStartup = { + Start-Transcript -Path $PSScriptRoot\AtStartup.log -Append + # Run pre-sysprep script if it exists + if (Test-Path "$env:SystemDrive\PsTemp\PreSysprepScript.ps1") + { + & "$env:SystemDrive\PsTemp\PreSysprepScript.ps1" + } + + + # Remove Scedualed task + Write-Verbose -Message 'SysPrep : Removeing AtStartup task' -Verbose + if (Get-Command -Name Unregister-ScheduledTask -ErrorAction SilentlyContinue) + { + Unregister-ScheduledTask -TaskName AtStartup -Confirm:$false -Verbose + } + else + { + schtasks.exe /delete /TN 'AtStartup' /f + } + $params = @{ + 'FilePath' = "$ENV:SystemRoot\System32\Sysprep\Sysprep.exe" + 'ArgumentList' = '/generalize', '/oobe', '/shutdown' + 'NoNewWindow' = $true + 'Wait' = $true + 'RedirectStandardOutput' = "$($env:temp)\$($exeName)-StandardOutput.txt" + 'RedirectStandardError' = "$($env:temp)\$($exeName)-StandardError.txt" + 'PassThru' = $true + } + + Write-Verbose -Message 'SysPrep : starting Sysprep' -Verbose + $ret = Start-Process @params + Start-Sleep -Seconds 30 + Get-Date | Out-File c:\sysprepfail.txt + } + + $CopyInSysprepFilesBlock = { + $null = New-Item -Path "$($driveLetter):\PsTemp" -Name AtStartup.ps1 -ItemType 'file' -Value $sysprepAtStartup -Force + } + Write-Verbose -Message "[$($MyInvocation.MyCommand)] : SysPrep : updateting AtStartup script" + MountVHDandRunBlock -vhd $SysprepImage -block $CopyInSysprepFilesBlock + Write-Verbose -Message "[$($MyInvocation.MyCommand)] : SysPrep : Creating temp vm and waiting" + createRunAndWaitVM -vhdPath $SysprepImage -vmGeneration $vmGeneration -configData $configData @ParametersToPass + + MountVHDandRunBlock -vhd $SysprepImage -block { + if (Test-Path "$($driveLetter):\sysprepfail.txt") + { + throw 'Sysprep Failed!' + } + } + + $CleanupVhdBlock = { + cleanupFile "$($driveLetter):\Unattend.xml" + cleanupFile "$($driveLetter):\PsTemp" + attrib.exe -s -h "$($driveLetter):\pagefile.sys" + cleanupFile "$($driveLetter):\pagefile.sys" + if ($ReduceImageSize) + { + $null = Dism.exe /image:$($driveLetter):\ /Cleanup-Image /StartComponentCleanup /ResetBase + $null = Get-WindowsOptionalFeature -Path "$($driveLetter):\" | + Where-Object State -EQ -Value 'Disabled' | + Disable-WindowsOptionalFeature -Remove -Path "$($driveLetter):\" @ParametersToPass + } + } + Write-Verbose -Message "[$($MyInvocation.MyCommand)] : SysPrep : Removing PageFile and PsTemp" + Write-Verbose -Message "[$($MyInvocation.MyCommand)] : SysPrep : Cleaning SxS" + MountVHDandRunBlock -vhd $SysprepImage -block $CleanupVhdBlock + } + #endregion + + #region export WIM + if (($ChangesMade) -or (-not (Test-Path $OutputWim)) -or (-not (Test-Path $OutputVhd))) + { + Write-Verbose -Message "[$($MyInvocation.MyCommand)] : WIM : Creating $OutputWim" + cleanupFile $OutputWim + MountVHDandRunBlock -ReadOnly $SysprepImage -block { + $nul = New-WindowsImage -CapturePath "$($driveLetter):" -ImagePath $OutputWim -Name "$TargetImage Updated $(Get-Date)" @ParametersToPass + } + Write-Verbose -Message "[$($MyInvocation.MyCommand)] : WIM : removing $SysprepImage" + cleanupFile $SysprepImage + } + + #endregion + + #region create output VHD + if ((($ChangesMade) -or (-not (Test-Path $OutputVhd))) -and $output -eq 'both') + { + Write-Verbose -Message "[$($MyInvocation.MyCommand)] : VHD : Creating $OutputVhd from $OutputWim" + cleanupFile $OutputVhd + $layout = 'BIOS' + if ($PartitionStyle -eq 'GPT') + { + $layout = 'UEFI' + } + $dynamic = $false + if ($vhdData.VhdType -eq 'Dynamic') + { + $dynamic = $true + } + $param = @{ + Path = "$OutputVhd" + Size = $vhdData.Size + dynamic = $dynamic + DiskLayout = $layout + force = $true + SourcePath = "$OutputWim" + } + $nul = Convert-Wim2VHD @param @ParametersToPass + } + #endregion + } + } + } + #endregion +} diff --git a/deployment/dsc/azshcihost/WindowsImageTools/1.0.7/Functions/Mount-VhdAndRunBlock.ps1 b/deployment/dsc/azshcihost/WindowsImageTools/1.0.7/Functions/Mount-VhdAndRunBlock.ps1 new file mode 100644 index 0000000..b195035 --- /dev/null +++ b/deployment/dsc/azshcihost/WindowsImageTools/1.0.7/Functions/Mount-VhdAndRunBlock.ps1 @@ -0,0 +1,55 @@ +function Mount-VhdAndRunBlock +{ + <# + .Synopsis + Mount a VHD(x), runs a script block and unmounts the VHD(x) driveleter stored in $driveLetter + .DESCRIPTION + Us this function to read / write files inside a vhd. Any objects emited by the scriptblock are returned by this function. + .EXAMPLE + Mount-VhdAndRunBlock -Vhd c:\win10.vhdx -Block { Copy-Item -Path 'c:\myfiles\unattend.xml' -Destination "$($driveletter):\unattend.xml"} + .EXAMPLE + $fileFound = Mount-VhdAndRunBlock -Vhd c:\lab.vhdx -ReadOnly { test-path "$($driveletter):\scripts\changesmade.log" } + #> + param + ( + # Path to VHD(x) file + [Parameter(Mandatory = $true)] + [ValidateNotNull()] + [ValidateNotNullOrEmpty()] + [string] + $vhd, + + # Script block to execute (Drive letter stored in $driveletter) + [Parameter(Mandatory = $true)] + [scriptblock] + $block, + + # Mount the VHD(x) readonly, This is faster. Use when only reading files. + [switch] + $ReadOnly + ) + + # This function mounts a VHD, runs a script block and unmounts the VHD. + # Drive letter of the mounted VHD is stored in $driveLetter - can be used by script blocks + if($ReadOnly) + { + $virtualDisk = Mount-VHD -Path $vhd -ReadOnly -Passthru + } + else + { + $virtualDisk = Mount-VHD -Path $vhd -Passthru + } + # Workarround for new drive letters in script modules + $null = Get-PSDrive + $global:driveLetter = ($virtualDisk | + Get-Disk | + Get-Partition | + Get-Volume).DriveLetter + $newScriptBlock = [scriptblock]::Create($block.ToString()) + & $newScriptBlock + + Dismount-VHD $vhd + + # Wait 2 seconds for activity to clean up + Start-Sleep -Seconds 2 +} diff --git a/deployment/dsc/azshcihost/WindowsImageTools/1.0.7/Functions/New-Unattend.ps1 b/deployment/dsc/azshcihost/WindowsImageTools/1.0.7/Functions/New-Unattend.ps1 new file mode 100644 index 0000000..54fe38c --- /dev/null +++ b/deployment/dsc/azshcihost/WindowsImageTools/1.0.7/Functions/New-Unattend.ps1 @@ -0,0 +1,971 @@ +function New-UnattendXml +{ + <# + .Synopsis + Create a new Unattend.xml + .DESCRIPTION + This Command Creates a new Unattend.xml that skips any prompts, and sets the administrator password + Has options for: Adding user accounts + Auto logon a set number of times + Set the Computer Name + First Boot or First Logon powersrhell script + Product Key + TimeZone + Input, System and User Locals + UI Language + Registered Owner and Orginization + First Boot, First Logon and Every Logon Commands + Enable Administrator account without autologon (client OS) + + If no Path is provided a the file will be created in a temp folder and the path returned. + .EXAMPLE + New-UnattendXml -AdminPassword 'P@ssword' -logonCount 1 + .EXAMPLE + New-UnattendXml -Path c:\temp\Unattent.xml -AdminPassword 'P@ssword' -logonCount 100 -FirstLogonScriptPath c:\pstemp\firstrun.ps1 + #> + [CmdletBinding(DefaultParameterSetName = 'Basic_FirstLogonScript', + SupportsShouldProcess = $true)] + [OutputType([System.IO.FileInfo])] + Param + ( + # The password to have unattnd.xml set the local Administrator to (minimum lenght 8) + [Parameter(Mandatory = $true, + ValueFromPipeline = $true, + ValueFromPipelineByPropertyName = $true, + Position = 0)] + [ValidateNotNull()] + [ValidateNotNullOrEmpty()] + [Alias('AdminPassword')] + [PSCredential] + $AdminCredential, + + # User account/password to create and add to Administators group + [PSCredential[]] + $UserAccount, + + # Output Path + [Alias('FilePath', 'FullName', 'pspath', 'outfile')] + [string] + $Path = "$(New-TemporaryDirectory)\unattend.xml", + + # Number of times that the local Administrator account should automaticaly login (default 0) + [ValidateRange(0,1000)] + [int] + $LogonCount, + + # ComputerName (default = *) + [ValidateLength(1,15)] + [string] + $ComputerName = '*', + + # PowerShell Script to run on FirstLogon (ie. %SystemDrive%\PSTemp\FirstRun.ps1 ) + [Parameter(ParameterSetName = 'Basic_FirstLogonScript')] + [string] + $FirstLogonScriptPath, + + # PowerShell Script to run on FirstBoot (ie.: %SystemDrive%\PSTemp\FirstRun.ps1 ) Executed in system context dureing specialize phase + [Parameter(ParameterSetName = 'Basic_FirstBootScript')] + [string] + $FirstBootScriptPath, + + # The product key to use for the unattended installation. + [ValidatePattern('^[A-Z0-9]{5,5}-[A-Z0-9]{5,5}-[A-Z0-9]{5,5}-[A-Z0-9]{5,5}-[A-Z0-9]{5,5}$')] + [string] + $ProductKey, + + # Timezone (default: Central Standard Time) + [ValidateSet('Dateline Standard Time', + 'UTC-11', + 'Hawaiian Standard Time', + 'Alaskan Standard Time', + 'Pacific Standard Time (Mexico)', + 'Pacific Standard Time', + 'US Mountain Standard Time', + 'Mountain Standard Time (Mexico)', + 'Mountain Standard Time', + 'Central America Standard Time', + 'Central Standard Time', + 'Central Standard Time (Mexico)', + 'Canada Central Standard Time', + 'SA Pacific Standard Time', + 'Eastern Standard Time (Mexico)', + 'Eastern Standard Time', + 'US Eastern Standard Time', + 'Venezuela Standard Time', + 'Paraguay Standard Time', + 'Atlantic Standard Time', + 'Central Brazilian Standard Time', + 'SA Western Standard Time', + 'Newfoundland Standard Time', + 'E. South America Standard Time', + 'SA Eastern Standard Time', + 'Argentina Standard Time', + 'Greenland Standard Time', + 'Montevideo Standard Time', + 'Bahia Standard Time', + 'Pacific SA Standard Time', + 'UTC-02', + 'Mid-Atlantic Standard Time', + 'Azores Standard Time', + 'Cape Verde Standard Time', + 'Morocco Standard Time', + 'UTC', + 'GMT Standard Time', + 'Greenwich Standard Time', + 'W. Europe Standard Time', + 'Central Europe Standard Time', + 'Romance Standard Time', + 'Central European Standard Time', + 'W. Central Africa Standard Time', + 'Namibia Standard Time', + 'Jordan Standard Time', + 'GTB Standard Time', + 'Middle East Standard Time', + 'Egypt Standard Time', + 'Syria Standard Time', + 'E. Europe Standard Time', + 'South Africa Standard Time', + 'FLE Standard Time', + 'Turkey Standard Time', + 'Israel Standard Time', + 'Kaliningrad Standard Time', + 'Libya Standard Time', + 'Arabic Standard Time', + 'Arab Standard Time', + 'Belarus Standard Time', + 'Russian Standard Time', + 'E. Africa Standard Time', + 'Iran Standard Time', + 'Arabian Standard Time', + 'Azerbaijan Standard Time', + 'Russia Time Zone 3', + 'Mauritius Standard Time', + 'Georgian Standard Time', + 'Caucasus Standard Time', + 'Afghanistan Standard Time', + 'West Asia Standard Time', + 'Ekaterinburg Standard Time', + 'Pakistan Standard Time', + 'India Standard Time', + 'Sri Lanka Standard Time', + 'Nepal Standard Time', + 'Central Asia Standard Time', + 'Bangladesh Standard Time', + 'N. Central Asia Standard Time', + 'Myanmar Standard Time', + 'SE Asia Standard Time', + 'North Asia Standard Time', + 'China Standard Time', + 'North Asia East Standard Time', + 'Singapore Standard Time', + 'W. Australia Standard Time', + 'Taipei Standard Time', + 'Ulaanbaatar Standard Time', + 'North Korea Standard Time', + 'Tokyo Standard Time', + 'Korea Standard Time', + 'Yakutsk Standard Time', + 'Cen. Australia Standard Time', + 'AUS Central Standard Time', + 'E. Australia Standard Time', + 'AUS Eastern Standard Time', + 'West Pacific Standard Time', + 'Tasmania Standard Time', + 'Magadan Standard Time', + 'Vladivostok Standard Time', + 'Russia Time Zone 10', + 'Central Pacific Standard Time', + 'Russia Time Zone 11', + 'New Zealand Standard Time', + 'UTC+12', + 'Fiji Standard Time', + 'Kamchatka Standard Time', + 'Tonga Standard Time', + 'Samoa Standard Time', + 'Line Islands Standard Time')] + [string] + $TimeZone, + + # Specifies the system input locale and the keyboard layout (default: en-US) + [Parameter(ValueFromPipelineByPropertyName)] + [ValidateSet('en-US', + 'nl-NL', + 'fr-FR', + 'de-DE', + 'it-IT', + 'ja-JP', + 'es-ES', + 'ar-SA', + 'zh-CN', + 'zh-HK', + 'zh-TW', + 'cs-CZ', + 'da-DK', + 'fi-FI', + 'el-GR', + 'he-IL', + 'hu-HU', + 'ko-KR', + 'nb-NO', + 'pl-PL', + 'pt-BR', + 'pt-PT', + 'ru-RU', + 'sv-SE', + 'tr-TR', + 'bg-BG', + 'hr-HR', + 'et-EE', + 'lv-LV', + 'lt-LT', + 'ro-RO', + 'sr-Latn-CS', + 'sk-SK', + 'sl-SI', + 'th-TH', + 'uk-UA', + 'af-ZA', + 'sq-AL', + 'am-ET', + 'hy-AM', + 'as-IN', + 'az-Latn-AZ', + 'eu-ES', + 'be-BY', + 'bn-BD', + 'bn-IN', + 'bs-Cyrl-BA', + 'bs-Latn-BA', + 'ca-ES', + 'fil-PH', + 'gl-ES', + 'ka-GE', + 'gu-IN', + 'ha-Latn-NG', + 'hi-IN', + 'is-IS', + 'ig-NG', + 'id-ID', + 'iu-Latn-CA', + 'ga-IE', + 'xh-ZA', + 'zu-ZA', + 'kn-IN', + 'kk-KZ', + 'km-KH', + 'rw-RW', + 'sw-KE', + 'kok-IN', + 'ky-KG', + 'lo-LA', + 'lb-LU', + 'mk-MK', + 'ms-BN', + 'ms-MY', + 'ml-IN', + 'mt-MT', + 'mi-NZ', + 'mr-IN', + 'ne-NP', + 'nn-NO', + 'or-IN', + 'ps-AF', + 'fa-IR', + 'pa-IN', + 'quz-PE', + 'sr-Cyrl-CS', + 'nso-ZA', + 'tn-ZA', + 'si-LK', + 'ta-IN', + 'tt-RU', + 'te-IN', + 'ur-PK', + 'uz-Latn-UZ', + 'vi-VN', + 'cy-GB', + 'wo-SN', + 'yo-NG')] + [Alias('keyboardlayout')] + [String] + $InputLocale, + + # Specifies the language for non-Unicode programs (default: en-US) + [ValidateSet('en-US', + 'nl-NL', + 'fr-FR', + 'de-DE', + 'it-IT', + 'ja-JP', + 'es-ES', + 'ar-SA', + 'zh-CN', + 'zh-HK', + 'zh-TW', + 'cs-CZ', + 'da-DK', + 'fi-FI', + 'el-GR', + 'he-IL', + 'hu-HU', + 'ko-KR', + 'nb-NO', + 'pl-PL', + 'pt-BR', + 'pt-PT', + 'ru-RU', + 'sv-SE', + 'tr-TR', + 'bg-BG', + 'hr-HR', + 'et-EE', + 'lv-LV', + 'lt-LT', + 'ro-RO', + 'sr-Latn-CS', + 'sk-SK', + 'sl-SI', + 'th-TH', + 'uk-UA', + 'af-ZA', + 'sq-AL', + 'am-ET', + 'hy-AM', + 'as-IN', + 'az-Latn-AZ', + 'eu-ES', + 'be-BY', + 'bn-BD', + 'bn-IN', + 'bs-Cyrl-BA', + 'bs-Latn-BA', + 'ca-ES', + 'fil-PH', + 'gl-ES', + 'ka-GE', + 'gu-IN', + 'ha-Latn-NG', + 'hi-IN', + 'is-IS', + 'ig-NG', + 'id-ID', + 'iu-Latn-CA', + 'ga-IE', + 'xh-ZA', + 'zu-ZA', + 'kn-IN', + 'kk-KZ', + 'km-KH', + 'rw-RW', + 'sw-KE', + 'kok-IN', + 'ky-KG', + 'lo-LA', + 'lb-LU', + 'mk-MK', + 'ms-BN', + 'ms-MY', + 'ml-IN', + 'mt-MT', + 'mi-NZ', + 'mr-IN', + 'ne-NP', + 'nn-NO', + 'or-IN', + 'ps-AF', + 'fa-IR', + 'pa-IN', + 'quz-PE', + 'sr-Cyrl-CS', + 'nso-ZA', + 'tn-ZA', + 'si-LK', + 'ta-IN', + 'tt-RU', + 'te-IN', + 'ur-PK', + 'uz-Latn-UZ', + 'vi-VN', + 'cy-GB', + 'wo-SN', + 'yo-NG')] + [Parameter(ValueFromPipelineByPropertyName)] + [String] + $SystemLocale, + + # Specifies the per-user settings used for formatting dates, times, currency and numbers (default: en-US) + [ValidateSet('en-US', + 'nl-NL', + 'fr-FR', + 'de-DE', + 'it-IT', + 'ja-JP', + 'es-ES', + 'ar-SA', + 'zh-CN', + 'zh-HK', + 'zh-TW', + 'cs-CZ', + 'da-DK', + 'fi-FI', + 'el-GR', + 'he-IL', + 'hu-HU', + 'ko-KR', + 'nb-NO', + 'pl-PL', + 'pt-BR', + 'pt-PT', + 'ru-RU', + 'sv-SE', + 'tr-TR', + 'bg-BG', + 'hr-HR', + 'et-EE', + 'lv-LV', + 'lt-LT', + 'ro-RO', + 'sr-Latn-CS', + 'sk-SK', + 'sl-SI', + 'th-TH', + 'uk-UA', + 'af-ZA', + 'sq-AL', + 'am-ET', + 'hy-AM', + 'as-IN', + 'az-Latn-AZ', + 'eu-ES', + 'be-BY', + 'bn-BD', + 'bn-IN', + 'bs-Cyrl-BA', + 'bs-Latn-BA', + 'ca-ES', + 'fil-PH', + 'gl-ES', + 'ka-GE', + 'gu-IN', + 'ha-Latn-NG', + 'hi-IN', + 'is-IS', + 'ig-NG', + 'id-ID', + 'iu-Latn-CA', + 'ga-IE', + 'xh-ZA', + 'zu-ZA', + 'kn-IN', + 'kk-KZ', + 'km-KH', + 'rw-RW', + 'sw-KE', + 'kok-IN', + 'ky-KG', + 'lo-LA', + 'lb-LU', + 'mk-MK', + 'ms-BN', + 'ms-MY', + 'ml-IN', + 'mt-MT', + 'mi-NZ', + 'mr-IN', + 'ne-NP', + 'nn-NO', + 'or-IN', + 'ps-AF', + 'fa-IR', + 'pa-IN', + 'quz-PE', + 'sr-Cyrl-CS', + 'nso-ZA', + 'tn-ZA', + 'si-LK', + 'ta-IN', + 'tt-RU', + 'te-IN', + 'ur-PK', + 'uz-Latn-UZ', + 'vi-VN', + 'cy-GB', + 'wo-SN', + 'yo-NG')] + [Parameter(ValueFromPipelineByPropertyName)] + [String] + $UserLocale, + + # Specifies the system default user interface (UI) language (default: en-US) + [ValidateSet('en-US', + 'nl-NL', + 'fr-FR', + 'de-DE', + 'it-IT', + 'ja-JP', + 'es-ES', + 'ar-SA', + 'zh-CN', + 'zh-HK', + 'zh-TW', + 'cs-CZ', + 'da-DK', + 'fi-FI', + 'el-GR', + 'he-IL', + 'hu-HU', + 'ko-KR', + 'nb-NO', + 'pl-PL', + 'pt-BR', + 'pt-PT', + 'ru-RU', + 'sv-SE', + 'tr-TR', + 'bg-BG', + 'hr-HR', + 'et-EE', + 'lv-LV', + 'lt-LT', + 'ro-RO', + 'sr-Latn-CS', + 'sk-SK', + 'sl-SI', + 'th-TH', + 'uk-UA', + 'af-ZA', + 'sq-AL', + 'am-ET', + 'hy-AM', + 'as-IN', + 'az-Latn-AZ', + 'eu-ES', + 'be-BY', + 'bn-BD', + 'bn-IN', + 'bs-Cyrl-BA', + 'bs-Latn-BA', + 'ca-ES', + 'fil-PH', + 'gl-ES', + 'ka-GE', + 'gu-IN', + 'ha-Latn-NG', + 'hi-IN', + 'is-IS', + 'ig-NG', + 'id-ID', + 'iu-Latn-CA', + 'ga-IE', + 'xh-ZA', + 'zu-ZA', + 'kn-IN', + 'kk-KZ', + 'km-KH', + 'rw-RW', + 'sw-KE', + 'kok-IN', + 'ky-KG', + 'lo-LA', + 'lb-LU', + 'mk-MK', + 'ms-BN', + 'ms-MY', + 'ml-IN', + 'mt-MT', + 'mi-NZ', + 'mr-IN', + 'ne-NP', + 'nn-NO', + 'or-IN', + 'ps-AF', + 'fa-IR', + 'pa-IN', + 'quz-PE', + 'sr-Cyrl-CS', + 'nso-ZA', + 'tn-ZA', + 'si-LK', + 'ta-IN', + 'tt-RU', + 'te-IN', + 'ur-PK', + 'uz-Latn-UZ', + 'vi-VN', + 'cy-GB', + 'wo-SN', + 'yo-NG')] + [Parameter(ValueFromPipelineByPropertyName)] + [String] + $UILanguage, + + # Registered Owner (default: 'Valued Customer') + [Parameter(ValueFromPipelineByPropertyName)] + [ValidateNotNull()] + [String] + $RegisteredOwner, + + # Registered Organization (default: 'Valued Customer') + [Parameter(ValueFromPipelineByPropertyName)] + [ValidateNotNull()] + [String] + $RegisteredOrganization, + + # Array of hashtables with Description, Order, and Path keys, and optional Domain, Password(plain text), username keys. Executed by in the system context + [Parameter(ValueFromPipelineByPropertyName = $true, + ParameterSetName = 'Advanced')] + [Hashtable[]] + $FirstBootExecuteCommand, + + # Array of hashtables with Description, Order and CommandLine keys. Execuded at first logon of an Administrator, will auto elivate + [Parameter(ValueFromPipelineByPropertyName = $true, + ParameterSetName = 'Advanced')] + [Hashtable[]] + $FirstLogonExecuteCommand, + + # Array of hashtables with Description, Order and CommandLine keys. Executed at every logon, does not elivate. + [Parameter(ValueFromPipelineByPropertyName = $true, + ParameterSetName = 'Advanced')] + [Hashtable[]] + $EveryLogonExecuteCommand, + + # Enable Local Administrator account (default $true) this is needed for client OS if your not useing autologon or adding aditional admin users. + [switch] + $enableAdministrator + ) + + Begin + { + $templateUnattendXml = [xml] @' +<?xml version="1.0" encoding="utf-8"?> +<unattend xmlns="urn:schemas-microsoft-com:unattend"> + <settings pass="specialize"> + <component name="Microsoft-Windows-Deployment" 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"></component> + <component name="Microsoft-Windows-Deployment" processorArchitecture="x86" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS" xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"></component> + <component name="Microsoft-Windows-Shell-Setup" 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"></component> + <component name="Microsoft-Windows-Shell-Setup" processorArchitecture="x86" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS" xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"></component> + </settings> + <settings pass="oobeSystem"> + <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> + <UserLocale>en-US</UserLocale> + </component> + <component name="Microsoft-Windows-International-Core" processorArchitecture="x86" 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> + <UserLocale>en-US</UserLocale> + </component> + <component name="Microsoft-Windows-Shell-Setup" 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"> + <OOBE> + <HideEULAPage>true</HideEULAPage> + <HideWirelessSetupInOOBE>true</HideWirelessSetupInOOBE> + <NetworkLocation>Work</NetworkLocation> + <ProtectYourPC>1</ProtectYourPC> + <SkipUserOOBE>true</SkipUserOOBE> + <SkipMachineOOBE>true</SkipMachineOOBE> + </OOBE> + <TimeZone>GMT Standard Time</TimeZone> + <UserAccounts> + <AdministratorPassword> + <Value></Value> + <PlainText>false</PlainText> + </AdministratorPassword> + </UserAccounts> + <RegisteredOrganization>Generic Organization</RegisteredOrganization> + <RegisteredOwner>Generic Owner</RegisteredOwner> + </component> + <component name="Microsoft-Windows-Shell-Setup" processorArchitecture="x86" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS" xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> + <OOBE> + <HideEULAPage>true</HideEULAPage> + <HideWirelessSetupInOOBE>true</HideWirelessSetupInOOBE> + <NetworkLocation>Work</NetworkLocation> + <ProtectYourPC>1</ProtectYourPC> + <SkipUserOOBE>true</SkipUserOOBE> + <SkipMachineOOBE>true</SkipMachineOOBE> + </OOBE> + <TimeZone>GMT Standard Time</TimeZone> + <UserAccounts> + <AdministratorPassword> + <Value></Value> + <PlainText>false</PlainText> + </AdministratorPassword> + </UserAccounts> + <RegisteredOrganization>Generic Organization</RegisteredOrganization> + <RegisteredOwner>Generic Owner</RegisteredOwner> + </component> + </settings> +</unattend> +'@ + + $PowerShellStartupCmd = '%SystemRoot%\System32\WindowsPowerShell\v1.0\powershell.exe -NoProfile -ExecutionPolicy Bypass -File' + + if ($LogonCount -gt 0) + { + Write-Warning -Message '-Autologon places the Administrator password in plain txt' + } + } + Process + { + if ($pscmdlet.ShouldProcess('$path', 'Create new Unattended.xml')) + { + if ($FirstBootScriptPath) + { + Write-Verbose -Message "[$($MyInvocation.MyCommand)] Adding PowerShell script to First boot command" + $FirstBootExecuteCommand = @(@{ + Description = 'PowerShell First boot script' + order = 1 + path = "%SystemRoot%\System32\WindowsPowerShell\v1.0\powershell.exe -NoProfile -ExecutionPolicy Bypass -File `"$FirstBootScriptPath`"" + }) + } + + if ($FirstLogonScriptPath) + { + Write-Verbose -Message "[$($MyInvocation.MyCommand)] Adding PowerShell script to First Logon command" + $FirstLogonExecuteCommand = @(@{ + Description = 'PowerShell First logon script' + order = 1 + CommandLine = "%SystemRoot%\System32\WindowsPowerShell\v1.0\powershell.exe -NoProfile -ExecutionPolicy Bypass -File `"$FirstBootScriptPath`"" + }) + } + + if ($enableAdministrator) + { + Write-Verbose -Message "[$($MyInvocation.MyCommand)] Enabeling Administrator via First boot command" + if ($FirstBootExecuteCommand) + { + $FirstBootExecuteCommand = $FirstBootExecuteCommand + @{ + Description = 'Enable Administrator' + order = 0 + path = 'net user administrator /active:yes' + } + } + else + { + $FirstBootExecuteCommand = @{ + Description = 'Enable Administrator' + order = 0 + path = 'net user administrator /active:yes' + } + } + } + else + { + if (-not ($UserAccount) ) + { + Write-Warning -Message "$Path only usable on a server SKU, for a client OS, use either -EnableAdministrator or -UserAccount" + } + } + + [xml] $unattendXml = $templateUnattendXml + foreach ($setting in $unattendXml.Unattend.Settings) + { + foreach($component in $setting.Component) + { + if ($setting.'Pass' -eq 'specialize' -and $component.'Name' -eq 'Microsoft-Windows-Deployment' ) + { + if (($FirstBootExecuteCommand -ne $null -or $FirstBootExecuteCommand.Length -gt 0) -and $component.'processorArchitecture' -eq 'x86') + { + Write-Verbose -Message "[$($MyInvocation.MyCommand)] Adding first boot command(s)" + $commandOrder = 1 + $runSynchronousElement = $component.AppendChild($unattendXml.CreateElement('RunSynchronous','urn:schemas-microsoft-com:unattend')) + foreach ($synchronousCommand in ($FirstBootExecuteCommand | Sort-Object -Property { + $_.order + })) + { + $syncCommandElement = $runSynchronousElement.AppendChild($unattendXml.CreateElement('RunSynchronousCommand','urn:schemas-microsoft-com:unattend')) + $null = $syncCommandElement.SetAttribute('action','http://schemas.microsoft.com/WMIConfig/2002/State','add') + $syncCommandDescriptionElement = $syncCommandElement.AppendChild($unattendXml.CreateElement('Description','urn:schemas-microsoft-com:unattend')) + $syncCommandDescriptionTextNode = $syncCommandDescriptionElement.AppendChild($unattendXml.CreateTextNode($synchronousCommand['Description'])) + $syncCommandOrderElement = $syncCommandElement.AppendChild($unattendXml.CreateElement('Order','urn:schemas-microsoft-com:unattend')) + $syncCommandOrderTextNode = $syncCommandOrderElement.AppendChild($unattendXml.CreateTextNode($commandOrder)) + $syncCommandPathElement = $syncCommandElement.AppendChild($unattendXml.CreateElement('Path','urn:schemas-microsoft-com:unattend')) + $syncCommandPathTextNode = $syncCommandPathElement.AppendChild($unattendXml.CreateTextNode($synchronousCommand['Path'])) + $commandOrder++ + } + } + } + if (($setting.'Pass' -eq 'specialize') -and ($component.'Name' -eq 'Microsoft-Windows-Shell-Setup')) + { + if ($ComputerName) + { + Write-Verbose -Message "[$($MyInvocation.MyCommand)] Adding custom computername for $($component.'processorArchitecture') Architecture" + $computerNameElement = $component.AppendChild($unattendXml.CreateElement('ComputerName','urn:schemas-microsoft-com:unattend')) + $computerNameTextNode = $computerNameElement.AppendChild($unattendXml.CreateTextNode($ComputerName)) + } + if ($ProductKey) + { + Write-Verbose -Message "[$($MyInvocation.MyCommand)] Adding Product key for $($component.'processorArchitecture') Architecture" + $productKeyElement = $component.AppendChild($unattendXml.CreateElement('ProductKey','urn:schemas-microsoft-com:unattend')) + $productKeyTextNode = $productKeyElement.AppendChild($unattendXml.CreateTextNode($ProductKey.ToUpper())) + } + } + + if (($setting.'Pass' -eq 'oobeSystem') -and ($component.'Name' -eq 'Microsoft-Windows-International-Core')) + { + if ($InputLocale) + { + Write-Verbose -Message "[$($MyInvocation.MyCommand)] Adding Input Locale for $($component.'processorArchitecture') Architecture" + $component.InputLocale = $InputLocale + } + if ($SystemLocale) + { + Write-Verbose -Message "[$($MyInvocation.MyCommand)] Adding System Locale for $($component.'processorArchitecture') Architecture" + $component.SystemLocale = $SystemLocale + } + if ($UILanguage) + { + Write-Verbose -Message "[$($MyInvocation.MyCommand)] Adding UI Language for $($component.'processorArchitecture') Architecture" + $component.UILanguage = $UILanguage + } + if ($UserLocale) + { + Write-Verbose -Message "[$($MyInvocation.MyCommand)] Adding User Locale for $($component.'processorArchitecture') Architecture" + $component.UserLocale = $UserLocale + } + } + + if (($setting.'Pass' -eq 'oobeSystem') -and ($component.'Name' -eq 'Microsoft-Windows-Shell-Setup')) + { + if ($TimeZone) + { + Write-Verbose -Message "[$($MyInvocation.MyCommand)] Adding Time Zone for $($component.'processorArchitecture') Architecture" + $component.TimeZone = $TimeZone + } + Write-Verbose -Message "[$($MyInvocation.MyCommand)] Adding Administrator Passwords for $($component.'processorArchitecture') Architecture" + $concatenatedPassword = '{0}AdministratorPassword' -f $AdminCredential.GetNetworkCredential().password + $component.UserAccounts.AdministratorPassword.Value = [System.Convert]::ToBase64String([System.Text.Encoding]::Unicode.GetBytes($concatenatedPassword)) + if ($RegisteredOrganization) + { + Write-Verbose -Message "[$($MyInvocation.MyCommand)] Adding Registred Organization for $($component.'processorArchitecture') Architecture" + $component.RegisteredOrganization = $RegisteredOrganization + } + if ($RegisteredOwner) + { + Write-Verbose -Message "[$($MyInvocation.MyCommand)] Adding Registered Owner for $($component.'processorArchitecture') Architecture" + $component.RegisteredOwner = $RegisteredOwner + } + if ($UserAccount) + { + Write-Verbose -Message "[$($MyInvocation.MyCommand)] Adding User Account(s) for $($component.'processorArchitecture') Architecture" + $UserAccountsElement = $component.UserAccounts + $LocalAccountsElement = $UserAccountsElement.AppendChild($unattendXml.CreateElement('LocalAccounts','urn:schemas-microsoft-com:unattend')) + foreach ($Account in $UserAccount) + { + $LocalAccountElement = $LocalAccountsElement.AppendChild($unattendXml.CreateElement('LocalAccount','urn:schemas-microsoft-com:unattend')) + $LocalAccountPasswordElement = $LocalAccountElement.AppendChild($unattendXml.CreateElement('Password','urn:schemas-microsoft-com:unattend')) + $LocalAccountPasswordValueElement = $LocalAccountPasswordElement.AppendChild($unattendXml.CreateElement('Value','urn:schemas-microsoft-com:unattend')) + $LocalAccountPasswordPlainTextElement = $LocalAccountPasswordElement.AppendChild($unattendXml.CreateElement('PlainText','urn:schemas-microsoft-com:unattend')) + $LocalAccountDisplayNameElement = $LocalAccountElement.AppendChild($unattendXml.CreateElement('DisplayName','urn:schemas-microsoft-com:unattend')) + $LocalAccountGroupElement = $LocalAccountElement.AppendChild($unattendXml.CreateElement('Group','urn:schemas-microsoft-com:unattend')) + $LocalAccountNameElement = $LocalAccountElement.AppendChild($unattendXml.CreateElement('Name','urn:schemas-microsoft-com:unattend')) + + $null = $LocalAccountElement.SetAttribute('action','http://schemas.microsoft.com/WMIConfig/2002/State','add') + $concatenatedPassword = '{0}Password' -f $Account.GetNetworkCredential().password + $null = $LocalAccountPasswordValueElement.AppendChild($unattendXml.CreateTextNode([System.Convert]::ToBase64String([System.Text.Encoding]::Unicode.GetBytes($concatenatedPassword)))) + $null = $LocalAccountPasswordPlainTextElement.AppendChild($unattendXml.CreateTextNode('false')) + $null = $LocalAccountDisplayNameElement.AppendChild($unattendXml.CreateTextNode($Account.UserName)) + $null = $LocalAccountGroupElement.AppendChild($unattendXml.CreateTextNode('Administrators')) + $null = $LocalAccountNameElement.AppendChild($unattendXml.CreateTextNode($Account.UserName)) + } + } + + if ($LogonCount) + { + Write-Verbose -Message "[$($MyInvocation.MyCommand)] Adding Autologon for $($component.'processorArchitecture') Architecture" + $autoLogonElement = $component.AppendChild($unattendXml.CreateElement('AutoLogon','urn:schemas-microsoft-com:unattend')) + $autoLogonPasswordElement = $autoLogonElement.AppendChild($unattendXml.CreateElement('Password','urn:schemas-microsoft-com:unattend')) + $autoLogonPasswordValueElement = $autoLogonPasswordElement.AppendChild($unattendXml.CreateElement('Value','urn:schemas-microsoft-com:unattend')) + $autoLogonCountElement = $autoLogonElement.AppendChild($unattendXml.CreateElement('LogonCount','urn:schemas-microsoft-com:unattend')) + $autoLogonUsernameElement = $autoLogonElement.AppendChild($unattendXml.CreateElement('Username','urn:schemas-microsoft-com:unattend')) + $autoLogonEnabledElement = $autoLogonElement.AppendChild($unattendXml.CreateElement('Enabled','urn:schemas-microsoft-com:unattend')) + + $null = $autoLogonPasswordValueElement.AppendChild($unattendXml.CreateTextNode($AdminCredential.GetNetworkCredential().password)) + $null = $autoLogonCountElement.AppendChild($unattendXml.CreateTextNode($LogonCount)) + $null = $autoLogonUsernameElement.AppendChild($unattendXml.CreateTextNode('administrator')) + $null = $autoLogonEnabledElement.AppendChild($unattendXml.CreateTextNode('true')) + } + + if (($FirstLogonExecuteCommand -ne $null -or $FirstBootExecuteCommand.Length -gt 0) -and $component.'processorArchitecture' -eq 'x86') + { + Write-Verbose -Message "[$($MyInvocation.MyCommand)] Adding First Logon Commands" + $commandOrder = 1 + $FirstLogonCommandsElement = $component.AppendChild($unattendXml.CreateElement('FirstLogonCommands','urn:schemas-microsoft-com:unattend')) + foreach ($command in ($FirstLogonExecuteCommand | Sort-Object -Property { + $_.order + })) + { + $CommandElement = $FirstLogonCommandsElement.AppendChild($unattendXml.CreateElement('SynchronousCommand','urn:schemas-microsoft-com:unattend')) + $CommandDescriptionElement = $CommandElement.AppendChild($unattendXml.CreateElement('Description','urn:schemas-microsoft-com:unattend')) + $CommandOrderElement = $CommandElement.AppendChild($unattendXml.CreateElement('Order','urn:schemas-microsoft-com:unattend')) + $CommandCommandLineElement = $CommandElement.AppendChild($unattendXml.CreateElement('CommandLine','urn:schemas-microsoft-com:unattend')) + $CommandRequireInputlement = $CommandElement.AppendChild($unattendXml.CreateElement('RequiresUserInput','urn:schemas-microsoft-com:unattend')) + + $null = $CommandElement.SetAttribute('action','http://schemas.microsoft.com/WMIConfig/2002/State','add') + $null = $CommandDescriptionElement.AppendChild($unattendXml.CreateTextNode($command['Description'])) + $null = $CommandOrderElement.AppendChild($unattendXml.CreateTextNode($commandOrder)) + $null = $CommandCommandLineElement.AppendChild($unattendXml.CreateTextNode($command['CommandLine'])) + $null = $CommandRequireInputlement.AppendChild($unattendXml.CreateTextNode('false')) + $commandOrder++ + } + } + if (($EveryLogonExecuteCommand -ne $null -or $FirstBootExecuteCommand.Length -gt 0) -and $component.'processorArchitecture' -eq 'x86') + { + Write-Verbose -Message "[$($MyInvocation.MyCommand)] Adding Every-Logon Commands" + $commandOrder = 1 + $FirstLogonCommandsElement = $component.AppendChild($unattendXml.CreateElement('LogonCommands','urn:schemas-microsoft-com:unattend')) + foreach ($command in ($EveryLogonExecuteCommand | Sort-Object -Property { + $_.order + })) + { + $CommandElement = $FirstLogonCommandsElement.AppendChild($unattendXml.CreateElement('AsynchronousCommand','urn:schemas-microsoft-com:unattend')) + $CommandDescriptionElement = $CommandElement.AppendChild($unattendXml.CreateElement('Description','urn:schemas-microsoft-com:unattend')) + $CommandOrderElement = $CommandElement.AppendChild($unattendXml.CreateElement('Order','urn:schemas-microsoft-com:unattend')) + $CommandCommandLineElement = $CommandElement.AppendChild($unattendXml.CreateElement('CommandLine','urn:schemas-microsoft-com:unattend')) + $CommandRequireInputlement = $CommandElement.AppendChild($unattendXml.CreateElement('RequiresUserInput','urn:schemas-microsoft-com:unattend')) + + $null = $CommandElement.SetAttribute('action','http://schemas.microsoft.com/WMIConfig/2002/State','add') + $null = $CommandDescriptionElement.AppendChild($unattendXml.CreateTextNode($command['Description'])) + $null = $CommandOrderElement.AppendChild($unattendXml.CreateTextNode($commandOrder)) + $null = $CommandCommandLineElement.AppendChild($unattendXml.CreateTextNode($command['CommandLine'])) + $null = $CommandRequireInputlement.AppendChild($unattendXml.CreateTextNode('false')) + $commandOrder++ + } + } + } + } #end foreach setting.Component + } #end foreach unattendXml.Unattend.Settings + + Write-Verbose -Message "[$($MyInvocation.MyCommand)] Saving file" + + $unattendXml.Save($Path) + Get-ChildItem $Path + # } + # catch + # { + # throw $_.Exception.Message + # } + } + } +} + + +function Get-UnattendChunk +{ + param + ( + [string] $pass, + [string] $component, + [string] $arch, + [xml] $unattend + ) + + # Helper function that returns one component chunk from the Unattend XML data structure + return $unattend.unattend.settings | + Where-Object -Property pass -EQ -Value $pass | + Select-Object -ExpandProperty component | + Where-Object -Property name -EQ -Value $component | + Where-Object -Property processorArchitecture -EQ -Value $arch +} diff --git a/deployment/dsc/azshcihost/WindowsImageTools/1.0.7/Functions/New-WindowsImageToolsExample.ps1 b/deployment/dsc/azshcihost/WindowsImageTools/1.0.7/Functions/New-WindowsImageToolsExample.ps1 new file mode 100644 index 0000000..689d0f6 --- /dev/null +++ b/deployment/dsc/azshcihost/WindowsImageTools/1.0.7/Functions/New-WindowsImageToolsExample.ps1 @@ -0,0 +1,485 @@ +function New-WindowsImageToolsExample +{ + <# + .Synopsis + Create folders and script examples on the use of Windows Image Tools + .DESCRIPTION + This Command creates the folders structures and example files needed to use Windows Image Tools to auto update windows images. + .EXAMPLE + New-WitExample -Path c:\WitExample + .NOTES + This is a work in progress + #> + [CmdletBinding(SupportsShouldProcess = $true + )] + [OutputType([System.IO.DirectoryInfo])] + Param + ( + # Path path to Folder/Directory to create (should not exist) + [Parameter(Mandatory = $true, + Position = 0)] + [ValidateNotNull()] + [ValidateNotNullOrEmpty()] + [ValidateScript({ + If (Test-Path -Path $_) + { + throw "$_ allready exist" + } + else + { + $true + } + })] + [Alias('FullName')] + [string]$Path + ) + + if ($pscmdlet.ShouldProcess($Path, 'Create new Windows Image Tools Example')) + { + #region File Content + $DownloadEvalIsoContent = { + Write-Warning -Message 'Eval copies are only good for a short period then will automaticaly shutdown if not licenced.' + function BitsDownload + { + [CmdletBinding()] + param ( + [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [System.String] $DestinationPath, + [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [System.String] $Uri + ) + $destinationFilename = [System.IO.Path]::GetFileName($DestinationPath) + $startBitsTransferParams = @{ + Source = $Uri + Destination = $DestinationPath + TransferType = 'Download' + DisplayName = "Downloading $destinationFilename" + Description = $Uri + Priority = 'Foreground' + } + Start-BitsTransfer @startBitsTransferParams #-ErrorAction Stop + } #end function SetBitsDownload + + $win10Evalx64 = 'http://download.microsoft.com/download/B/B/3/BB3611B6-9781-437F-A293-AB43B85C2190/10586.0.151029-1700.TH2_RELEASE_CLIENTENTERPRISEEVAL_OEMRET_X64FRE_EN-US.ISO' + $Win10Evalx86 = 'http://download.microsoft.com/download/B/B/3/BB3611B6-9781-437F-A293-AB43B85C2190/10586.0.151029-1700.TH2_RELEASE_CLIENTENTERPRISEEVAL_OEMRET_X86FRE_EN-US.ISO' + $Win81Evalx64 = 'http://download.microsoft.com/download/B/9/9/B999286E-0A47-406D-8B3D-5B5AD7373A4A/9600.17050.WINBLUE_REFRESH.140317-1640_X64FRE_ENTERPRISE_EVAL_EN-US-IR3_CENA_X64FREE_EN-US_DV9.ISO' + $Win81Evalx86 = 'http://download.microsoft.com/download/B/9/9/B999286E-0A47-406D-8B3D-5B5AD7373A4A/9600.17050.WINBLUE_REFRESH.140317-1640_X86FRE_ENTERPRISE_EVAL_EN-US-IR3_CENA_X86FREE_EN-US_DV9.ISO' + $Srv2016tp4Eval = 'http://download.microsoft.com/download/C/2/5/C257AD1A-45C1-48F9-B31C-5D37D6463123/10586.0.151029-1700.TH2_RELEASE_SERVER_OEMRET_X64FRE_EN-US.ISO' + $HyperV2016tp4Eval = 'http://download.microsoft.com/download/C/2/5/C257AD1A-45C1-48F9-B31C-5D37D6463123/10586.0.151029-1700.TH2_RELEASE_SERVERHYPERCORE_OEM_X64FRE_EN-US.ISO' + $Srv2012r2Eval = 'http://download.microsoft.com/download/6/2/A/62A76ABB-9990-4EFC-A4FE-C7D698DAEB96/9600.17050.WINBLUE_REFRESH.140317-1640_X64FRE_SERVER_EVAL_EN-US-IR3_SSS_X64FREE_EN-US_DV9.ISO' + $HyperV2012r2Eval = 'http://download.microsoft.com/download/F/7/D/F7DF966B-5C40-4674-9A32-D83D869A3244/9600.16384.WINBLUE_RTM.130821-1623_X64FRE_SERVERHYPERCORE_EN-US-IRM_SHV_X64FRE_EN-US_DV5.ISO' + + if (-not (Test-Path -Path $PSScriptRoot\ISO\Win10Evalx64.ISO)) + { + Write-Verbose -Message 'win10x64' -Verbose + BitsDownload -Uri $win10Evalx64 -DestinationPath $PSScriptRoot\ISO\Win10Evalx64.ISO + } + if (-not (Test-Path -Path $PSScriptRoot\ISO\Win10Evalx86.ISO)) + { + Write-Verbose -Message 'win10x86' -Verbose + BitsDownload -Uri $Win10Evalx86 -DestinationPath $PSScriptRoot\ISO\Win10Evalx86.ISO + } + if (-not (Test-Path -Path $PSScriptRoot\ISO\Win81Evalx64.ISO)) + { + Write-Verbose -Message 'win81x64' -Verbose + BitsDownload -Uri $Win81Evalx64 -DestinationPath $PSScriptRoot\ISO\Win81Evalx64.ISO + } + if (-not (Test-Path -Path $PSScriptRoot\ISO\Win81Evalx86.ISO)) + { + Write-Verbose -Message 'win81x86' -Verbose + BitsDownload -Uri $Win81Evalx86 -DestinationPath $PSScriptRoot\ISO\Win81Evalx86.ISO + } + if (-not (Test-Path -Path $PSScriptRoot\ISO\Srv2016tp4Eval.ISO)) + { + Write-Verbose -Message 'Srv2016' -Verbose + BitsDownload -Uri $Srv2016tp4Eval -DestinationPath $PSScriptRoot\ISO\Srv2016tp4Eval.ISO + } + if (-not (Test-Path -Path $PSScriptRoot\ISO\HyperV2016tp4Eval.ISO)) + { + Write-Verbose -Message 'hv2016' -Verbose + BitsDownload -Uri $HyperV2016tp4Eval -DestinationPath $PSScriptRoot\ISO\HyperV2016tp4Eval.ISO + } + if (-not (Test-Path -Path $PSScriptRoot\ISO\Srv2012r2Eval.ISO)) + { + Write-Verbose -Message 'srv2012' -Verbose + BitsDownload -Uri $Srv2012r2Eval -DestinationPath $PSScriptRoot\ISO\Srv2012r2Eval.ISO + } + if (-not (Test-Path -Path $PSScriptRoot\ISO\HyperV2012r2Eval.ISO)) + { + Write-Verbose -Message 'hv2012' -Verbose + BitsDownload -Uri $HyperV2012r2Eval -DestinationPath $PSScriptRoot\ISO\HyperV2012r2Eval.ISO + } + } + $BasicExampleContent = { + Write-Warning -Message "You need to edit the configuration in $PSCommandPath and then commend out or delete line 1" + break + # Delete or comment out the above line + Write-Verbose -Message 'This example creates a no frils updated images of various windows versions' -Verbose + Write-Verbose -Message 'Win7 if found will be updated to WMF4' -Verbose + + Import-Module -Name WindowsImageTools -Force + + ## Done use plain text plasswords in production + #$adminCred = New-Object -TypeName System.Management.Automation.PSCredential -ArgumentList ('Administrator', ('P@ssw0rd'|ConvertTo-SecureString -Force -AsPlainText)) + $adminCred = Get-Credential -UserName 'Administrator' -Message 'Local Administrator' + + # Set the values of the VM configuration + $switch = 'Bridge' # Must allready exist + $vLan = 0 # 0 = no vLAN + $IpType = 'DHCP' # DHCP, IPv4, IPv6 + $IPAddress = '192.168.0.101' # Skiped if using DHCP + $SubnetMask = 24 # Skiped if using DHCP + $Gateway = '192.168.0.1' # Skiped if using DHCP + $DnsServer = '192.168.0.1' # Skiped if using DHCP + + $null = Set-UpdateConfig -Path $PSScriptRoot -VmSwitch $switch -vLAN $vLan -IpType $IpType -IpAddress $IPAddress -SubnetMask $SubnetMask -Gateway $Gateway -DnsServer $DnsServer -Verbose + + $Name = 'Win81Evalx86' + $Layout = 'BIOS' + $ISOPath = "$PSScriptRoot\ISO\Win81Evalx86.ISO" + if (Test-Path $ISOPath) + { + Add-UpdateImage -Path $PSScriptRoot -FriendlyName $Name -DiskLayout $Layout -SourcePath $ISOPath -Verbose -AdminCredential $adminCred + } + else + { + Write-Warning -Message "$ISOPath does not exist skipping" + } + + $Name = 'Win81Evalx64' + $Layout = 'UEFI' + $ISOPath = "$PSScriptRoot\ISO\Win81Evalx64.ISO" + if (Test-Path $ISOPath) + { + Add-UpdateImage -Path $PSScriptRoot -FriendlyName $Name -DiskLayout $Layout -SourcePath $ISOPath -Verbose -AdminCredential $adminCred + } + else + { + Write-Warning -Message "$ISOPath does not exist skipping" + } + + $Name = 'Win10Evalx86' + $Layout = 'BIOS' + $ISOPath = "$PSScriptRoot\ISO\Win10Evalx86.ISO" + if (Test-Path $ISOPath) + { + Add-UpdateImage -Path $PSScriptRoot -FriendlyName $Name -DiskLayout $Layout -SourcePath $ISOPath -Verbose -AdminCredential $adminCred + } + else + { + Write-Warning -Message "$ISOPath does not exist skipping" + } + + $Name = 'Win10Evalx64' + $Layout = 'UEFI' + $ISOPath = "$PSScriptRoot\ISO\Win10Evalx64.ISO" + if (Test-Path $ISOPath) + { + Add-UpdateImage -Path $PSScriptRoot -FriendlyName $Name -DiskLayout $Layout -SourcePath $ISOPath -Verbose -AdminCredential $adminCred + } + else + { + Write-Warning -Message "$ISOPath does not exist skipping" + } + + $Name = 'Srv2016tp4Eval' + $Layout = 'UEFI' + $ISOPath = "$PSScriptRoot\ISO\Srv2016tp4Eval.ISO" + if (Test-Path $ISOPath) + { + Add-UpdateImage -Path $PSScriptRoot -FriendlyName "$($Name)1" -DiskLayout $Layout -SourcePath $ISOPath -Verbose -AdminCredential $adminCred + Add-UpdateImage -Path $PSScriptRoot -FriendlyName "$($Name)2" -DiskLayout $Layout -SourcePath $ISOPath -Verbose -Index 2 -AdminCredential $adminCred + Add-UpdateImage -Path $PSScriptRoot -FriendlyName "$($Name)3" -DiskLayout $Layout -SourcePath $ISOPath -Verbose -Index 3 -AdminCredential $adminCred + Add-UpdateImage -Path $PSScriptRoot -FriendlyName "$($Name)4" -DiskLayout $Layout -SourcePath $ISOPath -Verbose -Index 4 -AdminCredential $adminCred + } + else + { + Write-Warning -Message "$ISOPath does not exist skipping" + } + + $Name = 'Srv2012r2Eval' + $Layout = 'UEFI' + $ISOPath = "$PSScriptRoot\ISO\Srv2012r2Eval.ISO" + if (Test-Path $ISOPath) + { + Add-UpdateImage -Path $PSScriptRoot -FriendlyName "$($Name)1" -DiskLayout $Layout -SourcePath $ISOPath -Verbose -AdminCredential $adminCred + Add-UpdateImage -Path $PSScriptRoot -FriendlyName "$($Name)2" -DiskLayout $Layout -SourcePath $ISOPath -Verbose -Index 2 -AdminCredential $adminCred + Add-UpdateImage -Path $PSScriptRoot -FriendlyName "$($Name)3" -DiskLayout $Layout -SourcePath $ISOPath -Verbose -Index 3 -AdminCredential $adminCred + Add-UpdateImage -Path $PSScriptRoot -FriendlyName "$($Name)4" -DiskLayout $Layout -SourcePath $ISOPath -Verbose -Index 4 -AdminCredential $adminCred + } + else + { + Write-Warning -Message "$ISOPath does not exist skipping" + } + + $Name = 'HyperV2016tp4Eval' + $Layout = 'UEFI' + $ISOPath = "$PSScriptRoot\ISO\HyperV2016tp4Eval.ISO" + if (Test-Path $ISOPath) + { + Add-UpdateImage -Path $PSScriptRoot -FriendlyName $Name -DiskLayout $Layout -SourcePath $ISOPath -Verbose -AdminCredential $adminCred + } + else + { + Write-Warning -Message "$ISOPath does not exist skipping" + } + + $Name = 'HyperV2012r2Eval' + $Layout = 'UEFI' + $ISOPath = "$PSScriptRoot\ISO\HyperV2012r2Eval.ISO" + if (Test-Path $ISOPath) + { + Add-UpdateImage -Path $PSScriptRoot -FriendlyName $Name -DiskLayout $Layout -SourcePath $ISOPath -Verbose -AdminCredential $adminCred + } + else + { + Write-Warning -Message "$ISOPath does not exist skipping" + } + + + $Name = 'Win7x64' + $Layout = 'BIOS' + $ISOPath = "$PSScriptRoot\ISO\Win7ent_x64.ISO" + if (Test-Path $ISOPath) + { + Add-UpdateImage -Path $PSScriptRoot -FriendlyName $Name -DiskLayout $Layout -SourcePath $ISOPath -Verbose -AdminCredential $adminCred + Update-WindowsImageWMF -Path $PSScriptRoot -ImageName $Name -Wmf4 -Verbose + } + else + { + Write-Warning -Message "$ISOPath does not exist skipping" + } + + $Name = 'Win7x86' + $Layout = 'BIOS' + $ISOPath = "$PSScriptRoot\ISO\Win7ent_x86.ISO" + if (Test-Path $ISOPath) + { + Add-UpdateImage -Path $PSScriptRoot -FriendlyName $Name -DiskLayout $Layout -SourcePath $ISOPath -Verbose -AdminCredential $adminCred + Update-WindowsImageWMF -Path $PSScriptRoot -ImageName $Name -Wmf4 -Verbose + } + else + { + Write-Warning -Message "$ISOPath does not exist skipping" + } + + Invoke-WindowsImageUpdate -Path $PSScriptRoot -verbose + } + $AdvancedExampleContent = { + Write-Warning -Message "You need to edit the configuration in $PSCommandPath and then commend out or delete line 1" + break + # Delete or comment out the above line + Import-Module -Name WindowsImageTools -Force + + #region config + + ## Dont save admin credentials in production + #$adminCred = New-Object -TypeName System.Management.Automation.PSCredential -ArgumentList ('Administrator', ('P@ssw0rd'|ConvertTo-SecureString -Force -AsPlainText)) + $adminCred = Get-Credential -UserName 'Administrator' -Message 'Local Administrator' + + # Set the values of the VM configuration + $switch = 'Bridge' # Must allready exist + $vLan = 0 # 0 = no vLAN + $IpType = 'DHCP' # DHCP, IPv4, IPv6 + $IPAddress = '192.168.0.101' # Skiped if using DHCP + $SubnetMask = 24 # Skiped if using DHCP + $Gateway = '192.168.0.1' # Skiped if using DHCP + $DnsServer = '192.168.0.1' # Skiped if using DHCP + + # Set path to Server 2012 R2 Eval Iso + $ISOPath = "$PSScriptRoot\ISO\Srv2012r2Eval.ISO" + #endregion + + #region Code + # Update configuration file with supplied values + $null = Set-UpdateConfig -Path $PSScriptRoot -VmSwitch $switch -vLAN $vLan -IpType $IpType -IpAddress $IPAddress -SubnetMask $SubnetMask -Gateway $Gateway -DnsServer $DnsServer -Verbose + + # Add 'Source' image to use for adding features to a patched image + Add-UpdateImage -Path $PSScriptRoot -FriendlyName 'Srv2012r2_Source' -DiskLayout UEFI -SourcePath $ISOPath -AdminCredential $adminCred -Verbose -AddPayloadForRemovedFeature -Index 4 + # Add 'Core' image + Add-UpdateImage -Path $PSScriptRoot -FriendlyName 'Srv2012r2_Core' -DiskLayout UEFI -SourcePath $ISOPath -AdminCredential $adminCred -Verbose -Index 3 + + # update both images to WMF5 Production Preview + Update-WindowsImageWMF -Path $PSScriptRoot -ImageName Srv2012r2_Core -Wmf5pp -verbose + Update-WindowsImageWMF -Path $PSScriptRoot -ImageName Srv2012r2_source -Wmf5pp -verbose + + # Update 'Core' image and remove unused feature payloads + Invoke-WindowsImageUpdate -Path $PSScriptRoot -Verbose -ImageName Srv2012r2_Core -ReduceImageSize + # Update 'Source' and only create WIM + Invoke-WindowsImageUpdate -Path $PSScriptRoot -Verbose -ImageName Srv2012r2_source -output WIM + + # create scedualed task to update images once a week on Wednesday night + # First action solves prompting of nuget updates, and must be in a seporate process. + $action1 = New-ScheduledTaskAction -ID 1 -Execute '%SystemRoot%\System32\WindowsPowerShell\v1.0\powershell.exe' -Argument " -Command `"& {get-packageprovider -name nuget -forcebootstrap }`"" + $action2 = New-ScheduledTaskAction -ID 2 -Execute '%SystemRoot%\System32\WindowsPowerShell\v1.0\powershell.exe' -Argument " -Command `"& {Start-Transcript $env:ALLUSERSPROFILE\WITUpdate.log -Append; import-module WindowsImageTools -erroraction stop; Invoke-WindowsImageUpdate -Path $PSScriptRoot -Verbose -ImageName Srv2012r2_Core -ReduceImageSize ; Invoke-WindowsImageUpdate -Path $PSScriptRoot -Verbose -ImageName Srv2012r2_source -output WIM }`"" + + $Paramaters = @{ + Action = $action1, $action2 + Trigger = New-ScheduledTaskTrigger -Weekly -DaysOfWeek Wednesday -At 11pm + Settings = New-ScheduledTaskSettingsSet + } + $Name = $PSScriptRoot.Replace('\','-').Replace(':','') + $TaskObject = New-ScheduledTask @Paramaters + $null = Register-ScheduledTask -InputObject $TaskObject -User 'nt authority\system' -Verbose -TaskName "Advanced-ImageUpdate-for-$Name" + + #endregion + } + $ConvertExampleContent = { + Import-Module -Name $PSScriptRoot\WindowsImageTools -Force + + # Example of WIM2VHD conversion + + #Initialize-VHDPartition -Path g:\temp\temp1.vhdx -Dynamic -Verbose -DiskLayout BIOS -RecoveryImage -force -Passthru | + # Set-VHDPartition -SourcePath C:\iso\Win7ent_x64.ISO -Index 1 -Confirm:$false -force -Verbose + + #Convert-Wim2VHD -Path g:\temp\test2.vhdx -SourcePath C:\iso\Server2012R2.ISO -DiskLayout UEFI -Dynamic -Index 1 -Size 60GB -Force -Verbose -RecoveryImage + $commonParams = @{ + 'Dynamic' = $true + 'Verbose' = $true + 'Force' = $true + 'Unattend' = (New-UnattendXml -AdminPassword 'LocalP@ssword' -LogonCount 1) + 'filesToInject' = 'g:\temp\inject\pstemp\' + } + + $vhds = @( + @{ + 'SourcePath' = 'C:\iso\server_2016_preview_3.iso' + 'DiskLayout' = 'UEFI' + 'index' = 1 + 'size' = 40Gb + 'Path' = 'G:\temp\2016_CoreStd.vhdx' + }, + @{ + 'SourcePath' = 'C:\iso\server_2016_preview_3.iso' + 'DiskLayout' = 'UEFI' + 'index' = 2 + 'size' = 40Gb + 'Path' = 'G:\temp\2016_GUIStd.vhdx' + }, + @{ + 'SourcePath' = 'C:\iso\server_2016_preview_3.iso' + 'DiskLayout' = 'UEFI' + 'index' = 3 + 'size' = 40Gb + 'Path' = 'G:\temp\2016_CoreDC.vhdx' + }, + @{ + 'SourcePath' = 'C:\iso\server_2016_preview_3.iso' + 'DiskLayout' = 'UEFI' + 'index' = 4 + 'size' = 40Gb + 'Path' = 'G:\temp\2016_GUIDC.vhdx' + }, + @{ + 'SourcePath' = 'C:\iso\Svr_2012_R2.ISO' + 'DiskLayout' = 'UEFI' + 'index' = 1 + 'size' = 40Gb + 'Path' = 'G:\temp\2012r2_CoreStd.vhdx' + }, + @{ + 'SourcePath' = 'C:\iso\Svr_2012_R2.ISO' + 'DiskLayout' = 'UEFI' + 'index' = 2 + 'size' = 40Gb + 'Path' = 'G:\temp\2012r2_GUIStd.vhdx' + }, + @{ + 'SourcePath' = 'C:\iso\Svr_2012_R2.ISO' + 'DiskLayout' = 'UEFI' + 'index' = 3 + 'size' = 40Gb + 'Path' = 'G:\temp\2012r2_CoreDC.vhdx' + }, + @{ + 'SourcePath' = 'C:\iso\Svr_2012_R2.ISO' + 'DiskLayout' = 'UEFI' + 'index' = 4 + 'size' = 40Gb + 'Path' = 'G:\temp\2012r2_GUIDC.vhdx' + }, + @{ + 'SourcePath' = 'C:\iso\Win10ent_x64.ISO' + 'DiskLayout' = 'UEFI' + 'index' = 1 + 'size' = 40GB + 'Path' = 'G:\temp\Win10E_x64_UEFI.vhdx' + }, + @{ + 'SourcePath' = 'C:\iso\Win10ent_x64.ISO' + 'DiskLayout' = 'BIOS' + 'index' = 1 + 'size' = 40GB + 'Path' = 'G:\temp\Win10E_x64_BIOS.vhdx' + }, + @{ + 'SourcePath' = 'C:\ISO\Win10ent_x86.ISO' + 'DiskLayout' = 'BIOS' + 'index' = 1 + 'size' = 40GB + 'Path' = 'G:\temp\Win10E_x86_BIOS.vhdx' + }, + @{ + 'SourcePath' = 'C:\ISO\Win7ent_x64.ISO' + 'DiskLayout' = 'BIOS' + 'index' = 1 + 'size' = 40GB + 'Path' = 'G:\temp\Win7ent_x64_BIOS.vhdx' + }, + @{ + 'SourcePath' = 'C:\ISO\Win7ent_x86.ISO' + 'DiskLayout' = 'BIOS' + 'Index' = 1 + 'size' = 40GB + 'Path' = 'G:\temp\Win7ent_x86_BIOS.vhdx' + } + ) + + foreach ($VhdParms in $vhds) + { + Convert-Wim2VHD @VhdParms @commonParams #-WhatIf + } + } + #endregion + + #region Creat Directories + try + { + $null = New-Item -ItemType Directory -Path $Path -ErrorAction Stop + $null = New-Item -ItemType Directory -Path $Path\UpdatedImageShare -ErrorAction Stop + $null = New-Item -ItemType Directory -Path $Path\BaseImage -ErrorAction Stop + $null = New-Item -ItemType Directory -Path $Path\ISO -ErrorAction Stop + $null = New-Item -ItemType Directory -Path $Path\Resource -ErrorAction Stop + } + catch + { + throw "Error creating Directories in $Path" + } + #endregion + + #region create Files + try + { + $null = Set-UpdateConfig -Path $Path + $null = New-Item -Path $Path -Name BasicUpdateExample.ps1 -ItemType 'file' -Value $BasicExampleContent -Force + $null = New-Item -Path $Path -Name AdvancedUpdateExample.ps1 -ItemType 'file' -Value $AdvancedExampleContent -Force + $null = New-Item -Path $Path -Name DownloadEvalIso.ps1 -ItemType 'file' -Value $DownloadEvalIsoContent -Force + $null = New-Item -Path $Path -Name BasicConvertExample.ps1 -ItemType 'file' -Value $ConvertExampleContent -Force + } + catch + { + throw "trying to create files in $Path" + } + #endregion + + #region Download Modules + try + { + Find-Module -Name PSWindowsUpdate -ErrorAction Stop | Save-Module -Force -Path $Path\Resource -ErrorAction Stop + } + catch + { + Write-Warning -Message 'Unable to download PSWindowsUpdate useing PowerShellGet' + } + #endregion + } + return (Get-Item $Path) +} diff --git a/deployment/dsc/azshcihost/WindowsImageTools/1.0.7/Functions/Set-UpdateConfig.ps1 b/deployment/dsc/azshcihost/WindowsImageTools/1.0.7/Functions/Set-UpdateConfig.ps1 new file mode 100644 index 0000000..882d809 --- /dev/null +++ b/deployment/dsc/azshcihost/WindowsImageTools/1.0.7/Functions/Set-UpdateConfig.ps1 @@ -0,0 +1,254 @@ +function Set-UpdateConfig +{ + <# + .Synopsis + Set the Windows Image Tools Update Config used for creating the temp VM + .DESCRIPTION + Set the config used by Invoke-WitUpdate to build a VM and update Windows Images + .EXAMPLE + Set-WitUpdateConfig -Path C:\WitUpdate -VmSwitch 'VM' -IpType DCHP + Set the temp VM to attach to siwth "VM" and use DCHP for IP addresses + .EXAMPLE + Set-WitUPdateConfig -Path C:\WitUpdate -VmSwitch CorpIntAccess -vLAN 1752 -IpType 'IPv4' -IPAddress '172.17.52.100' -SubnetMask 24 -Gateway '172.17.52.254' -DNS '208.67.222.123' + Setup the temp VM to attache to swithc CorpIntAccess, tag the packets with vLAN id 1752, and set the statis IPv4 Address, mask, gateway and DNS + .INPUTS + System.IO.DirectoryInfo + .OUTPUTS + System.IO.DirectoryInfo + #> + [CmdletBinding(SupportsShouldProcess = $true)] + [OutputType([System.IO.DirectoryInfo])] + Param + ( + # Path to the Windows Image Tools Update Folders (created via New-WitExample) + [Parameter(Mandatory = $true, + ValueFromPipelineByPropertyName = $true)] + [ValidateNotNull()] + [ValidateNotNullOrEmpty()] + [ValidateScript({ + if (Test-Path $_) + { + $true + } + else + { + throw "Path $_ does not exist" + } + })] + [Alias('FullName')] + $Path, + + # Existing VM Switch + [String] + $VmSwitch, + + # vLAN to have the VM tag it's trafic to (0 = No vLAN taging) + [int] + $vLAN, + + # IP Address Type used to set give the Temporary VM internet access DHCP, IPv4, or IPv6 + [ValidateSet('DHCP', 'IPv4', 'Ipv4')] + [String] + $IpType, + + # Static IP IPv4 or IPv6 Address to asign the Temporary VM help description + [ValidateScript({ + $ipObj = [System.Net.IPAddress]::parse($_) + $isValidIP = [System.Net.IPAddress]::tryparse([string]$_, [ref]$ipObj) + if ($isValidIP) + { + $true + } + else + { + throw 'IpAddress must be a valid IPv4 or IPv6 address' + } + })] + [String] + $IpAddress, + + # IP SubnetMask Ex. + [int] + $SubnetMask, + + # Static Gateway + [ValidateScript({ + $ipObj = [System.Net.IPAddress]::parse($_) + $isValidIP = [System.Net.IPAddress]::tryparse([string]$_, [ref]$ipObj) + if ($isValidIP) + { + $true + } + else + { + throw 'Gateway must be a valid IPv4 or IPv6 address' + } + })] + [String] + $Gateway, + + # Static DNS Server + [ValidateScript({ + $ipObj = [System.Net.IPAddress]::parse($_) + $isValidIP = [System.Net.IPAddress]::tryparse([string]$_, [ref]$ipObj) + if ($isValidIP) + { + $true + } + else + { + throw 'DNSServer must be a valid IPv4 or IPv6 address' + } + })] + [String] + $DnsServer + + + + ) + + if ($pscmdlet.ShouldProcess("$Path", 'Set the Windows Image Tools Update Configuration')) + { + $ConfigFilePath = $Path + $ParentPath = (Get-Item $Path).Parent.FullName + if (Test-Path -Path "$Path" -PathType Container) + { + $ConfigFilePath = "$Path\Config.xml" + $ParentPath = $Path + } + try + { + $ConfigData = Import-Clixml -Path $ConfigFilePath -ErrorAction Stop + } + catch + { + Write-Warning -Message "Unable to read Windows Image Tools Update Cofniguration from $ConfigFilePath, creating a new file" + $ConfigData = @{ + VmSwitch = 'vmswitch' + vLan = 0 + IpAddress = '192.168.0.100' + SubnetMask = 24 + Gateway = '192.168.0.1' + DnsServer = '192.168.0.1' + IpType = 'DHCP' + } + } + # validate data structure incase useing older or malformed xml + If (-not ($ConfigData.ContainsKey('VmSwitch'))) + { + $ConfigData.add('VmSwitch','vmswitch') + } + If (-not ($ConfigData.ContainsKey('vLan'))) + { + $ConfigData.add('vLan','0') + } + If (-not ($ConfigData.ContainsKey('IpType'))) + { + $ConfigData.add('IpType','DHCP') + } + If (-not ($ConfigData.ContainsKey('IpAddress'))) + { + $ConfigData.add('IpAddress','192.168.0.100') + } + If (-not ($ConfigData.ContainsKey('SubnetMask'))) + { + $ConfigData.add('SubnetMask','24') + } + If (-not ($ConfigData.ContainsKey('Gateway'))) + { + $ConfigData.add('Gateway','192.168.0.1') + } + If (-not ($ConfigData.ContainsKey('DnsServer'))) + { + $ConfigData.add('DnsServer','192.168.0.1') + } + + # update values + if ($VmSwitch) + { + $ConfigData.VmSwitch = $VmSwitch + } + if ($vLAN) + { + $ConfigData.vLan = $vLAN + } + if ($IpType) + { + $ConfigData.IpType = $IpType + } + if ($IpAddress) + { + $ConfigData.IpAddress = $IpAddress + } + if ($SubnetMask) + { + $ConfigData.SubnetMask = $SubnetMask + } + if ($Gateway) + { + $ConfigData.Gateway = $Gateway + } + if ($DnsServer) + { + $ConfigData.DnsServer = $DnsServer + } + + Write-Verbose -Message 'New Configuration' + Write-Verbose -Message ($ConfigData | Out-String) + + try + { + $ConfigData | Export-Clixml -Path $ConfigFilePath -ErrorAction Stop + } + catch + { + Throw "Failed to write $ConfigFilePath. $($_.Exception.Message)" + } + return (Get-Item $ParentPath) + } +} + +function Get-UpdateConfig +{ + <# + .Synopsis + Get the Windows Image Tools Update Config used for creating the temp VM + .DESCRIPTION + This command will Get the config used by Invoke-WindowsImageUpdate to build a VM and update Windows Images + .EXAMPLE + Set-WitUpdateConfig -Path C:\WitUpdate -VmSwitch 'VM' -IpType DCHP + Set the temp VM to attach to siwth "VM" and use DCHP for IP addresses + .EXAMPLE + Set-WitUPdateConfig -Path C:\WitUpdate -VmSwitch CorpIntAccess -vLAN 1752 -IpType 'IPv4' -IPAddress '172.17.52.100' -SubnetMask 24 -Gateway '172.17.52.254' -DNS '208.67.222.123' + Setup the temp VM to attache to swithc CorpIntAccess, tag the packets with vLAN id 1752, and set the statis IPv4 Address, mask, gateway and DNS + .INPUTS + System.IO.DirectoryInfo + .OUTPUTS + System.IO.DirectoryInfo + #> + [CmdletBinding()] + [Alias()] + [OutputType([Hashtable])] + Param + ( + # Path to the Windows Image Tools Update Folders (created via New-WitExample) + [Parameter(Mandatory = $true, + ValueFromPipelineByPropertyName = $true)] + [ValidateNotNull()] + [ValidateNotNullOrEmpty()] + [ValidateScript({ + if (Test-Path $_) + { + $true + } + else + { + throw "Path $_ does not exist" + } + })] + [Alias('FullName')] + $Path + ) + + return (Import-Clixml -Path "$Path\config.xml") +} diff --git a/deployment/dsc/azshcihost/WindowsImageTools/1.0.7/Functions/Set-VHDPartition.ps1 b/deployment/dsc/azshcihost/WindowsImageTools/1.0.7/Functions/Set-VHDPartition.ps1 new file mode 100644 index 0000000..d9ef813 --- /dev/null +++ b/deployment/dsc/azshcihost/WindowsImageTools/1.0.7/Functions/Set-VHDPartition.ps1 @@ -0,0 +1,530 @@ +function Set-VHDPartition +{ + <# + .Synopsis + Sets the content of a VHD(X) using a source WIM or ISO + .DESCRIPTION + This command will copy the content of the SourcePath ISO or WIM and populate the + partitions found in the VHD(X) You must supply the path to the VHD(X) file and a + valid WIM/ISO. You should also include the index number for the Windows Edition + to install. If the recovery partitions are present the source WIM will be copied + to the recovery partition. Optionally, you can also specify an XML file to be + inserted into the OS partition as unattend.xml, any Drivers, WindowsUpdate (MSU) + or Optional Features you want installed. And any additional files to add. + CAUTION: This command will replace the content partitions. + .EXAMPLE + PS C:\> Set-VHDPartition -Path D:\vhd\demo3.vhdx -SourcePath D:\wim\Win2012R2-Install.wim -Index 1 + .EXAMPLE + PS C:\> Set-VHDPartition -Path D:\vhd\demo3.vhdx -SourcePath D:\wim\Win2012R2-Install.wim -Index 1 -Confirm:$false -force -Verbose + #> + [CmdletBinding(SupportsShouldProcess = $true, + PositionalBinding = $true, + ConfirmImpact = 'High')] + Param + ( + # Path to VHDX + [parameter(Position = 0,Mandatory = $true, + HelpMessage = 'Enter the path to the VHDX file', + ValueFromPipeline = $true, + ValueFromPipelineByPropertyName = $true)] + [Alias('FullName','pspath','ImagePath')] + [ValidateScript({ + Test-Path -Path (Get-FullFilePath -Path $_) + })] + [string]$Path, + + # Path to WIM or ISO used to populate VHDX + [parameter(Position = 1,Mandatory = $true, + HelpMessage = 'Enter the path to the WIM/ISO file')] + [ValidateScript({ + Test-Path -Path (Get-FullFilePath -Path $_ ) + })] + [string]$SourcePath, + + # Index of image inside of WIM (Default 1) + [int]$Index = 1, + + # Path to file to copy inside of VHD(X) as C:\unattent.xml + [ValidateScript({ + if ($_) + { + Test-Path -Path $_ + } + else + { + $true + } + })] + [string]$Unattend, + + # Native Boot does not have the boot code inside the VHD(x) it must exist on the physical disk. + [switch]$NativeBoot, + + # Add payload for all removed features + [switch]$AddPayloadForRemovedFeature, + + # Feature to turn on (in DISM format) + [ValidateNotNullOrEmpty()] + [string[]]$Feature, + + # Feature to remove (in DISM format) + [ValidateNotNullOrEmpty()] + [string[]]$RemoveFeature, + + # Feature Source path. If not provided, all ISO and WIM images in $sourcePath searched + [ValidateNotNullOrEmpty()] + [ValidateScript({ + Test-Path -Path $(Resolve-Path $_) + })] + [string]$FeatureSource, + + # Feature Source index. If the source is a .wim provide an index Default =1 + [int]$FeatureSourceIndex = 1, + + # Path to drivers to inject + [ValidateNotNullOrEmpty()] + [ValidateScript({ + Test-Path -Path $(Resolve-Path $_) + })] + [string[]]$Driver, + + # Path of packages to install via DSIM + [ValidateNotNullOrEmpty()] + [ValidateScript({ + Test-Path -Path $(Resolve-Path $_) + })] + [string[]]$Package, + + # Files/Folders to copy to root of Winodws Drive (to place files in directories mimic the direcotry structure off of C:\) + [ValidateNotNullOrEmpty()] + [ValidateScript({ + foreach ($Path in $_) + { + Test-Path -Path $(Resolve-Path $Path) + } + })] + [string[]]$filesToInject, + + # Bypass the warning and about lost data + [switch]$Force + ) + + + Process { + $Path = $Path | Get-FullFilePath + $SourcePath = $SourcePath | Get-FullFilePath + + $VhdxFileName = Split-Path -Leaf -Path $Path + + if ($pscmdlet.ShouldProcess("[$($MyInvocation.MyCommand)] : Overwrite partitions inside [$Path] with content of [$SourcePath]", + "Overwrite partitions inside [$Path] with contentce of [$SourcePath]? ", + 'Overwrite WARNING!')) + { + if($Force -Or $pscmdlet.ShouldContinue('Are you sure? Any existin data will be lost!', 'Warning')) + { + $ParametersToPass = @{} + foreach ($key in ('Whatif', 'Verbose', 'Debug')) + { + if ($PSBoundParameters.ContainsKey($key)) + { + $ParametersToPass[$key] = $PSBoundParameters[$key] + } + } + #region ISO detection + # If we're using an ISO, mount it and get the path to the WIM file. + if (([IO.FileInfo]$SourcePath).Extension -ilike '.ISO') + { + # If the ISO isn't local, copy it down so we don't have to worry about resource contention + # or about network latency. + if (Test-IsNetworkLocation -Path $SourcePath) + { + Write-Verbose -Message "[$($MyInvocation.MyCommand)] : Copying ISO [$(Split-Path -Path $SourcePath -Leaf)] to [$env:temp]" + $null = & "$env:windir\system32\robocopy.exe" $(Split-Path -Path $SourcePath -Parent) $env:temp $(Split-Path -Path $SourcePath -Leaf) + $SourcePath = "$($env:temp)\$(Split-Path -Path $SourcePath -Leaf)" + + $tempSource = $SourcePath + } + + $isoPath = (Resolve-Path $SourcePath).Path + + Write-Verbose -Message "[$($MyInvocation.MyCommand)] : Opening ISO [$(Split-Path -Path $isoPath -Leaf)]" + $openIso = Mount-DiskImage -ImagePath $isoPath -StorageType ISO -PassThru + # Workarround for new drive letters in script modules + $null = Get-PSDrive + # Refresh the DiskImage object so we can get the real information about it. I assume this is a bug. + $openIso = Get-DiskImage -ImagePath $isoPath + $driveLetter = ($openIso | Get-Volume).DriveLetter + + $SourcePath = "$($driveLetter):\sources\install.wim" + + # Check to see if there's a WIM file we can muck about with. + Write-Verbose -Message "[$($MyInvocation.MyCommand)] : Looking for $($SourcePath)" + if (!(Test-Path $SourcePath)) + { + throw 'The specified ISO does not appear to be valid Windows installation media.' + } + } + #endregion + + #region WIM on network + # Check to see if the WIM is local, or on a network location. If the latter, copy it locally. + if (Test-IsNetworkLocation -Path $SourcePath) + { + Write-Verbose -Message "[$($MyInvocation.MyCommand)] : Copying WIM $(Split-Path -Path $SourcePath -Leaf) to [$env:temp]" + $null = & "$env:windir\system32\robocopy.exe" $(Split-Path -Path $SourcePath -Parent) $env:temp $(Split-Path -Path $SourcePath -Leaf) + $SourcePath = "$($TempDirectory)\$(Split-Path -Path $SourcePath -Leaf)" + + $tempSource = $SourcePath + } + $SourcePath = (Resolve-Path $SourcePath).Path + #endregion + + #region mount the VHDX file + try + { + Write-Verbose -Message "[$($MyInvocation.MyCommand)] [$VhdxFileName] : Mounting disk image [$Path]" + $disk = Mount-DiskImage -ImagePath $Path -PassThru | + Get-DiskImage | + Get-Disk + } + catch + { + throw $_.Exception.Message + } + #endregion + + try + { + Write-Verbose -Message "[$($MyInvocation.MyCommand)] [$VhdxFileName] : Munted as disknumber [$($disk.Number)]" + + #region Assign Drive Letters + foreach ($partition in (Get-Partition -DiskNumber $disk.Number | + Where-Object -Property Type -NE -Value Reserved)) + { + $partition | Add-PartitionAccessPath -AssignDriveLetter -ErrorAction Stop + } + # Workarround for new drive letters in script modules + $null = Get-PSDrive + Write-Verbose -Message "[$($MyInvocation.MyCommand)] [$VhdxFileName] : Partition Table" + Write-Verbose -Message (Get-Partition -DiskNumber $disk.Number | + Select-Object -Property PartitionNumber, DriveLetter, Size, Type| + Out-String) + #endregion + + #region get partitions + $RecoveryToolsPartition = Get-Partition -DiskNumber $disk.Number | + Where-Object -Property Type -EQ -Value Recovery | + Select-Object -First 1 + if ((Get-Partition -DiskNumber $disk.Number | + Where-Object -Property Type -EQ -Value Recovery).count -gt 1) + { + $RecoveryImagePartition = Get-Partition -DiskNumber $disk.Number | + Where-Object -Property Type -EQ -Value Recovery | + Select-Object -Last 1 + } + $WindowsPartition = Get-Partition -DiskNumber $disk.Number | + Where-Object -Property Type -EQ -Value Basic| + Select-Object -First 1 + $SystemPartition = Get-Partition -DiskNumber $disk.Number | + Where-Object -Property Type -EQ -Value System| + Select-Object -First 1 + + $DiskLayout = 'UEFI' + if (-not ($WindowsPartition -and $SystemPartition)) + { + $WindowsPartition = Get-Partition -DiskNumber $disk.Number | + Where-Object -Property Type -EQ -Value IFS| + Select-Object -First 1 + $SystemPartition = $WindowsPartition + $DiskLayout = 'BIOS' + } + if (Get-Partition -DiskNumber $disk.Number | + Where-Object -Property Type -EQ -Value FAT32 ) + { + $DiskLayout = 'WindowsToGo' + } + + #endregion + + #region Recovery Image + if ($RecoveryImagePartition) + { + #copy the WIM to recovery image partition as Install.wim + $recoverfolder = Join-Path -Path "$($RecoveryImagePartition.DriveLetter):" -ChildPath 'Recovery' + $null = mkdir -Path $recoverfolder + $recoveryPath = Join-Path -Path $recoverfolder -ChildPath 'install.wim' + Write-Verbose -Message "[$($MyInvocation.MyCommand)] [$VhdxFileName] Recovery Image Partition [$($RecoveryImagePartition.PartitionNumber)] : copying [$SourcePath] to [$recoveryPath]" + Copy-Item -Path $SourcePath -Destination $recoveryPath -ErrorAction Stop + } # end if Recovery + #endregion + + #region Windows partition + if ($WindowsPartition) + { + $WinPath = Join-Path -Path "$($WindowsPartition.DriveLetter):" -ChildPath '\' + $windir = Join-Path -Path $WinPath -ChildPath Windows + Write-Verbose -Message "[$($MyInvocation.MyCommand)] [$VhdxFileName] Windows Partition [$($WindowsPartition.partitionNumber)] : Applying image from [$SourcePath] to [$WinPath] using Index [$Index]" + $null = Expand-WindowsImage -ImagePath $SourcePath -Index $Index -ApplyPath $WinPath -ErrorAction Stop + + #region Modify the OS with Drivers, Active Features and Packages + if ($Driver) + { + Write-Verbose -Message "[$($MyInvocation.MyCommand)] [$VhdxFileName] : Adding Windows Drivers to the Image" + + $Driver | ForEach-Object -Process + { + Write-Verbose -Message "[$($MyInvocation.MyCommand)] [$VhdxFileName] : Driver path: [$PSItem]" + $Dism = Add-WindowsDriver -Path $WinPath -Recurse -Driver $PSItem + } + } + if ($filesToInject) + { + foreach ($filePath in $filesToInject) + { + Write-Verbose -Message "[$($MyInvocation.MyCommand)] [$VhdxFileName] Windows Partition [$($WindowsPartition.partitionNumber)] : Adding files from $filePath" + $recurse = $false + if (Test-Path $filePath -PathType Container) + { + $recurse = $true + } + Copy-Item -Path $filePath -Destination $WinPath -Recurse:$recurse + } + } + + + if ($Unattend) + { + try + { + Write-Verbose -Message "[$($MyInvocation.MyCommand)] [$VhdxFileName] Windows Partition [$($WindowsPartition.partitionNumber)] : Adding Unattend.xml ($Unattend)" + Copy-Item $Unattend -Destination "$WinPath\unattend.xml" + } + catch + { + Write-Error -Message "[$($MyInvocation.MyCommand)] [$VhdxFileName] : Error Installing Windows Feature " + throw $_.Exception.Message + } + } + if ($AddPayloadForRemovedFeature) + { + $Feature = $Feature + (Get-WindowsOptionalFeature -Path $WinPath | Where-Object -Property state -EQ -Value 'DisabledWithPayloadRemoved' ).FeatureName + } + + If ($Feature) + { + try + { + Write-Verbose -Message "[$($MyInvocation.MyCommand)] [$VhdxFileName] : Installing Windows Feature(s) : Colecting posible source paths" + $FeatureSourcePath = @() + $MountFolderList = @() + if ($FeatureSource) + { + Write-Verbose -Message "[$($MyInvocation.MyCommand)] [$VhdxFileName] : Installing Windows Feature(s) : Source Path provided [$FeatureSource]" + if (($FeatureSource | + Resolve-Path | + Get-Item ).PSIsContainer -eq $true ) + { + Write-Verbose -Message "[$($MyInvocation.MyCommand)] [$VhdxFileName] : Installing Windows Feature(s) : Source Path [$FeatureSource] in folder" + $FeatureSourcePath += $FeatureSource + } + elseif (($FeatureSource | + Resolve-Path | + Get-Item ).extension -like '.wim') + { + #$FeatureSourcePath += Convert-Path $FeatureSource + $MountFolder = [System.IO.Directory]::CreateDirectory((Join-Path -Path $env:temp -ChildPath ([System.IO.Path]::GetRandomFileName().split('.')[0]))) + $MountFolderList += $MountFolder.FullName + Write-Verbose -Message "[$($MyInvocation.MyCommand)] [$VhdxFileName] : Installing Windows Feature(s) : Mounting Source [$FeatureSource] Index [$FeatureSourceIndex]" + $null = Mount-WindowsImage -ImagePath $FeatureSource -Index $FeatureSourceIndex -Path $MountFolder.FullName -ReadOnly + $FeatureSourcePath += Join-Path -Path $MountFolder.FullName -ChildPath 'Windows\WinSxS' + } + else + { + Write-Warning -Message "$FeatureSource is not a .wim or folder" + } + } + else + { + if ($driveLetter) #ISO + { + $FeatureSourcePath += Join-Path -Path "$($driveLetter):" -ChildPath 'sources\sxs' + } + + $images = Get-WindowsImage -ImagePath $SourcePath + + foreach ($image in $images) + { + #$image | fl * + $MountFolder = [System.IO.Directory]::CreateDirectory((Join-Path -Path $env:temp -ChildPath ([System.IO.Path]::GetRandomFileName().split('.')[0]))) + $MountFolderList += $MountFolder.FullName + Write-Verbose -Message "[$($MyInvocation.MyCommand)] [$VhdxFileName] : Installing Windows Feature(s) : Mounting Source $($image.ImageIndex) $($image.ImageName)" + $null = Mount-WindowsImage -ImagePath $SourcePath -Index $image.ImageIndex -Path $MountFolder.FullName -ReadOnly + $FeatureSourcePath += Join-Path -Path $MountFolder.FullName -ChildPath 'Windows\WinSxS' + } + } #end if FeatureSource + Write-Verbose -Message "[$($MyInvocation.MyCommand)] [$VhdxFileName] : Installing Windows Feature(s) [$Feature] to the Image : Search Source Path [$FeatureSourcePath]" + $null = Enable-WindowsOptionalFeature -Path $WinPath -All -FeatureName $Feature -Source $FeatureSourcePath + } + catch + { + Write-Error -Message "[$($MyInvocation.MyCommand)] [$VhdxFileName] : Error Installing Windows Feature " + throw $_.Exception.Message + } + finally + { + foreach ($MountFolder in $MountFolderList) + { + $null = Dismount-WindowsImage -Path $MountFolder -Discard + } + } + } + + if ($Package) + { + Write-Verbose -Message "[$($MyInvocation.MyCommand)] [$VhdxFileName] : Adding Windows Packages to the Image" + + $Package | ForEach-Object -Process { + Write-Verbose -Message "[$($MyInvocation.MyCommand)] [$VhdxFileName] : Package path: [$PSItem]" + $Dism = Add-WindowsPackage -Path $WinPath -PackagePath $PSItem + } + } + if ($RemoveFeature) + { + Write-Verbose -Message "[$($MyInvocation.MyCommand)] [$VhdxFileName] : Removing Windows Features from the Image" + + $Package | ForEach-Object -Process { + Write-Verbose -Message "[$($MyInvocation.MyCommand)] [$VhdxFileName] : Package path: [$PSItem]" + try + { + $null = Disable-WindowsOptionalFeature -Path $WinPath -All -FeatureName $Feature + } + catch + { + Write-Error -Message "[$($MyInvocation.MyCommand)] [$VhdxFileName] : Error Removeing Windows Feature " + throw $_.Exception.Message + } + } + } + #endregion + } + else + { + throw 'Unable to find OS partition' + } + #endregion + + #region System partition + if ($SystemPartition -and (-not ($NativeBoot))) + { + $systemDrive = "$($SystemPartition.driveletter):" + + + $bcdBootArgs = @( + "$($WinPath)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. + ) + + #if ($UEFICapable) { + Write-Verbose -Message "[$($MyInvocation.MyCommand)] [$VhdxFileName] : Disk Layout [$DiskLayout]" + switch ($DiskLayout) + { + 'UEFI' + { + $bcdBootArgs += '/f UEFI' # Specifies the firmware type of the target system partition + } + 'BIOS' + { + $bcdBootArgs += '/f BIOS' # Specifies the firmware type of the target system partition + } + + 'WindowsToGo' + { + # Create entries for both UEFI and BIOS if possible + if (Test-Path -Path "$($windowsDrive)\Windows\boot\EFI\bootmgfw.efi") + { + $bcdBootArgs += '/f ALL' + } + } + } + Write-Verbose -Message "[$($MyInvocation.MyCommand)] [$VhdxFileName] System Partition [$($SystemPartition.partitionNumber)] : Running [$windir\System32\bcdboot.exe] -> $bcdBootArgs" + Run-Executable -Executable "$windir\System32\bcdboot.exe" -Arguments $bcdBootArgs @ParametersToPass + + # The following is added to mitigate the VMM diff disk handling + # We're going to change from MBRBootOption to LocateBootOption. + if ($DiskLayout -eq 'BIOS') + { + Write-Verbose -Message "[$($MyInvocation.MyCommand)] [$VhdxFileName] System Partition [$($SystemPartition.partitionNumber)] : Fixing the Device ID in the BCD store on [$($VHDFormat)]" + Run-Executable -Executable 'BCDEDIT.EXE' -Arguments ( + "/store $($WinPath)boot\bcd", + "/set `{bootmgr`} device locate" + ) + Run-Executable -Executable 'BCDEDIT.EXE' -Arguments ( + "/store $($WinPath)boot\bcd", + "/set `{default`} device locate" + ) + Run-Executable -Executable 'BCDEDIT.EXE' -Arguments ( + "/store $($WinPath)boot\bcd", + "/set `{default`} osdevice locate" + ) + } + } + #endregion + + #region Recovery Tools + if ($RecoveryToolsPartition) + { + $recoverfolder = Join-Path -Path "$($RecoveryToolsPartition.DriveLetter):" -ChildPath 'Recovery' + Write-Verbose -Message "[$($MyInvocation.MyCommand)] [$VhdxFileName] Recovery Tools Partition [$($RecoveryToolsPartition.partitionNumber)] : [$cmd]" + Start-Process -NoNewWindow -Wait -FilePath "$windir\System32\reagentc.exe" -ArgumentList "/setosimage /path $recoverfolder /index $Index /target $windir" -NoNewWindow + + Write-Verbose -Message "[$($MyInvocation.MyCommand)] [$VhdxFileName] Recovery Tools Partition [$($RecoveryToolsPartition.partitionNumber)] : Creating Recovery\WindowsRE folder [$($RecoveryToolsPartition.driveletter):\Recovery\WindowsRE]" + $repath = mkdir -Path "$($RecoveryToolsPartition.driveletter):\Recovery\WindowsRE" + Write-Verbose -Message "[$($MyInvocation.MyCommand)] [$VhdxFileName] Recovery Tools Partition [$($RecoveryToolsPartition.partitionNumber)] : Copying [$($WindowsPartition.DriveLetter):\Windows\System32\recovery\winre.wim] to [$($repath.fullname)]" + #the winre.wim file is hidden + Get-ChildItem -Path "$($WindowsPartition.DriveLetter):\Windows\System32\recovery\winre.wim" -Hidden | + Copy-Item -Destination $repath.FullName + } + #endregion + } + catch + { + Write-Error -Message "[$($MyInvocation.MyCommand)] [$VhdxFileName] : Error setting partition content " + throw $_.Exception.Message + } + finally + { + Write-Verbose -Message "[$($MyInvocation.MyCommand)] [$VhdxFileName] : Removing Drive letters" + Get-Partition -DiskNumber $disk.number | + Where-Object -FilterScript { + $_.driveletter + } | + Where-Object -Property Type -NE -Value 'Basic' | + Where-Object -Property Type -NE -Value 'IFS' | + ForEach-Object -Process { + $dl = "$($_.DriveLetter):" + $_ | + Remove-PartitionAccessPath -AccessPath $dl + } + #dismount + Write-Verbose -Message "[$($MyInvocation.MyCommand)] [$VhdxFileName] : Dismounting" + $null = Dismount-DiskImage -ImagePath $Path + if ($isoPath -and (Get-DiskImage $isoPath).Attached) + { + $null = Dismount-DiskImage -ImagePath $isoPath + } + Write-Verbose -Message "[$($MyInvocation.MyCommand)] [$VhdxFileName] : Finished" + } + } + else + { + Write-Warning -Message 'Process aborted by user' + } + } + else + { + # Write-Warning 'Process aborted by user' + } + + } +} diff --git a/deployment/dsc/azshcihost/WindowsImageTools/1.0.7/Functions/Update-WindowsImageWMF.ps1 b/deployment/dsc/azshcihost/WindowsImageTools/1.0.7/Functions/Update-WindowsImageWMF.ps1 new file mode 100644 index 0000000..385db44 --- /dev/null +++ b/deployment/dsc/azshcihost/WindowsImageTools/1.0.7/Functions/Update-WindowsImageWMF.ps1 @@ -0,0 +1,338 @@ +function Update-WindowsImageWMF +{ + <# + .Synopsis + Updates WMF to 4.0, 5.0 Production Preview or 5.0 (and .NET to 4.6) in a Windows Update Image + .DESCRIPTION + This Command downloads WMF 4.0, 5.0PP or 5.0 (Production Preview) and .NET 4.6 offline installer + Creates a temp VM and updates .NET if needed and WMF + .EXAMPLE + Update-UpdateImageWMF -Path C:\WITExample + Updates every Image in c:\WITExample\BaseImages + .EXAMPLE + Update-UpdateImageWMF -Path C:\WitExample -Name Server2012R2Core + Updates only C:\WitExample\BaseImages\Server2012R2Core_Base.vhdx + #> + [CmdletBinding(SupportsShouldProcess)] + #[OutputType([String])] + Param + ( + # Path to the Windows Image Tools Update Folders (created via New-WindowsImageToolsExample) + [Parameter(Mandatory, + ValueFromPipelineByPropertyName)] + [ValidateNotNull()] + [ValidateNotNullOrEmpty()] + [ValidateScript({ + if (Test-Path $_) + { + $true + } + else + { + throw "Path $_ does not exist" + } + })] + [Alias('FullName')] + $Path, + + # Name of the Image to update + [Parameter(Mandatory)] + [ValidateNotNull()] + [ValidateNotNullOrEmpty()] + [Alias('FriendlyName')] + [string[]] + $ImageName, + + # Use WMF 4 instead of the default WMF 5 + [switch] + $Wmf4, + + # Use WMF5 Production Preview instead of the default WMF 5 (overrides -vmf4) + [switch] + $Wmf5pp + + ) + + foreach ($image in $ImageName) + { + $parentVHD = "$Path\BaseImage\$($image)_Base.vhdx" + $target = "$Path\BaseImage\$($image)_Update.vhdx" + + if ($pscmdlet.ShouldProcess("$parentVHD", 'Update WMF in Windows Image Tools Update Image')) + { + $ParametersToPass = @{} + foreach ($key in ('Whatif', 'Verbose', 'Debug')) + { + if ($PSBoundParameters.ContainsKey($key)) + { + $ParametersToPass[$key] = $PSBoundParameters[$key] + } + } + + Write-Verbose -Message "[$($MyInvocation.MyCommand)] : Creating $target from $parentVHD" + $null = New-VHD -Path $target -ParentPath $parentVHD + + #region Validate Input + try + { + $null = Test-Path -Path "$Path\BaseImage" -ErrorAction Stop + $null = Test-Path -Path "$Path\Resource" -ErrorAction Stop + } + catch + { + Throw "$Path missing required folder structure use New-WindowsImagetoolsExample to create example" + } + + if (-not(Test-Path -Path "$Path\BaseImage\$($ImageName)_Base.vhdx")) + { + Throw "BaseImage for $ImageName does not exists. Use Add-UpdateImage first" + } + #endregion + + #region Update Resource Folder + ## download WMF + $wmfPath = "$Path\Resource\WMF\5" + $wmfDownloadUrl = 'http://aka.ms/wmf5latest' + + if ($Wmf4) + { + $wmfPath = "$Path\Resource\WMF\4" + $wmfDownloadUrl = 'http://www.microsoft.com/en-us/download/details.aspx?id=40855' + } + if ($Wmf5pp) + { + $wmfPath = "$Path\Resource\WMF\5pp" + $wmfDownloadUrl = 'https://www.microsoft.com/en-us/download/details.aspx?id=48729' + } + try + { + if (-not (Test-Path -Path $wmfPath)) + { + $null = mkdir -Path $wmfPath + } + Write-Verbose -Message "[$($MyInvocation.MyCommand)] : Checking for the latest WMF in $wmfPath" + $confirmationPage = 'http://www.microsoft.com/en-us/download/' + $((Invoke-WebRequest -Uri $wmfDownloadUrl -UseBasicParsing).links | + Where-Object -Property Class -EQ -Value 'mscom-link download-button dl' | + ForEach-Object -MemberName href) + $directURLs = (Invoke-WebRequest -Uri $confirmationPage -UseBasicParsing).Links | + Where-Object -Property Class -EQ -Value 'mscom-link' | + Where-Object -Property href -Like -Value '*.msu' | + ForEach-Object -MemberName href + foreach ($directURL in $directURLs) + { + $filename = $directURL -split '/' | Select-Object -Last 1 + if (-not (Test-Path -Path "$wmfPath\$filename" )) + { + Write-Warning -Message "[$($MyInvocation.MyCommand)] : Checking for the latest WMF : $filename Missing, Downloading" + $null = Invoke-WebRequest -Uri $directURL -OutFile "$wmfPath\$filename" + } + else + { + Write-Verbose -Message "[$($MyInvocation.MyCommand)] : Checking for the latest WMF : $wmfPath\$filename : Found" + } + } + } + catch + { + if (-not (Test-Path -Path "$wmfPath\*.msu")) + { + throw "Unable to downlaod WMF to $wmfPath. please download WMF manualy and place in $wmfPath " + } + } + + + ## download .NET 4.6 + try + { + if (-not (Test-Path -Path $Path\Resource\dotNET)) + { + mkdir -Path $Path\Resource\dotNET + } + Write-Verbose -Message "[$($MyInvocation.MyCommand)] : Checking for .NET 4.6" + $directURL = 'https://download.microsoft.com/download/C/3/A/C3A5200B-D33C-47E9-9D70-2F7C65DAAD94/NDP46-KB3045557-x86-x64-AllOS-ENU.exe' + $filename = 'dotNet4-6.exe' + if (-not (Test-Path -Path "$Path\Resource\dotNET\$filename" )) + { + Write-Warning -Message "[$($MyInvocation.MyCommand)] : Checking for .NET 4.6 : Missing : Downloading" + $null = Invoke-WebRequest -Uri $directURL -OutFile "$Path\Resource\dotNET\$filename" + } + } + catch + { + if (-not (Test-Path -Path "$Path\Resource\dotNET\$filename")) + { + throw "Unable to downlaod .net 4.6 to $Path\Resource\dotNET\$filename. please download .net 4.6 manualy " + } + } + #endregion + + #region Install .NET + $dotNetInstallAtStartup = { + Start-Transcript -Path $PSScriptRoot\AtStartup.log -Append + $currentDotNetVersionv = (Get-ChildItem -Path 'HKLM:\SOFTWARE\Microsoft\NET Framework Setup\NDP' -Recurse | + Get-ItemProperty -Name Version, Release -EA 0 | + Where-Object -FilterScript { + $_.PSChildName -match '^(?!S)\p{L}' + } | + Sort-Object -Property version -Descending | + Select-Object -First 1 ).version + if ($currentDotNetVersionv -lt 4.6) + { + if (-not (Test-Path -Path c:\PsTemp\dotNET\attempt.txt)) + { + Get-Date | Out-File -FilePath c:\PsTemp\dotNET\attempt.txt + Write-Verbose -Message '.Net 4.6 : Installing' -Verbose + Start-Process -Verb runas -Wait -FilePath 'C:\PsTemp\dotNET\dotNet4-6.exe' -ArgumentList '/q', '/norestart', '/log c:\PsTemp\dotNet\dotNetLog.htm' + } + + else + { + Write-Error -Message '.Net 4.6 : install attempted but failed!' + Start-Sleep -Seconds 30 + # Stop-Computer does not have -force in 2008/win7 WMF2 + if ((Get-Command Stop-Computer -Syntax) -like '*[force]*') + { + Stop-Computer -Verbose -Force + } + else + { + & "$env:windir\system32\shutdown.exe" /s /t 0 /f + } + Stop-Transcript + } + } + else + { + Get-Date | Out-File -FilePath c:\PsTemp\dotNET\Verified.txt + Write-Verbose -Message '.Net 4.6 : detected shuting down' -Verbose + # Stop-Computer does not have -force in 2008/win7 WMF2 + if ((Get-Command Stop-Computer -Syntax) -like '*[force]*') + { + Stop-Computer -Verbose -Force + } + else + { + & "$env:windir\system32\shutdown.exe" /s /t 0 /f + } + Stop-Transcript + } + Start-Sleep -Seconds 30 + Write-Verbose -Message 'Rebooting computer' -Verbose + # Restart-Computer does not have -force in 2008/win7 WMF2 + if ((Get-Command Restart-Computer -Syntax) -like '*[force]*') + { + Restart-Computer -Verbose -Force + } + else + { + & "$env:windir\system32\shutdown.exe" /r /t 0 /f + } + Stop-Transcript + } + + $AddDotNetFilesBlock = { + if (-not (Test-Path -Path "$($driveLetter):\PsTemp")) + { + $null = mkdir -Path "$($driveLetter):\PsTemp" + } + if (-not (Test-Path -Path "$($driveLetter):\PsTemp\dotNET")) + { + $null = mkdir -Path "$($driveLetter):\PsTemp\dotNET" + } + $null = New-Item -Path "$($driveLetter):\PsTemp" -Name AtStartup.ps1 -ItemType 'file' -Value $dotNetInstallAtStartup -Force + $null = Copy-Item -Path "$Path\Resource\dotNET\$filename" -Destination "$($driveLetter):\PsTemp\dotNET\$filname" + } + + + Write-Verbose -Message "[$($MyInvocation.MyCommand)] : .NET : Adding installer to $target" + Write-Verbose -Message "[$($MyInvocation.MyCommand)] : .NET : updateting AtStartup script" + MountVHDandRunBlock -vhd $target -block $AddDotNetFilesBlock + $vmGeneration = 1 + if ((GetVHDPartitionStyle -vhd $target) -eq 'GPT') + { + $vmGeneration = 2 + } + $ConfigData = Get-UpdateConfig -Path $Path + + Write-Verbose -Message "[$($MyInvocation.MyCommand)] : .NET : Creating temp vm and waiting " + createRunAndWaitVM -vhdPath $target -vmGeneration $vmGeneration -configData $ConfigData @ParametersToPass + #endregion + + #region Install WMF + $verifyWmfVersion4 = { + Start-Transcript -Path $PSScriptRoot\AtStartup.log -Append + if ($PSVersionTable.PSVersion.Major -ge 4) + { + Write-Verbose -Message 'WMF : version varified' + Get-Date | Out-File -FilePath c:\PsTemp\ChangesMade.txt + } + else + { + Write-Warning -Message "WMF : Excpected version 4, found $($PSVersionTable.PSVersion.Major)" + } + Stop-Transcript + Stop-Computer -Force + } + $verifyWmfVersion5 = { + Start-Transcript -Path $PSScriptRoot\AtStartup.log -Append + if ($PSVersionTable.PSVersion.Major -ge 5) + { + Write-Verbose -Message 'WMF : version varified' + Get-Date | Out-File -FilePath c:\PsTemp\ChangesMade.txt + } + else + { + Write-Warning -Message "WMF : Excpected version 4, found $($PSVersionTable.PSVersion.Major)" + } + Stop-Transcript + Stop-Computer -Force + } + + if ($Wmf4) + { + $VeirfyWmfAtStartup = $verifyWmfVersion4 + } + else + { + $VeirfyWmfAtStartup = $verifyWmfVersion5 + } + + $addWmfFilesBlock = { + foreach ($update in (Get-ChildItem -Path $wmfPath\*.msu ).FullName ) + { + Write-Verbose -Message "checking if $update applies to $target" + $null = Add-WindowsPackage -PackagePath $update -Path "$($driveLetter):" + } + $null = New-Item -Path "$($driveLetter):\PsTemp" -Name AtStartup.ps1 -ItemType 'file' -Value $VeirfyWmfAtStartup -Force + } + + Write-Verbose -Message "[$($MyInvocation.MyCommand)] : WMF : Applying WMF to $target and Updating AtStartup script" + MountVHDandRunBlock -vhd $target -block $addWmfFilesBlock + Write-Verbose -Message "[$($MyInvocation.MyCommand)] : WMF : creating temp VM to finalize install on $target" + createRunAndWaitVM -vhdPath $target -vmGeneration $vmGeneration -configData $ConfigData @ParametersToPass + #endregion + + #region check for changes and merge or delete + Write-Verbose -Message "[$($MyInvocation.MyCommand)] : WMF : Checking if changes made" + $checkresultsBlock = { + Test-Path -Path "$($driveLetter):\PsTemp\ChangesMade.txt" + if (Test-Path -Path "$($driveLetter):\PsTemp\ChangesMade.txt") + { + Remove-Item -Path "$($driveLetter):\PsTemp\AtStartup.ps1" -ErrorAction SilentlyContinue + } + } + $ChangesMade = MountVHDandRunBlock -vhd $target -block $checkresultsBlock + if ($ChangesMade) + { + Write-Verbose -Message "[$($MyInvocation.MyCommand)] : WMF : Changes found : Merging $target into $parentVHD" + Merge-VHD -Path $target -DestinationPath $parentVHD + } + else + { + Write-Verbose -Message "[$($MyInvocation.MyCommand)] : WMF : No Changes : Discarding $target" + Remove-Item $target + } + #endregion + } + } +} diff --git a/deployment/dsc/azshcihost/WindowsImageTools/1.0.7/Functions/Wim2VhdClass.ps1 b/deployment/dsc/azshcihost/WindowsImageTools/1.0.7/Functions/Wim2VhdClass.ps1 new file mode 100644 index 0000000..83bfb4a --- /dev/null +++ b/deployment/dsc/azshcihost/WindowsImageTools/1.0.7/Functions/Wim2VhdClass.ps1 @@ -0,0 +1,1637 @@ +$code = @" +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.ComponentModel; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Runtime.InteropServices; +using System.Security; +using System.Text; +using System.Text.RegularExpressions; +using System.Threading; +using System.Xml.Linq; +using System.Xml.XPath; +using Microsoft.Win32.SafeHandles; + +namespace WIM2VHD +{ + +/// <summary> +/// P/Invoke methods and associated enums, flags, and structs. +/// </summary> +public class +NativeMethods +{ + + #region Delegates and Callbacks + #region WIMGAPI + + ///<summary> + ///User-defined function used with the RegisterMessageCallback or UnregisterMessageCallback function. + ///</summary> + ///<param name="MessageId">Specifies the message being sent.</param> + ///<param name="wParam">Specifies additional message information. The contents of this parameter depend on the value of the + ///MessageId parameter.</param> + ///<param name="lParam">Specifies additional message information. The contents of this parameter depend on the value of the + ///MessageId parameter.</param> + ///<param name="UserData">Specifies the user-defined value passed to RegisterCallback.</param> + ///<returns> + ///To indicate success and to enable other subscribers to process the message return WIM_MSG_SUCCESS. + ///To prevent other subscribers from receiving the message, return WIM_MSG_DONE. + ///To cancel an image apply or capture, return WIM_MSG_ABORT_IMAGE when handling the WIM_MSG_PROCESS message. + ///</returns> + public delegate uint + WimMessageCallback( + uint MessageId, + IntPtr wParam, + IntPtr lParam, + IntPtr UserData + ); + + public static void + RegisterMessageCallback( + WimFileHandle hWim, + WimMessageCallback callback) + { + + uint _callback = NativeMethods.WimRegisterMessageCallback(hWim, callback, IntPtr.Zero); + int rc = Marshal.GetLastWin32Error(); + if (0 != rc) + { + // Throw an exception if something bad happened on the Win32 end. + throw + new InvalidOperationException( + string.Format( + CultureInfo.CurrentCulture, + "Unable to register message callback." + )); + } + } + + public static void + UnregisterMessageCallback( + WimFileHandle hWim, + WimMessageCallback registeredCallback) + { + + bool status = NativeMethods.WimUnregisterMessageCallback(hWim, registeredCallback); + int rc = Marshal.GetLastWin32Error(); + if (!status) + { + throw + new InvalidOperationException( + string.Format( + CultureInfo.CurrentCulture, + "Unable to unregister message callback." + )); + } + } + + #endregion WIMGAPI + #endregion Delegates and Callbacks + + #region Constants + + #region VDiskInterop + + /// <summary> + /// The default depth in a VHD parent chain that this library will search through. + /// If you want to go more than one disk deep into the parent chain, provide a different value. + /// </summary> + public const uint OPEN_VIRTUAL_DISK_RW_DEFAULT_DEPTH = 0x00000001; + + public const uint DEFAULT_BLOCK_SIZE = 0x00080000; + public const uint DISK_SECTOR_SIZE = 0x00000200; + + internal const uint ERROR_VIRTDISK_NOT_VIRTUAL_DISK = 0xC03A0015; + internal const uint ERROR_NOT_FOUND = 0x00000490; + internal const uint ERROR_IO_PENDING = 0x000003E5; + internal const uint ERROR_INSUFFICIENT_BUFFER = 0x0000007A; + internal const uint ERROR_ERROR_DEV_NOT_EXIST = 0x00000037; + internal const uint ERROR_BAD_COMMAND = 0x00000016; + internal const uint ERROR_SUCCESS = 0x00000000; + + public const uint GENERIC_READ = 0x80000000; + public const uint GENERIC_WRITE = 0x40000000; + public const short FILE_ATTRIBUTE_NORMAL = 0x00000080; + public const uint CREATE_NEW = 0x00000001; + public const uint CREATE_ALWAYS = 0x00000002; + public const uint OPEN_EXISTING = 0x00000003; + public const short INVALID_HANDLE_VALUE = -1; + + internal static Guid VirtualStorageTypeVendorUnknown = new Guid("00000000-0000-0000-0000-000000000000"); + internal static Guid VirtualStorageTypeVendorMicrosoft = new Guid("EC984AEC-A0F9-47e9-901F-71415A66345B"); + + #endregion VDiskInterop + + #region WIMGAPI + + public const uint WIM_FLAG_VERIFY = 0x00000002; + public const uint WIM_FLAG_INDEX = 0x00000004; + + public const uint WM_APP = 0x00008000; + + #endregion WIMGAPI + + #endregion Constants + + #region Enums and Flags + + #region VDiskInterop + + /// <summary> + /// Indicates the version of the virtual disk to create. + /// </summary> + public enum CreateVirtualDiskVersion : int + { + VersionUnspecified = 0x00000000, + Version1 = 0x00000001, + Version2 = 0x00000002 + } + + public enum OpenVirtualDiskVersion : int + { + VersionUnspecified = 0x00000000, + Version1 = 0x00000001, + Version2 = 0x00000002 + } + + /// <summary> + /// Contains the version of the virtual hard disk (VHD) ATTACH_VIRTUAL_DISK_PARAMETERS structure to use in calls to VHD functions. + /// </summary> + public enum AttachVirtualDiskVersion : int + { + VersionUnspecified = 0x00000000, + Version1 = 0x00000001, + Version2 = 0x00000002 + } + + public enum CompactVirtualDiskVersion : int + { + VersionUnspecified = 0x00000000, + Version1 = 0x00000001 + } + + /// <summary> + /// Contains the type and provider (vendor) of the virtual storage device. + /// </summary> + public enum VirtualStorageDeviceType : int + { + /// <summary> + /// The storage type is unknown or not valid. + /// </summary> + Unknown = 0x00000000, + /// <summary> + /// For internal use only. This type is not supported. + /// </summary> + ISO = 0x00000001, + /// <summary> + /// Virtual Hard Disk device type. + /// </summary> + VHD = 0x00000002, + /// <summary> + /// Virtual Hard Disk v2 device type. + /// </summary> + VHDX = 0x00000003 + } + + /// <summary> + /// Contains virtual hard disk (VHD) open request flags. + /// </summary> + [Flags] + public enum OpenVirtualDiskFlags + { + /// <summary> + /// No flags. Use system defaults. + /// </summary> + None = 0x00000000, + /// <summary> + /// Open the VHD file (backing store) without opening any differencing-chain parents. Used to correct broken parent links. + /// </summary> + NoParents = 0x00000001, + /// <summary> + /// Reserved. + /// </summary> + BlankFile = 0x00000002, + /// <summary> + /// Reserved. + /// </summary> + BootDrive = 0x00000004, + } + + /// <summary> + /// Contains the bit mask for specifying access rights to a virtual hard disk (VHD). + /// </summary> + [Flags] + public enum VirtualDiskAccessMask + { + /// <summary> + /// Only Version2 of OpenVirtualDisk API accepts this parameter + /// </summary> + None = 0x00000000, + /// <summary> + /// Open the virtual disk for read-only attach access. The caller must have READ access to the virtual disk image file. + /// </summary> + /// <remarks> + /// If used in a request to open a virtual disk that is already open, the other handles must be limited to either + /// VIRTUAL_DISK_ACCESS_DETACH or VIRTUAL_DISK_ACCESS_GET_INFO access, otherwise the open request with this flag will fail. + /// </remarks> + AttachReadOnly = 0x00010000, + /// <summary> + /// Open the virtual disk for read-write attaching access. The caller must have (READ | WRITE) access to the virtual disk image file. + /// </summary> + /// <remarks> + /// If used in a request to open a virtual disk that is already open, the other handles must be limited to either + /// VIRTUAL_DISK_ACCESS_DETACH or VIRTUAL_DISK_ACCESS_GET_INFO access, otherwise the open request with this flag will fail. + /// If the virtual disk is part of a differencing chain, the disk for this request cannot be less than the readWriteDepth specified + /// during the prior open request for that differencing chain. + /// </remarks> + AttachReadWrite = 0x00020000, + /// <summary> + /// Open the virtual disk to allow detaching of an attached virtual disk. The caller must have + /// (FILE_READ_ATTRIBUTES | FILE_READ_DATA) access to the virtual disk image file. + /// </summary> + Detach = 0x00040000, + /// <summary> + /// Information retrieval access to the virtual disk. The caller must have READ access to the virtual disk image file. + /// </summary> + GetInfo = 0x00080000, + /// <summary> + /// Virtual disk creation access. + /// </summary> + Create = 0x00100000, + /// <summary> + /// Open the virtual disk to perform offline meta-operations. The caller must have (READ | WRITE) access to the virtual + /// disk image file, up to readWriteDepth if working with a differencing chain. + /// </summary> + /// <remarks> + /// If the virtual disk is part of a differencing chain, the backing store (host volume) is opened in RW exclusive mode up to readWriteDepth. + /// </remarks> + MetaOperations = 0x00200000, + /// <summary> + /// Reserved. + /// </summary> + Read = 0x000D0000, + /// <summary> + /// Allows unrestricted access to the virtual disk. The caller must have unrestricted access rights to the virtual disk image file. + /// </summary> + All = 0x003F0000, + /// <summary> + /// Reserved. + /// </summary> + Writable = 0x00320000 + } + + /// <summary> + /// Contains virtual hard disk (VHD) creation flags. + /// </summary> + [Flags] + public enum CreateVirtualDiskFlags + { + /// <summary> + /// Contains virtual hard disk (VHD) creation flags. + /// </summary> + None = 0x00000000, + /// <summary> + /// Pre-allocate all physical space necessary for the size of the virtual disk. + /// </summary> + /// <remarks> + /// The CREATE_VIRTUAL_DISK_FLAG_FULL_PHYSICAL_ALLOCATION flag is used for the creation of a fixed VHD. + /// </remarks> + FullPhysicalAllocation = 0x00000001 + } + + /// <summary> + /// Contains virtual disk attach request flags. + /// </summary> + [Flags] + public enum AttachVirtualDiskFlags + { + /// <summary> + /// No flags. Use system defaults. + /// </summary> + None = 0x00000000, + /// <summary> + /// Attach the virtual disk as read-only. + /// </summary> + ReadOnly = 0x00000001, + /// <summary> + /// No drive letters are assigned to the disk's volumes. + /// </summary> + /// <remarks>Oddly enough, this doesn't apply to NTFS mount points.</remarks> + NoDriveLetter = 0x00000002, + /// <summary> + /// Will decouple the virtual disk lifetime from that of the VirtualDiskHandle. + /// The virtual disk will be attached until the Detach() function is called, even if all open handles to the virtual disk are closed. + /// </summary> + PermanentLifetime = 0x00000004, + /// <summary> + /// Reserved. + /// </summary> + NoLocalHost = 0x00000008 + } + + [Flags] + public enum DetachVirtualDiskFlag + { + None = 0x00000000 + } + + [Flags] + public enum CompactVirtualDiskFlags + { + None = 0x00000000, + NoZeroScan = 0x00000001, + NoBlockMoves = 0x00000002 + } + + #endregion VDiskInterop + + #region WIMGAPI + + [FlagsAttribute] + internal enum + WimCreateFileDesiredAccess : uint + { + WimQuery = 0x00000000, + WimGenericRead = 0x80000000 + } + + public enum WimMessage : uint + { + WIM_MSG = WM_APP + 0x1476, + WIM_MSG_TEXT, + ///<summary> + ///Indicates an update in the progress of an image application. + ///</summary> + WIM_MSG_PROGRESS, + ///<summary> + ///Enables the caller to prevent a file or a directory from being captured or applied. + ///</summary> + WIM_MSG_PROCESS, + ///<summary> + ///Indicates that volume information is being gathered during an image capture. + ///</summary> + WIM_MSG_SCANNING, + ///<summary> + ///Indicates the number of files that will be captured or applied. + ///</summary> + WIM_MSG_SETRANGE, + ///<summary> + ///Indicates the number of files that have been captured or applied. + ///</summary> + WIM_MSG_SETPOS, + ///<summary> + ///Indicates that a file has been either captured or applied. + ///</summary> + WIM_MSG_STEPIT, + ///<summary> + ///Enables the caller to prevent a file resource from being compressed during a capture. + ///</summary> + WIM_MSG_COMPRESS, + ///<summary> + ///Alerts the caller that an error has occurred while capturing or applying an image. + ///</summary> + WIM_MSG_ERROR, + ///<summary> + ///Enables the caller to align a file resource on a particular alignment boundary. + ///</summary> + WIM_MSG_ALIGNMENT, + WIM_MSG_RETRY, + ///<summary> + ///Enables the caller to align a file resource on a particular alignment boundary. + ///</summary> + WIM_MSG_SPLIT, + WIM_MSG_SUCCESS = 0x00000000, + WIM_MSG_ABORT_IMAGE = 0xFFFFFFFF + } + + internal enum + WimCreationDisposition : uint + { + WimOpenExisting = 0x00000003, + } + + internal enum + WimActionFlags : uint + { + WimIgnored = 0x00000000 + } + + internal enum + WimCompressionType : uint + { + WimIgnored = 0x00000000 + } + + internal enum + WimCreationResult : uint + { + WimCreatedNew = 0x00000000, + WimOpenedExisting = 0x00000001 + } + + #endregion WIMGAPI + + #endregion Enums and Flags + + #region Structs + + [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] + public struct CreateVirtualDiskParameters + { + /// <summary> + /// A CREATE_VIRTUAL_DISK_VERSION enumeration that specifies the version of the CREATE_VIRTUAL_DISK_PARAMETERS structure being passed to or from the virtual hard disk (VHD) functions. + /// </summary> + public CreateVirtualDiskVersion Version; + + /// <summary> + /// Unique identifier to assign to the virtual disk object. If this member is set to zero, a unique identifier is created by the system. + /// </summary> + public Guid UniqueId; + + /// <summary> + /// The maximum virtual size of the virtual disk object. Must be a multiple of 512. + /// If a ParentPath is specified, this value must be zero. + /// If a SourcePath is specified, this value can be zero to specify the size of the source VHD to be used, otherwise the size specified must be greater than or equal to the size of the source disk. + /// </summary> + public ulong MaximumSize; + + /// <summary> + /// Internal size of the virtual disk object blocks. + /// The following are predefined block sizes and their behaviors. For a fixed VHD type, this parameter must be zero. + /// </summary> + public uint BlockSizeInBytes; + + /// <summary> + /// Internal size of the virtual disk object sectors. Must be set to 512. + /// </summary> + public uint SectorSizeInBytes; + + /// <summary> + /// Optional path to a parent virtual disk object. Associates the new virtual disk with an existing virtual disk. + /// If this parameter is not NULL, SourcePath must be NULL. + /// </summary> + public string ParentPath; + + /// <summary> + /// Optional path to pre-populate the new virtual disk object with block data from an existing disk. This path may refer to a VHD or a physical disk. + /// If this parameter is not NULL, ParentPath must be NULL. + /// </summary> + public string SourcePath; + + /// <summary> + /// Flags for opening the VHD + /// </summary> + public OpenVirtualDiskFlags OpenFlags; + + /// <summary> + /// GetInfoOnly flag for V2 handles + /// </summary> + public bool GetInfoOnly; + + /// <summary> + /// Virtual Storage Type of the parent disk + /// </summary> + public VirtualStorageType ParentVirtualStorageType; + + /// <summary> + /// Virtual Storage Type of the source disk + /// </summary> + public VirtualStorageType SourceVirtualStorageType; + + /// <summary> + /// A GUID to use for fallback resiliency over SMB. + /// </summary> + public Guid ResiliencyGuid; + } + + [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] + public struct VirtualStorageType + { + public VirtualStorageDeviceType DeviceId; + public Guid VendorId; + } + + [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] + public struct SecurityDescriptor + { + public byte revision; + public byte size; + public short control; + public IntPtr owner; + public IntPtr group; + public IntPtr sacl; + public IntPtr dacl; + } + + #endregion Structs + + #region VirtDisk.DLL P/Invoke + + [DllImport("virtdisk.dll", CharSet = CharSet.Unicode)] + public static extern uint + CreateVirtualDisk( + [In, Out] ref VirtualStorageType VirtualStorageType, + [In] string Path, + [In] VirtualDiskAccessMask VirtualDiskAccessMask, + [In, Out] ref SecurityDescriptor SecurityDescriptor, + [In] CreateVirtualDiskFlags Flags, + [In] uint ProviderSpecificFlags, + [In, Out] ref CreateVirtualDiskParameters Parameters, + [In] IntPtr Overlapped, + [Out] out SafeFileHandle Handle); + + #endregion VirtDisk.DLL P/Invoke + + #region Win32 P/Invoke + + [DllImport("advapi32", SetLastError = true)] + public static extern bool InitializeSecurityDescriptor( + [Out] out SecurityDescriptor pSecurityDescriptor, + [In] uint dwRevision); + + #endregion Win32 P/Invoke + + #region WIMGAPI P/Invoke + + #region SafeHandle wrappers for WimFileHandle and WimImageHandle + + public sealed class WimFileHandle : SafeHandle + { + + public WimFileHandle( + string wimPath) + : base(IntPtr.Zero, true) + { + + if (String.IsNullOrEmpty(wimPath)) + { + throw new ArgumentNullException("wimPath"); + } + + if (!File.Exists(Path.GetFullPath(wimPath))) + { + throw new FileNotFoundException((new FileNotFoundException()).Message, wimPath); + } + + NativeMethods.WimCreationResult creationResult; + + this.handle = NativeMethods.WimCreateFile( + wimPath, + NativeMethods.WimCreateFileDesiredAccess.WimGenericRead, + NativeMethods.WimCreationDisposition.WimOpenExisting, + NativeMethods.WimActionFlags.WimIgnored, + NativeMethods.WimCompressionType.WimIgnored, + out creationResult + ); + + // Check results. + if (creationResult != NativeMethods.WimCreationResult.WimOpenedExisting) + { + throw new Win32Exception(); + } + + if (this.handle == IntPtr.Zero) + { + throw new Win32Exception(); + } + + // Set the temporary path. + NativeMethods.WimSetTemporaryPath( + this, + Environment.ExpandEnvironmentVariables("%TEMP%") + ); + } + + protected override bool ReleaseHandle() + { + return NativeMethods.WimCloseHandle(this.handle); + } + + public override bool IsInvalid + { + get { return this.handle == IntPtr.Zero; } + } + } + + public sealed class WimImageHandle : SafeHandle + { + public WimImageHandle( + WimFile Container, + uint ImageIndex) + : base(IntPtr.Zero, true) + { + + if (null == Container) + { + throw new ArgumentNullException("Container"); + } + + if ((Container.Handle.IsClosed) || (Container.Handle.IsInvalid)) + { + throw new ArgumentNullException("The handle to the WIM file has already been closed, or is invalid.", "Container"); + } + + if (ImageIndex > Container.ImageCount) + { + throw new ArgumentOutOfRangeException("ImageIndex", "The index does not exist in the specified WIM file."); + } + + this.handle = NativeMethods.WimLoadImage( + Container.Handle.DangerousGetHandle(), + ImageIndex); + } + + protected override bool ReleaseHandle() + { + return NativeMethods.WimCloseHandle(this.handle); + } + + public override bool IsInvalid + { + get { return this.handle == IntPtr.Zero; } + } + } + + #endregion SafeHandle wrappers for WimFileHandle and WimImageHandle + + [DllImport("Wimgapi.dll", CharSet = CharSet.Unicode, SetLastError = true, EntryPoint = "WIMCreateFile")] + internal static extern IntPtr + WimCreateFile( + [In, MarshalAs(UnmanagedType.LPWStr)] string WimPath, + [In] WimCreateFileDesiredAccess DesiredAccess, + [In] WimCreationDisposition CreationDisposition, + [In] WimActionFlags FlagsAndAttributes, + [In] WimCompressionType CompressionType, + [Out, Optional] out WimCreationResult CreationResult + ); + + [DllImport("Wimgapi.dll", CharSet = CharSet.Unicode, SetLastError = true, EntryPoint = "WIMCloseHandle")] + [return: MarshalAs(UnmanagedType.Bool)] + internal static extern bool + WimCloseHandle( + [In] IntPtr Handle + ); + + [DllImport("Wimgapi.dll", CharSet = CharSet.Unicode, SetLastError = true, EntryPoint = "WIMLoadImage")] + internal static extern IntPtr + WimLoadImage( + [In] IntPtr Handle, + [In] uint ImageIndex + ); + + [DllImport("Wimgapi.dll", CharSet = CharSet.Unicode, SetLastError = true, EntryPoint = "WIMGetImageCount")] + internal static extern uint + WimGetImageCount( + [In] WimFileHandle Handle + ); + + [DllImport("Wimgapi.dll", CharSet = CharSet.Unicode, SetLastError = true, EntryPoint = "WIMGetImageInformation")] + [return: MarshalAs(UnmanagedType.Bool)] + internal static extern bool + WimGetImageInformation( + [In] SafeHandle Handle, + [Out] out StringBuilder ImageInfo, + [Out] out uint SizeOfImageInfo + ); + + [DllImport("Wimgapi.dll", CharSet = CharSet.Unicode, SetLastError = true, EntryPoint = "WIMSetTemporaryPath")] + [return: MarshalAs(UnmanagedType.Bool)] + internal static extern bool + WimSetTemporaryPath( + [In] WimFileHandle Handle, + [In] string TempPath + ); + + [DllImport("Wimgapi.dll", CharSet = CharSet.Unicode, SetLastError = true, EntryPoint = "WIMRegisterMessageCallback", CallingConvention = CallingConvention.StdCall)] + internal static extern uint + WimRegisterMessageCallback( + [In, Optional] WimFileHandle hWim, + [In] WimMessageCallback MessageProc, + [In, Optional] IntPtr ImageInfo + ); + + [DllImport("Wimgapi.dll", CharSet = CharSet.Unicode, SetLastError = true, EntryPoint = "WIMUnregisterMessageCallback", CallingConvention = CallingConvention.StdCall)] + [return: MarshalAs(UnmanagedType.Bool)] + internal static extern bool + WimUnregisterMessageCallback( + [In, Optional] WimFileHandle hWim, + [In] WimMessageCallback MessageProc + ); + + + #endregion WIMGAPI P/Invoke +} + +#region WIM Interop + +public class WimFile +{ + + internal XDocument m_xmlInfo; + internal List<WimImage> m_imageList; + + private static NativeMethods.WimMessageCallback wimMessageCallback; + + #region Events + + /// <summary> + /// DefaultImageEvent handler + /// </summary> + public delegate void DefaultImageEventHandler(object sender, DefaultImageEventArgs e); + + ///<summary> + ///ProcessFileEvent handler + ///</summary> + public delegate void ProcessFileEventHandler(object sender, ProcessFileEventArgs e); + + ///<summary> + ///Enable the caller to prevent a file resource from being compressed during a capture. + ///</summary> + public event ProcessFileEventHandler ProcessFileEvent; + + ///<summary> + ///Indicate an update in the progress of an image application. + ///</summary> + public event DefaultImageEventHandler ProgressEvent; + + ///<summary> + ///Alert the caller that an error has occurred while capturing or applying an image. + ///</summary> + public event DefaultImageEventHandler ErrorEvent; + + ///<summary> + ///Indicate that a file has been either captured or applied. + ///</summary> + public event DefaultImageEventHandler StepItEvent; + + ///<summary> + ///Indicate the number of files that will be captured or applied. + ///</summary> + public event DefaultImageEventHandler SetRangeEvent; + + ///<summary> + ///Indicate the number of files that have been captured or applied. + ///</summary> + public event DefaultImageEventHandler SetPosEvent; + + #endregion Events + + private + enum + ImageEventMessage : uint + { + ///<summary> + ///Enables the caller to prevent a file or a directory from being captured or applied. + ///</summary> + Progress = NativeMethods.WimMessage.WIM_MSG_PROGRESS, + ///<summary> + ///Notification sent to enable the caller to prevent a file or a directory from being captured or applied. + ///To prevent a file or a directory from being captured or applied, call WindowsImageContainer.SkipFile(). + ///</summary> + Process = NativeMethods.WimMessage.WIM_MSG_PROCESS, + ///<summary> + ///Enables the caller to prevent a file resource from being compressed during a capture. + ///</summary> + Compress = NativeMethods.WimMessage.WIM_MSG_COMPRESS, + ///<summary> + ///Alerts the caller that an error has occurred while capturing or applying an image. + ///</summary> + Error = NativeMethods.WimMessage.WIM_MSG_ERROR, + ///<summary> + ///Enables the caller to align a file resource on a particular alignment boundary. + ///</summary> + Alignment = NativeMethods.WimMessage.WIM_MSG_ALIGNMENT, + ///<summary> + ///Enables the caller to align a file resource on a particular alignment boundary. + ///</summary> + Split = NativeMethods.WimMessage.WIM_MSG_SPLIT, + ///<summary> + ///Indicates that volume information is being gathered during an image capture. + ///</summary> + Scanning = NativeMethods.WimMessage.WIM_MSG_SCANNING, + ///<summary> + ///Indicates the number of files that will be captured or applied. + ///</summary> + SetRange = NativeMethods.WimMessage.WIM_MSG_SETRANGE, + ///<summary> + ///Indicates the number of files that have been captured or applied. + /// </summary> + SetPos = NativeMethods.WimMessage.WIM_MSG_SETPOS, + ///<summary> + ///Indicates that a file has been either captured or applied. + ///</summary> + StepIt = NativeMethods.WimMessage.WIM_MSG_STEPIT, + ///<summary> + ///Success. + ///</summary> + Success = NativeMethods.WimMessage.WIM_MSG_SUCCESS, + ///<summary> + ///Abort. + ///</summary> + Abort = NativeMethods.WimMessage.WIM_MSG_ABORT_IMAGE + } + + ///<summary> + ///Event callback to the Wimgapi events + ///</summary> + private + uint + ImageEventMessagePump( + uint MessageId, + IntPtr wParam, + IntPtr lParam, + IntPtr UserData) + { + + uint status = (uint) NativeMethods.WimMessage.WIM_MSG_SUCCESS; + + DefaultImageEventArgs eventArgs = new DefaultImageEventArgs(wParam, lParam, UserData); + + switch ((ImageEventMessage)MessageId) + { + + case ImageEventMessage.Progress: + ProgressEvent(this, eventArgs); + break; + + case ImageEventMessage.Process: + if (null != ProcessFileEvent) + { + string fileToImage = Marshal.PtrToStringUni(wParam); + ProcessFileEventArgs fileToProcess = new ProcessFileEventArgs(fileToImage, lParam); + ProcessFileEvent(this, fileToProcess); + + if (fileToProcess.Abort == true) + { + status = (uint)ImageEventMessage.Abort; + } + } + break; + + case ImageEventMessage.Error: + if (null != ErrorEvent) + { + ErrorEvent(this, eventArgs); + } + break; + + case ImageEventMessage.SetRange: + if (null != SetRangeEvent) + { + SetRangeEvent(this, eventArgs); + } + break; + + case ImageEventMessage.SetPos: + if (null != SetPosEvent) + { + SetPosEvent(this, eventArgs); + } + break; + + case ImageEventMessage.StepIt: + if (null != StepItEvent) + { + StepItEvent(this, eventArgs); + } + break; + + default: + break; + } + return status; + + } + + /// <summary> + /// Constructor. + /// </summary> + /// <param name="wimPath">Path to the WIM container.</param> + public + WimFile(string wimPath) + { + if (string.IsNullOrEmpty(wimPath)) + { + throw new ArgumentNullException("wimPath"); + } + + if (!File.Exists(Path.GetFullPath(wimPath))) + { + throw new FileNotFoundException((new FileNotFoundException()).Message, wimPath); + } + + Handle = new NativeMethods.WimFileHandle(wimPath); + + // Hook up the events before we return. + //wimMessageCallback = new NativeMethods.WimMessageCallback(ImageEventMessagePump); + //NativeMethods.RegisterMessageCallback(this.Handle, wimMessageCallback); + } + + /// <summary> + /// Closes the WIM file. + /// </summary> + public void + Close() + { + foreach (WimImage image in Images) + { + image.Close(); + } + + if (null != wimMessageCallback) + { + NativeMethods.UnregisterMessageCallback(this.Handle, wimMessageCallback); + wimMessageCallback = null; + } + + if ((!Handle.IsClosed) && (!Handle.IsInvalid)) + { + Handle.Close(); + } + } + + /// <summary> + /// Provides a list of WimImage objects, representing the images in the WIM container file. + /// </summary> + public List<WimImage> + Images + { + get + { + if (null == m_imageList) + { + + int imageCount = (int)ImageCount; + m_imageList = new List<WimImage>(imageCount); + for (int i = 0; i < imageCount; i++) + { + + // Load up each image so it's ready for us. + m_imageList.Add( + new WimImage(this, (uint)i + 1)); + } + } + + return m_imageList; + } + } + + /// <summary> + /// Provides a list of names of the images in the specified WIM container file. + /// </summary> + public List<string> + ImageNames + { + get + { + List<string> nameList = new List<string>(); + foreach (WimImage image in Images) + { + nameList.Add(image.ImageName); + } + return nameList; + } + } + + /// <summary> + /// Indexer for WIM images inside the WIM container, indexed by the image number. + /// The list of Images is 0-based, but the WIM container is 1-based, so we automatically compensate for that. + /// this[1] returns the 0th image in the WIM container. + /// </summary> + /// <param name="ImageIndex">The 1-based index of the image to retrieve.</param> + /// <returns>WinImage object.</returns> + public WimImage + this[int ImageIndex] + { + get { return Images[ImageIndex - 1]; } + } + + /// <summary> + /// Indexer for WIM images inside the WIM container, indexed by the image name. + /// WIMs created by different processes sometimes contain different information - including the name. + /// Some images have their name stored in the Name field, some in the Flags field, and some in the EditionID field. + /// We take all of those into account in while searching the WIM. + /// </summary> + /// <param name="ImageName"></param> + /// <returns></returns> + public WimImage + this[string ImageName] + { + get + { + return + Images.Where(i => ( + i.ImageName.ToUpper() == ImageName.ToUpper() || + i.ImageFlags.ToUpper() == ImageName.ToUpper() )) + .DefaultIfEmpty(null) + .FirstOrDefault<WimImage>(); + } + } + + /// <summary> + /// Returns the number of images in the WIM container. + /// </summary> + internal uint + ImageCount + { + get { return NativeMethods.WimGetImageCount(Handle); } + } + + /// <summary> + /// Returns an XDocument representation of the XML metadata for the WIM container and associated images. + /// </summary> + internal XDocument + XmlInfo + { + get + { + + if (null == m_xmlInfo) + { + StringBuilder builder; + uint bytes; + if (!NativeMethods.WimGetImageInformation(Handle, out builder, out bytes)) + { + throw new Win32Exception(); + } + + // Ensure the length of the returned bytes to avoid garbage characters at the end. + int charCount = (int)bytes / sizeof(char); + if (null != builder) + { + // Get rid of the unicode file marker at the beginning of the XML. + builder.Remove(0, 1); + builder.EnsureCapacity(charCount - 1); + builder.Length = charCount - 1; + + // This isn't likely to change while we have the image open, so cache it. + m_xmlInfo = XDocument.Parse(builder.ToString().Trim()); + } + else + { + m_xmlInfo = null; + } + } + + return m_xmlInfo; + } + } + + public NativeMethods.WimFileHandle Handle + { + get; + private set; + } +} + +public class +WimImage +{ + + internal XDocument m_xmlInfo; + + public + WimImage( + WimFile Container, + uint ImageIndex) + { + + if (null == Container) + { + throw new ArgumentNullException("Container"); + } + + if ((Container.Handle.IsClosed) || (Container.Handle.IsInvalid)) + { + throw new ArgumentNullException("The handle to the WIM file has already been closed, or is invalid.", "Container"); + } + + if (ImageIndex > Container.ImageCount) + { + throw new ArgumentOutOfRangeException("ImageIndex", "The index does not exist in the specified WIM file."); + } + + Handle = new NativeMethods.WimImageHandle(Container, ImageIndex); + } + + public enum + Architectures : uint + { + x86 = 0x0, + ARM = 0x5, + IA64 = 0x6, + AMD64 = 0x9, + ARM64 = 0xC + } + + public void + Close() + { + if ((!Handle.IsClosed) && (!Handle.IsInvalid)) + { + Handle.Close(); + } + } + + public NativeMethods.WimImageHandle + Handle + { + get; + private set; + } + + internal XDocument + XmlInfo + { + get + { + + if (null == m_xmlInfo) + { + StringBuilder builder; + uint bytes; + if (!NativeMethods.WimGetImageInformation(Handle, out builder, out bytes)) + { + throw new Win32Exception(); + } + + // Ensure the length of the returned bytes to avoid garbage characters at the end. + int charCount = (int)bytes / sizeof(char); + if (null != builder) + { + // Get rid of the unicode file marker at the beginning of the XML. + builder.Remove(0, 1); + builder.EnsureCapacity(charCount - 1); + builder.Length = charCount - 1; + + // This isn't likely to change while we have the image open, so cache it. + m_xmlInfo = XDocument.Parse(builder.ToString().Trim()); + } + else + { + m_xmlInfo = null; + } + } + + return m_xmlInfo; + } + } + + public string + ImageIndex + { + get { return XmlInfo.Element("IMAGE").Attribute("INDEX").Value; } + } + + public string + ImageName + { + get { return XmlInfo.XPathSelectElement("/IMAGE/NAME").Value; } + } + + public string + ImageEditionId + { + get { return XmlInfo.XPathSelectElement("/IMAGE/WINDOWS/EDITIONID").Value; } + } + + public string + ImageFlags + { + get + { + string flagValue = String.Empty; + + try + { + flagValue = XmlInfo.XPathSelectElement("/IMAGE/FLAGS").Value; + } + catch + { + + // Some WIM files don't contain a FLAGS element in the metadata. + // In an effort to support those WIMs too, inherit the EditionId if there + // are no Flags. + + if (String.IsNullOrEmpty(flagValue)) + { + flagValue = this.ImageEditionId; + + // Check to see if the EditionId is "ServerHyper". If so, + // tweak it to be "ServerHyperCore" instead. + + if (0 == String.Compare("serverhyper", flagValue, true)) + { + flagValue = "ServerHyperCore"; + } + } + + } + + return flagValue; + } + } + + public string + ImageProductType + { + get { return XmlInfo.XPathSelectElement("/IMAGE/WINDOWS/PRODUCTTYPE").Value; } + } + + public string + ImageInstallationType + { + get { return XmlInfo.XPathSelectElement("/IMAGE/WINDOWS/INSTALLATIONTYPE").Value; } + } + + public string + ImageDescription + { + get { return XmlInfo.XPathSelectElement("/IMAGE/DESCRIPTION").Value; } + } + + public ulong + ImageSize + { + get { return ulong.Parse(XmlInfo.XPathSelectElement("/IMAGE/TOTALBYTES").Value); } + } + + public Architectures + ImageArchitecture + { + get + { + int arch = -1; + try + { + arch = int.Parse(XmlInfo.XPathSelectElement("/IMAGE/WINDOWS/ARCH").Value); + } + catch { } + + return (Architectures)arch; + } + } + + public string + ImageDefaultLanguage + { + get + { + string lang = null; + try + { + lang = XmlInfo.XPathSelectElement("/IMAGE/WINDOWS/LANGUAGES/DEFAULT").Value; + } + catch { } + + return lang; + } + } + + public Version + ImageVersion + { + get + { + int major = 0; + int minor = 0; + int build = 0; + int revision = 0; + + try + { + major = int.Parse(XmlInfo.XPathSelectElement("/IMAGE/WINDOWS/VERSION/MAJOR").Value); + minor = int.Parse(XmlInfo.XPathSelectElement("/IMAGE/WINDOWS/VERSION/MINOR").Value); + build = int.Parse(XmlInfo.XPathSelectElement("/IMAGE/WINDOWS/VERSION/BUILD").Value); + revision = int.Parse(XmlInfo.XPathSelectElement("/IMAGE/WINDOWS/VERSION/SPBUILD").Value); + } + catch { } + + return (new Version(major, minor, build, revision)); + } + } + + public string + ImageDisplayName + { + get { return XmlInfo.XPathSelectElement("/IMAGE/DISPLAYNAME").Value; } + } + + public string + ImageDisplayDescription + { + get { return XmlInfo.XPathSelectElement("/IMAGE/DISPLAYDESCRIPTION").Value; } + } +} + +///<summary> +///Describes the file that is being processed for the ProcessFileEvent. +///</summary> +public class +DefaultImageEventArgs : EventArgs +{ + ///<summary> + ///Default constructor. + ///</summary> + public + DefaultImageEventArgs( + IntPtr wideParameter, + IntPtr leftParameter, + IntPtr userData) + { + + WideParameter = wideParameter; + LeftParameter = leftParameter; + UserData = userData; + } + + ///<summary> + ///wParam + ///</summary> + public IntPtr WideParameter + { + get; + private set; + } + + ///<summary> + ///lParam + ///</summary> + public IntPtr LeftParameter + { + get; + private set; + } + + ///<summary> + ///UserData + ///</summary> + public IntPtr UserData + { + get; + private set; + } +} + +///<summary> +///Describes the file that is being processed for the ProcessFileEvent. +///</summary> +public class +ProcessFileEventArgs : EventArgs +{ + ///<summary> + ///Default constructor. + ///</summary> + ///<param name="file">Fully qualified path and file name. For example: c:\file.sys.</param> + ///<param name="skipFileFlag">Default is false - skip file and continue. + ///Set to true to abort the entire image capture.</param> + public + ProcessFileEventArgs( + string file, + IntPtr skipFileFlag) + { + + m_FilePath = file; + m_SkipFileFlag = skipFileFlag; + } + + ///<summary> + ///Skip file from being imaged. + ///</summary> + public void + SkipFile() + { + byte[] byteBuffer = + { + 0 + }; + int byteBufferSize = byteBuffer.Length; + Marshal.Copy(byteBuffer, 0, m_SkipFileFlag, byteBufferSize); + } + + ///<summary> + ///Fully qualified path and file name. + ///</summary> + public string + FilePath + { + get + { + string stringToReturn = ""; + if (m_FilePath != null) + { + stringToReturn = m_FilePath; + } + return stringToReturn; + } + } + + ///<summary> + ///Flag to indicate if the entire image capture should be aborted. + ///Default is false - skip file and continue. Setting to true will + ///abort the entire image capture. + ///</summary> + public bool Abort + { + set { m_Abort = value; } + get { return m_Abort; } + } + + private string m_FilePath; + private bool m_Abort; + private IntPtr m_SkipFileFlag; + +} + +#endregion WIM Interop + +#region VHD Interop +// Based on code written by the Hyper-V Test team. +/// <summary> +/// The Virtual Hard Disk class provides methods for creating and manipulating Virtual Hard Disk files. +/// </summary> +public class +VirtualHardDisk +{ + #region Static Methods + + #region Sparse Disks + + /// <summary> + /// Abbreviated signature of CreateSparseDisk so it's easier to use from WIM2VHD. + /// </summary> + /// <param name="virtualStorageDeviceType">The type of disk to create, VHD or VHDX.</param> + /// <param name="path">The path of the disk to create.</param> + /// <param name="size">The maximum size of the disk to create.</param> + /// <param name="overwrite">Overwrite the VHD if it already exists.</param> + public static void + CreateSparseDisk( + NativeMethods.VirtualStorageDeviceType virtualStorageDeviceType, + string path, + ulong size, + bool overwrite) + { + + CreateSparseDisk( + path, + size, + overwrite, + null, + IntPtr.Zero, + (virtualStorageDeviceType == NativeMethods.VirtualStorageDeviceType.VHD) + ? NativeMethods.DEFAULT_BLOCK_SIZE + : 0, + virtualStorageDeviceType, + NativeMethods.DISK_SECTOR_SIZE); + } + + /// <summary> + /// Creates a new sparse (dynamically expanding) virtual hard disk (.vhd). Supports both sync and async modes. + /// The VHD image file uses only as much space on the backing store as needed to store the actual data the VHD currently contains. + /// </summary> + /// <param name="path">The path and name of the VHD to create.</param> + /// <param name="size">The size of the VHD to create in bytes. + /// When creating this type of VHD, the VHD API does not test for free space on the physical backing store based on the maximum size requested, + /// therefore it is possible to successfully create a dynamic VHD with a maximum size larger than the available physical disk free space. + /// The maximum size of a dynamic VHD is 2,040 GB. The minimum size is 3 MB.</param> + /// <param name="source">Optional path to pre-populate the new virtual disk object with block data from an existing disk + /// This path may refer to a VHD or a physical disk. Use NULL if you don't want a source.</param> + /// <param name="overwrite">If the VHD exists, setting this parameter to 'True' will delete it and create a new one.</param> + /// <param name="overlapped">If not null, the operation runs in async mode</param> + /// <param name="blockSizeInBytes">Block size for the VHD.</param> + /// <param name="virtualStorageDeviceType">VHD format version (VHD1 or VHD2)</param> + /// <param name="sectorSizeInBytes">Sector size for the VHD.</param> + /// <exception cref="ArgumentOutOfRangeException">Thrown when an invalid size is specified</exception> + /// <exception cref="FileNotFoundException">Thrown when source VHD is not found.</exception> + /// <exception cref="SecurityException">Thrown when there was an error while creating the default security descriptor.</exception> + /// <exception cref="Win32Exception">Thrown when an error occurred while creating the VHD.</exception> + public static void + CreateSparseDisk( + string path, + ulong size, + bool overwrite, + string source, + IntPtr overlapped, + uint blockSizeInBytes, + NativeMethods.VirtualStorageDeviceType virtualStorageDeviceType, + uint sectorSizeInBytes) + { + + // Validate the virtualStorageDeviceType + if (virtualStorageDeviceType != NativeMethods.VirtualStorageDeviceType.VHD && virtualStorageDeviceType != NativeMethods.VirtualStorageDeviceType.VHDX) + { + + throw ( + new ArgumentOutOfRangeException( + "virtualStorageDeviceType", + virtualStorageDeviceType, + "VirtualStorageDeviceType must be VHD or VHDX." + )); + } + + // Validate size. It needs to be a multiple of DISK_SECTOR_SIZE (512)... + if ((size % NativeMethods.DISK_SECTOR_SIZE) != 0) + { + + throw ( + new ArgumentOutOfRangeException( + "size", + size, + "The size of the virtual disk must be a multiple of 512." + )); + } + + if ((!String.IsNullOrEmpty(source)) && (!System.IO.File.Exists(source))) + { + + throw ( + new System.IO.FileNotFoundException( + "Unable to find the source file.", + source + )); + } + + if ((overwrite) && (System.IO.File.Exists(path))) + { + + System.IO.File.Delete(path); + } + + NativeMethods.CreateVirtualDiskParameters createParams = new NativeMethods.CreateVirtualDiskParameters(); + + // Select the correct version. + createParams.Version = (virtualStorageDeviceType == NativeMethods.VirtualStorageDeviceType.VHD) + ? NativeMethods.CreateVirtualDiskVersion.Version1 + : NativeMethods.CreateVirtualDiskVersion.Version2; + + createParams.UniqueId = Guid.NewGuid(); + createParams.MaximumSize = size; + createParams.BlockSizeInBytes = blockSizeInBytes; + createParams.SectorSizeInBytes = sectorSizeInBytes; + createParams.ParentPath = null; + createParams.SourcePath = source; + createParams.OpenFlags = NativeMethods.OpenVirtualDiskFlags.None; + createParams.GetInfoOnly = false; + createParams.ParentVirtualStorageType = new NativeMethods.VirtualStorageType(); + createParams.SourceVirtualStorageType = new NativeMethods.VirtualStorageType(); + + // + // Create and init a security descriptor. + // Since we're creating an essentially blank SD to use with CreateVirtualDisk + // the VHD will take on the security values from the parent directory. + // + + NativeMethods.SecurityDescriptor securityDescriptor; + if (!NativeMethods.InitializeSecurityDescriptor(out securityDescriptor, 1)) + { + + throw ( + new SecurityException( + "Unable to initialize the security descriptor for the virtual disk." + )); + } + + NativeMethods.VirtualStorageType virtualStorageType = new NativeMethods.VirtualStorageType(); + virtualStorageType.DeviceId = virtualStorageDeviceType; + virtualStorageType.VendorId = NativeMethods.VirtualStorageTypeVendorMicrosoft; + + SafeFileHandle vhdHandle; + + uint returnCode = NativeMethods.CreateVirtualDisk( + ref virtualStorageType, + path, + (virtualStorageDeviceType == NativeMethods.VirtualStorageDeviceType.VHD) + ? NativeMethods.VirtualDiskAccessMask.All + : NativeMethods.VirtualDiskAccessMask.None, + ref securityDescriptor, + NativeMethods.CreateVirtualDiskFlags.None, + 0, + ref createParams, + overlapped, + out vhdHandle); + + vhdHandle.Close(); + + if (NativeMethods.ERROR_SUCCESS != returnCode && NativeMethods.ERROR_IO_PENDING != returnCode) + { + + throw ( + new Win32Exception( + (int)returnCode + )); + } + } + + #endregion Sparse Disks + + #endregion Static Methods + +} +#endregion VHD Interop +} +"@ +Add-Type -TypeDefinition $code -ReferencedAssemblies 'System.Xml', 'System.Linq', 'System.Xml.Linq' \ No newline at end of file diff --git a/deployment/dsc/azshcihost/WindowsImageTools/1.0.7/WindowsImageTools.psd1 b/deployment/dsc/azshcihost/WindowsImageTools/1.0.7/WindowsImageTools.psd1 new file mode 100644 index 0000000..cafb822 --- /dev/null +++ b/deployment/dsc/azshcihost/WindowsImageTools/1.0.7/WindowsImageTools.psd1 @@ -0,0 +1,132 @@ +# +# Module manifest for module 'WindowsImageTools' +# +# Generated by: David Jones +# +# Generated on: 8/18/2015 +# + +@{ + +# Script module or binary module file associated with this manifest. +RootModule = '.\WindowsImageTools.psm1' + +# Version number of this module. +ModuleVersion = '1.0.7' + +# ID used to uniquely identify this module +GUID = '6210674e-8cfa-4f61-a2fb-c54fd7ffcba1' + +# Author of this module +Author = 'David Jones' + +# Company or vendor of this module +CompanyName = 'BladeFireLight' + +# Copyright statement for this module +Copyright = '2015' + +# Description of the functionality provided by this module +Description = 'Tools for creating bootable virtual disks from an ISO or WIM' + +# Minimum version of the Windows PowerShell engine required by this module +# PowerShellVersion = '' + +# Name of the Windows PowerShell host required by this module +# PowerShellHostName = '' + +# Minimum version of the Windows PowerShell host required by this module +# PowerShellHostVersion = '' + +# Minimum version of Microsoft .NET Framework required by this module +# DotNetFrameworkVersion = '' + +# Minimum version of the common language runtime (CLR) required by this module +# CLRVersion = '' + +# Processor architecture (None, X86, Amd64) required by this module +# ProcessorArchitecture = '' + +# Modules that must be imported into the global environment prior to importing this module +# RequiredModules = @('storage') + +# Assemblies that must be loaded prior to importing this module +# RequiredAssemblies = @() + +# Script files (.ps1) that are run in the caller's environment prior to importing this module. +# ScriptsToProcess = @() + +# Type files (.ps1xml) to be loaded when importing this module +# TypesToProcess = @() + +# Format files (.ps1xml) to be loaded when importing this module +# FormatsToProcess = @() + +# Modules to import as nested modules of the module specified in RootModule/ModuleToProcess +# NestedModules = @() + +# Functions to export from this module +FunctionsToExport = 'Convert-Wim2VHD', + 'Initialize-VHDPartition', + 'Set-VHDPartition', + 'New-UnattendXml', + 'New-WindowsImageToolsExample', + 'Set-UpdateConfig', + 'Get-UpdateConfig', + 'Add-UpdateImage', + 'Update-WindowsImageWMF', + 'Invoke-WindowsImageUpdate', + 'Invoke-CreateVmRunAndWait', + 'Mount-VhdAndRunBlock', + 'Get-VhdPartitionStyle' + +# Cmdlets to export from this module +# CmdletsToExport = '*' + +# Variables to export from this module +# VariablesToExport = '*' + +# Aliases to export from this module +# AliasesToExport = '*' + +# DSC resources to export from this module +# DscResourcesToExport = @() + +# List of all modules packaged with this module +# ModuleList = @() + +# List of all files packaged with this module +# FileList = @() + +# Private data to pass to the module specified in RootModule/ModuleToProcess. This may also contain a PSData hashtable with additional module metadata used by PowerShell. +PrivateData = @{ + + PSData = @{ + + # Tags applied to this module. These help with module discovery in online galleries. + Tags = @('WIM', 'VHDX') + + # A URL to the license for this module. + LicenseUri = 'https://github.com/BladeFireLight/WindowsImageTools/blob/master/LICENSE' + + # A URL to the main website for this project. + ProjectUri = 'https://github.com/BladeFireLight/WindowsImageTools' + + # A URL to an icon representing this module. + # IconUri = '' + + # ReleaseNotes of this module + ReleaseNotes = 'Fixed but with BCDBoot ran fine but PowerShell thought it threw and error' + + } # End of PSData hashtable + +} # End of PrivateData hashtable + +# HelpInfo URI of this module +# HelpInfoURI = '' + +# Default prefix for commands exported from this module. Override the default prefix using Import-Module -Prefix. +# DefaultCommandPrefix = '' + +} + diff --git a/deployment/dsc/azshcihost/WindowsImageTools/1.0.7/WindowsImageTools.psm1 b/deployment/dsc/azshcihost/WindowsImageTools/1.0.7/WindowsImageTools.psm1 new file mode 100644 index 0000000..914a6aa --- /dev/null +++ b/deployment/dsc/azshcihost/WindowsImageTools/1.0.7/WindowsImageTools.psm1 @@ -0,0 +1,42 @@ +#requires -RunAsAdministrator + +# Check for New-VHD +$VHDCmdlets = $true +if (-not (Get-Module -Name hyper-v -ListAvailable)) +{ + $VHDCmdlets = $false + Write-Warning -Message '[Module : WindowsImageTools] Hyper-V Module Not Installed: ' +} +if ([environment]::OSVersion.Version.Major -ge 10 -and +(Get-WindowsOptionalFeature -Online -FeatureName Microsoft-Hyper-V-Services).state -eq 'Disabled') +{ + $VHDCmdlets = $false + Write-Warning -Message '[Module : WindowsImageTools] Hyper-v Services on windows 10 not installed' +} + +if (-not ($VHDCmdlets)) +{ + Write-Warning -Message '[Module : WindowsImageTools] *-VHD cmdlets not avalible ' + Write-Warning -Message ' Loading WIN2VHD Class' + . $PSScriptRoot\Functions\Wim2VHDClass.ps1 + Write-Warning -Message ' Windows Image Update function not loaded' +} + +# Import Basic functions +. $PSScriptRoot\Functions\HelperFunctions.ps1 +. $PSScriptRoot\Functions\Convert-Wim2VHD.ps1 +. $PSScriptRoot\Functions\Initialize-VHDPartition.ps1 +. $PSScriptRoot\Functions\Set-VHDPartition.ps1 +. $PSScriptRoot\Functions\New-Unattend.ps1 + +if ($VHDCmdlets) #only import if depended functions avalible +{ + . $PSScriptRoot\Functions\New-WindowsImageToolsExample.ps1 + . $PSScriptRoot\Functions\Set-UpdateConfig.ps1 + . $PSScriptRoot\Functions\Add-UpdateImage.ps1 + . $PSScriptRoot\Functions\Update-WindowsImageWMF.ps1 + . $PSScriptRoot\Functions\Invoke-WindowsImageUpdate.ps1 + . $PSScriptRoot\Functions\Mount-VhdAndRunBlock.ps1 + . $PSScriptRoot\Functions\Invoke-CreateVmRunAndWait.ps1 + . $PSScriptRoot\Functions\Get-VhdPartitionStyle.ps1 +} diff --git a/deployment/dsc/azshcihost/cChoco/2.5.0.0/AppVeyor/AppVeyorBuild.ps1 b/deployment/dsc/azshcihost/cChoco/2.5.0.0/AppVeyor/AppVeyorBuild.ps1 new file mode 100644 index 0000000..16a67ee --- /dev/null +++ b/deployment/dsc/azshcihost/cChoco/2.5.0.0/AppVeyor/AppVeyorBuild.ps1 @@ -0,0 +1,32 @@ +# Copyright (c) 2017 Chocolatey Software, Inc. +# Copyright (c) 2013 - 2017 Lawrence Gripper & original authors/contributors from https://github.com/chocolatey/cChoco +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +#---------------------------------# +# Header # +#---------------------------------# +Write-Host 'Running AppVeyor build script' -ForegroundColor Yellow +Write-Host "ModuleName : $env:ModuleName" +Write-Host "Build version : $env:APPVEYOR_BUILD_VERSION" +Write-Host "Author : $env:APPVEYOR_REPO_COMMIT_AUTHOR" +Write-Host "Branch : $env:APPVEYOR_REPO_BRANCH" +Write-Host "Repo : $env:APPVEYOR_REPO_NAME" +Write-Host "PSModulePath :" + +$env:PSModulePath -split ';' + +#---------------------------------# +# BuildScript # +#---------------------------------# +Write-Host 'Nothing to build, skipping.....' diff --git a/deployment/dsc/azshcihost/cChoco/2.5.0.0/AppVeyor/AppVeyorDeploy.ps1 b/deployment/dsc/azshcihost/cChoco/2.5.0.0/AppVeyor/AppVeyorDeploy.ps1 new file mode 100644 index 0000000..5cb1715 --- /dev/null +++ b/deployment/dsc/azshcihost/cChoco/2.5.0.0/AppVeyor/AppVeyorDeploy.ps1 @@ -0,0 +1,45 @@ +# Copyright (c) 2017 Chocolatey Software, Inc. +# Copyright (c) 2013 - 2017 Lawrence Gripper & original authors/contributors from https://github.com/chocolatey/cChoco +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +#---------------------------------# +# Header # +#---------------------------------# +Write-Host 'Running AppVeyor deploy script' -ForegroundColor Yellow + +#---------------------------------# +# Update module manifest # +#---------------------------------# +Write-Host 'Creating new module manifest' + +$ModuleManifestPath = Join-Path -path "$pwd" -ChildPath ("$env:ModuleName"+'.psd1') +$ModuleManifest = Get-Content $ModuleManifestPath -Raw + +Write-Host "Updating module manifest to version: $env:APPVEYOR_BUILD_VERSION" +[regex]::replace($ModuleManifest,'(ModuleVersion = )(.*)',"`$1'$env:APPVEYOR_BUILD_VERSION'") | Out-File -LiteralPath $ModuleManifestPath + +#---------------------------------# +# Publish to PS Gallery # +#---------------------------------# + +if ( ($env:APPVEYOR_REPO_NAME -notmatch 'chocolatey') -or (!$env:APPVEYOR_REPO_TAG_NAME) ) +{ + Write-Host "Finished testing of branch: $env:APPVEYOR_REPO_BRANCH - Exiting" + exit; +} + +Write-Host "Publishing module to Powershell Gallery: " +Publish-Module -Name $env:ModuleName -NuGetApiKey $env:nugetKey + +Write-Host 'Done!' -ForegroundColor Green diff --git a/deployment/dsc/azshcihost/cChoco/2.5.0.0/AppVeyor/AppVeyorInstall.ps1 b/deployment/dsc/azshcihost/cChoco/2.5.0.0/AppVeyor/AppVeyorInstall.ps1 new file mode 100644 index 0000000..1ce33ea --- /dev/null +++ b/deployment/dsc/azshcihost/cChoco/2.5.0.0/AppVeyor/AppVeyorInstall.ps1 @@ -0,0 +1,57 @@ +# Copyright (c) 2017 Chocolatey Software, Inc. +# Copyright (c) 2013 - 2017 Lawrence Gripper & original authors/contributors from https://github.com/chocolatey/cChoco +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +#---------------------------------# +# Header # +#---------------------------------# +Write-Host 'Running AppVeyor install script' -ForegroundColor Yellow + +#---------------------------------# +# Enable TLS 1.2 # +#---------------------------------# +[System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor 3072 + +#---------------------------------# +# Install NuGet # +#---------------------------------# +Write-Host 'Installing NuGet PackageProvider' +$pkg = Install-PackageProvider -Name NuGet -Force -ErrorAction Stop +Write-Host "Installed NuGet version '$($pkg.version)'" + +#---------------------------------# +# Install Modules # +#---------------------------------# +[version]$ScriptAnalyzerVersion = '1.8.1' +[version]$PesterVersion = '4.10.1' +Install-Module -Name 'PSScriptAnalyzer' -Repository PSGallery -Force -ErrorAction Stop -MaximumVersion $ScriptAnalyzerVersion +Install-Module -Name 'Pester' -SkipPublisherCheck -Repository PSGallery -Force -ErrorAction Stop -MaximumVersion $PesterVersion +Install-Module -Name 'xDSCResourceDesigner' -Repository PSGallery -Force -ErrorAction Stop + +#---------------------------------# +# Update PSModulePath # +#---------------------------------# +Write-Host 'Updating PSModulePath for DSC resource testing' +$env:PSModulePath = $env:PSModulePath + ";" + "C:\projects" + +#---------------------------------# +# Validate # +#---------------------------------# +$RequiredModules = 'PSScriptAnalyzer','Pester','xDSCResourceDesigner' +$InstalledModules = Get-Module -Name $RequiredModules -ListAvailable +if ( ($InstalledModules.count -lt $RequiredModules.Count) -or ($Null -eq $InstalledModules)) { + throw "Required modules are missing." +} else { + Write-Host 'All modules required found' -ForegroundColor Green +} diff --git a/deployment/dsc/azshcihost/cChoco/2.5.0.0/AppVeyor/AppVeyorTest.ps1 b/deployment/dsc/azshcihost/cChoco/2.5.0.0/AppVeyor/AppVeyorTest.ps1 new file mode 100644 index 0000000..01acf24 --- /dev/null +++ b/deployment/dsc/azshcihost/cChoco/2.5.0.0/AppVeyor/AppVeyorTest.ps1 @@ -0,0 +1,43 @@ +# Copyright (c) 2017 Chocolatey Software, Inc. +# Copyright (c) 2013 - 2017 Lawrence Gripper & original authors/contributors from https://github.com/chocolatey/cChoco +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +#---------------------------------# +# Header # +#---------------------------------# +Write-Host 'Running AppVeyor test script' -ForegroundColor Yellow +Write-Host "Current working directory: $pwd" + +#---------------------------------# +# Run Pester Tests # +#---------------------------------# +$resultsFile = '.\TestsResults.xml' +$testFiles = Get-ChildItem "$pwd\tests" | Where-Object {$_.FullName -match 'Tests.ps1$'} | Select-Object -ExpandProperty FullName +$results = Invoke-Pester -Script $testFiles -OutputFormat NUnitXml -OutputFile $resultsFile -PassThru + +Write-Host 'Uploading results' +try { + (New-Object 'System.Net.WebClient').UploadFile("https://ci.appveyor.com/api/testresults/nunit/$($env:APPVEYOR_JOB_ID)", (Resolve-Path $resultsFile)) +} catch { + throw "Upload failed." +} + +#---------------------------------# +# Validate # +#---------------------------------# +if (($results.FailedCount -gt 0) -or ($results.PassedCount -eq 0) -or ($null -eq $results)) { + throw "$($results.FailedCount) tests failed." +} else { + Write-Host 'All tests passed' -ForegroundColor Green +} diff --git a/deployment/dsc/azshcihost/cChoco/2.5.0.0/DSCResources/cChocoConfig/cChocoConfig.psm1 b/deployment/dsc/azshcihost/cChoco/2.5.0.0/DSCResources/cChocoConfig/cChocoConfig.psm1 new file mode 100644 index 0000000..bf0bd6e --- /dev/null +++ b/deployment/dsc/azshcihost/cChoco/2.5.0.0/DSCResources/cChocoConfig/cChocoConfig.psm1 @@ -0,0 +1,188 @@ +# Copyright (c) 2017 Chocolatey Software, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +<# +.Description +Returns the configuration for cChocoConfig. + +.Example +Get-TargetResource -ConfigName cacheLocation -Ensure 'Present' -Value 'c:\temp\choco' +#> +function Get-TargetResource +{ + [CmdletBinding()] + [OutputType([Hashtable])] + param + ( + [parameter(Mandatory = $true)] + [string] + $ConfigName, + + [ValidateSet('Present','Absent')] + [string] + $Ensure='Present', + + [parameter(Mandatory = $false)] + [string] + $Value + ) + + Write-Verbose "Starting cChocoConfig Get-TargetResource - Config Name: $ConfigName, Ensure: $Ensure" + + $returnValue = @{ + ConfigName = $ConfigName + Ensure = $Ensure + Value = $Value + } + + $returnValue + +} + +<# +.Description +Performs the set for the cChocoConfig resource. + +.Example +Set-TargetResource -ConfigName cacheLocation -Ensure 'Present' -Value 'c:\temp\choco' + +#> +function Set-TargetResource +{ + [CmdletBinding(SupportsShouldProcess=$true)] + param + ( + [parameter(Mandatory = $true)] + [string] + $ConfigName, + + [ValidateSet('Present','Absent')] + [string] + $Ensure='Present', + + [parameter(Mandatory = $false)] + [string] + $Value + ) + + + Write-Verbose "Starting cChocoConfig Set-TargetResource - Config Name: $ConfigName, Ensure: $Ensure" + + if ($pscmdlet.ShouldProcess("Choco config $ConfigName will be ensured $Ensure.")) + { + if ($Ensure -eq 'Present') + { + Write-Verbose "Setting choco config $ConfigName." + choco config set --name "'$ConfigName'" --value "'$Value'" + } + else + { + Write-Verbose "Unsetting choco config $ConfigName." + choco config unset --name "'$ConfigName'" + } + } + +} + +<# +.Description +Performs the test for cChocoFeature. + +.Example +Test-TargetResource -ConfigName cacheLocation -Ensure 'Present' -Value 'c:\temp\choco' +#> +function Test-TargetResource +{ + [CmdletBinding()] + [OutputType([Boolean])] + param + ( + [parameter(Mandatory = $true)] + [string] + $ConfigName, + + [ValidateSet('Present','Absent')] + [string] + $Ensure='Present', + + [parameter(Mandatory = $false)] + [string] + $Value + ) + + Write-Verbose "Starting cChocoConfig Test-TargetResource - Config Name: $ConfigName, Ensure: $Ensure." + + # validate value is given when ensure present + if ($Ensure -eq 'Present' -and (-not $PSBoundParameters.ContainsKey('Value') -or [String]::IsNullOrEmpty($Value))) { + throw "Missing parameter 'Value' when ensuring config is present!" + } + + if($env:ChocolateyInstall -eq "" -or $null -eq $env:ChocolateyInstall) + { + $command = Get-Command -Name choco.exe -ErrorAction SilentlyContinue + + if(!$command) { + throw "Unable to find choco.exe. Please make sure Chocolatey is installed correctly." + } + + $chocofolder = Split-Path $command.Source + + if( $chocofolder.EndsWith("bin") ) + { + $chocofolder = Split-Path $chocofolder + } + } + else + { + $chocofolder = $env:ChocolateyInstall + } + + if(!(Get-Item -Path $chocofolder -ErrorAction SilentlyContinue)) { + throw "Unable to find Chocolatey installation folder. Please make sure Chocolatey is installed and configured properly." + } + + $configfolder = Join-Path -Path $chocofolder -ChildPath "config" + $configfile = Get-ChildItem -Path $configfolder | Where-Object {$_.Name -match "chocolatey.config$"} + + if(!(Get-Item -Path $configfile.FullName -ErrorAction SilentlyContinue)) { + throw "Unable to find Chocolatey config file. Please make sure Chocolatey is installed and configured properly." + } + + # There is currently no choco command that only returns the settings in an CSV format. + # choco config list -r shows settings, sources, features and a note about API keys. + $xml = [xml](Get-Content -Path $configfile.FullName) + $settings = $xml.chocolatey.config.add + foreach($setting in $settings) + { + # If the config name matches and it should be present, check the value and + # if it matches it returns true. + if($setting.key -eq $ConfigName -and $Ensure -eq 'Present') + { + return ($setting.value -eq $Value) + } + # If the config name matches and it should be absent, check the value and + # if it is null or empty, return true + elseif($setting.key -eq $ConfigName -and $Ensure -eq 'Absent') + { + return ([String]::IsNullOrEmpty($setting.value)) + } + } + + # If we get this far, the configuraion item hasn't been found. + # There is currently no value, so return false if it should be present. + # True otherwise. + return !($Ensure -eq 'Present') +} + +Export-ModuleMember -Function *-TargetResource diff --git a/deployment/dsc/azshcihost/cChoco/2.5.0.0/DSCResources/cChocoConfig/cChocoConfig.schema.mof b/deployment/dsc/azshcihost/cChoco/2.5.0.0/DSCResources/cChocoConfig/cChocoConfig.schema.mof new file mode 100644 index 0000000..0838843 --- /dev/null +++ b/deployment/dsc/azshcihost/cChoco/2.5.0.0/DSCResources/cChocoConfig/cChocoConfig.schema.mof @@ -0,0 +1,9 @@ + +[ClassVersion("1.0.0.0"), FriendlyName("cChocoConfig")] +class cChocoConfig : OMI_BaseResource +{ + [Key] String ConfigName; + [Write,ValueMap{"Present", "Absent"},Values{"Present", "Absent"}] String Ensure; + [Write] String Value; +}; + diff --git a/deployment/dsc/azshcihost/cChoco/2.5.0.0/DSCResources/cChocoFeature/cChocoFeature.psm1 b/deployment/dsc/azshcihost/cChoco/2.5.0.0/DSCResources/cChocoFeature/cChocoFeature.psm1 new file mode 100644 index 0000000..716334f --- /dev/null +++ b/deployment/dsc/azshcihost/cChoco/2.5.0.0/DSCResources/cChocoFeature/cChocoFeature.psm1 @@ -0,0 +1,148 @@ +# Copyright (c) 2017 Chocolatey Software, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +<# +.Description +Returns the configuration for cChocoFeature. + +.Example +Get-TargetResource -FeatureName allowGlobalConfirmation -Ensure 'Present' +#> +function Get-TargetResource +{ + [CmdletBinding()] + [OutputType([System.Collections.Hashtable])] + param + ( + [parameter(Mandatory = $true)] + [System.String] + $FeatureName, + + [ValidateSet('Present','Absent')] + [System.String] + $Ensure='Present' + ) + + Write-Verbose "Starting cChocoFeature Get-TargetResource - Feature Name: $FeatureName, Ensure: $Ensure" + + $returnValue = @{ + FeatureName = $FeatureName + Ensure = $Ensure + } + + $returnValue + +} + +<# +.Description +Performs the set for the cChocoFeature resource. + +.Example +Get-TargetResource -FeatureName allowGlobalConfirmation -Ensure 'Present' + +#> +function Set-TargetResource +{ + [CmdletBinding(SupportsShouldProcess=$true)] + param + ( + [parameter(Mandatory = $true)] + [System.String] + $FeatureName, + + [ValidateSet('Present','Absent')] + [string] + $Ensure='Present' + ) + + + Write-Verbose "Starting cChocoFeature Set-TargetResource - Feature Name: $FeatureName, Ensure: $Ensure." + + if ($pscmdlet.ShouldProcess("Choco feature $FeatureName will be ensured $Ensure.")) + { + if ($Ensure -eq 'Present') + { + Write-Verbose "Enabling choco feature $FeatureName." + choco feature enable -n $FeatureName + } + else + { + Write-Verbose "Disabling choco feature $FeatureName." + choco feature disable -n $FeatureName + } + } + +} + +<# +.Description +Performs the test for cChocoFeature. + +.Example +Test-TargetResource -FeatureName allowGlobalConfirmation -Ensure 'Present' +#> +function Test-TargetResource +{ + [CmdletBinding()] + [OutputType([System.Boolean])] + param + ( + [parameter(Mandatory = $true)] + [System.String] + $FeatureName, + + [ValidateSet('Present','Absent')] + [System.String] + $Ensure='Present' + ) + + Write-Verbose "Starting cChocoFeature Test-TargetResource - Feature Name: $FeatureName, Ensure: $Ensure." + + $result = $false + $feature = Get-ChocoFeature -FeatureName $FeatureName | Where-Object {$_.State -eq "Enabled"} + + if (($Ensure -eq 'Present' -and ([bool]$feature)) -or ($Ensure -eq 'Absent' -and !([bool]$feature))) + { + Write-Verbose "Test-TargetResource is true, $FeatureName is $Ensure." + $result = $true + } + else + { + Write-Verbose "Test-TargetResource is false, $FeatureName is not $Ensure." + } + + return $result + +} + +<# +.Description +Query chocolatey features. +#> +function Get-ChocoFeature +{ + [OutputType([PSCustomObject])] + param( + [string] + $FeatureName + ) + choco feature -r | ConvertFrom-Csv -Delimiter "|" -Header Name, State, Description | Where-Object {$_.Name -eq $FeatureName} +} + + + + +Export-ModuleMember -Function *-TargetResource + diff --git a/deployment/dsc/azshcihost/cChoco/2.5.0.0/DSCResources/cChocoFeature/cChocoFeature.schema.mof b/deployment/dsc/azshcihost/cChoco/2.5.0.0/DSCResources/cChocoFeature/cChocoFeature.schema.mof new file mode 100644 index 0000000..6b1c53a --- /dev/null +++ b/deployment/dsc/azshcihost/cChoco/2.5.0.0/DSCResources/cChocoFeature/cChocoFeature.schema.mof @@ -0,0 +1,8 @@ + +[ClassVersion("1.0.0.0"), FriendlyName("cChocoFeature")] +class cChocoFeature : OMI_BaseResource +{ + [Key] String FeatureName; + [Write,ValueMap{"Present", "Absent"},Values{"Present", "Absent"}] String Ensure; +}; + diff --git a/deployment/dsc/azshcihost/cChoco/2.5.0.0/DSCResources/cChocoInstaller/cChocoInstaller.psm1 b/deployment/dsc/azshcihost/cChoco/2.5.0.0/DSCResources/cChocoInstaller/cChocoInstaller.psm1 new file mode 100644 index 0000000..85e3bb2 --- /dev/null +++ b/deployment/dsc/azshcihost/cChoco/2.5.0.0/DSCResources/cChocoInstaller/cChocoInstaller.psm1 @@ -0,0 +1,212 @@ +# Copyright (c) 2017 Chocolatey Software, Inc. +# Copyright (c) 2013 - 2017 Lawrence Gripper & original authors/contributors from https://github.com/chocolatey/cChoco +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +function Get-TargetResource +{ + [OutputType([hashtable])] + param + ( + [parameter(Mandatory)] + [ValidateNotNullOrEmpty()] + [string] + $InstallDir, + [parameter()] + [string] + $ChocoInstallScriptUrl = 'https://chocolatey.org/install.ps1' + ) + Write-Verbose 'Start Get-TargetResource' + + #Needs to return a hashtable that returns the current status of the configuration component + $Configuration = @{ + InstallDir = $env:ChocolateyInstall + ChocoInstallScriptUrl = $ChocoInstallScriptUrl + } + + Return $Configuration +} + +function Set-TargetResource +{ + [CmdletBinding(SupportsShouldProcess)] + param + ( + [parameter(Mandatory)] + [ValidateNotNullOrEmpty()] + [string] + $InstallDir, + + [parameter()] + [string] + $ChocoInstallScriptUrl = 'https://chocolatey.org/install.ps1' + ) + Write-Verbose 'Start Set-TargetResource' + $whatIfShouldProcess = $pscmdlet.ShouldProcess('Chocolatey', 'Download and Install') + if ($whatIfShouldProcess) { + Install-Chocolatey @PSBoundParameters + } +} + +function Test-TargetResource +{ + [OutputType([bool])] + param + ( + [parameter(Mandatory)] + [ValidateNotNullOrEmpty()] + [string] + $InstallDir, + [parameter()] + [string] + $ChocoInstallScriptUrl = 'https://chocolatey.org/install.ps1' + ) + + Write-Verbose 'Test-TargetResource' + if (-not (Test-ChocoInstalled)) + { + Write-Verbose 'Choco is not installed, calling set' + Return $false + } + + ##Test to see if the Install Directory is correct. + $env:ChocolateyInstall = [Environment]::GetEnvironmentVariable('ChocolateyInstall','Machine') + if(-not ($InstallDir -eq $env:ChocolateyInstall)) + { + Write-Verbose "Choco should be installed in $InstallDir but is installed to $env:ChocolateyInstall calling set" + Return $false + } + + Return $true +} + +function Test-ChocoInstalled +{ + Write-Verbose 'Test-ChocoInstalled' + $env:Path = [Environment]::GetEnvironmentVariable('Path','Machine') + + Write-Verbose "Env:Path contains: $env:Path" + if (Test-Command -command choco) + { + Write-Verbose 'YES - Choco is Installed' + return $true + } + + Write-Verbose "NO - Choco is not Installed" + return $false +} + +Function Test-Command +{ + Param ( + [string]$command = 'choco' + ) + Write-Verbose "Test-Command $command" + if (Get-Command -Name $command -ErrorAction SilentlyContinue) { + Write-Verbose "$command exists" + return $true + } else { + Write-Verbose "$command does NOT exist" + return $false + } +} + +#region - chocolately installer work arounds. Main issue is use of write-host +function global:Write-Host +{ + Param( + [Parameter(Mandatory,Position = 0)] + $Object, + [Switch] + $NoNewLine, + [ConsoleColor] + $ForegroundColor, + [ConsoleColor] + $BackgroundColor + ) + #Redirecting Write-Host -> Write-Verbose. + Write-Verbose $Object +} +#endregion + +function Get-FileDownload { + param ( + [parameter(Mandatory)] + [ValidateNotNullOrEmpty()] + [string]$url, + + [parameter(Mandatory)] + [ValidateNotNullOrEmpty()] + [string]$file + ) + # Set security protocol preference to avoid the download error if the machine has disabled TLS 1.0 and SSLv3 + # See: https://chocolatey.org/install (Installing With Restricted TLS section) + # Since cChoco requires at least PowerShell 4.0, we have .NET 4.5 available, so we can use [System.Net.SecurityProtocolType] enum values by name. + $securityProtocolSettingsOriginal = [System.Net.ServicePointManager]::SecurityProtocol + [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.SecurityProtocolType]::Tls12 -bor [System.Net.SecurityProtocolType]::Tls11 -bor [System.Net.SecurityProtocolType]::Tls -bor [System.Net.SecurityProtocolType]::Ssl3 + + Write-Verbose "Downloading $url to $file" + $downloader = new-object -TypeName System.Net.WebClient + $downloader.DownloadFile($url, $file) + + [System.Net.ServicePointManager]::SecurityProtocol = $securityProtocolSettingsOriginal +} + +Function Install-Chocolatey { + [CmdletBinding()] + param + ( + [parameter(Mandatory)] + [ValidateNotNullOrEmpty()] + [string] + $InstallDir, + + [parameter()] + [string] + $ChocoInstallScriptUrl = 'https://chocolatey.org/install.ps1' + ) + Write-Verbose 'Install-Chocolatey' + + #Create install directory if it does not exist + If(-not (Test-Path -Path $InstallDir)) { + Write-Verbose "[ChocoInstaller] Creating $InstallDir" + New-Item -Path $InstallDir -ItemType Directory + } + + #Set permanent EnvironmentVariable + Write-Verbose 'Setting ChocolateyInstall environment variables' + [Environment]::SetEnvironmentVariable('ChocolateyInstall', $InstallDir, [EnvironmentVariableTarget]::Machine) + $env:ChocolateyInstall = [Environment]::GetEnvironmentVariable('ChocolateyInstall','Machine') + Write-Verbose "Env:ChocolateyInstall has $env:ChocolateyInstall" + + #Download an execute install script + $tempPath = Join-Path -Path $env:TEMP -ChildPath ([GUID]::NewGuid().ToString()) + New-Item -Path $tempPath -ItemType Directory | Out-Null + $file = Join-Path -Path $tempPath -ChildPath 'install.ps1' + Get-FileDownload -url $ChocoInstallScriptUrl -file $file + . $file + + #refresh after install + Write-Verbose 'Adding Choco to path' + $env:Path = [Environment]::GetEnvironmentVariable('Path','Machine') + if ($env:path -notlike "*$InstallDir*") { + $env:Path += ";$InstallDir" + } + + Write-Verbose "Env:Path has $env:path" + #InstallChoco $InstallDir + $Null = Choco + Write-Verbose 'Finish InstallChoco' +} + +Export-ModuleMember -Function *-TargetResource diff --git a/deployment/dsc/azshcihost/cChoco/2.5.0.0/DSCResources/cChocoInstaller/cChocoInstaller.schema.mof b/deployment/dsc/azshcihost/cChoco/2.5.0.0/DSCResources/cChocoInstaller/cChocoInstaller.schema.mof new file mode 100644 index 0000000..aa9f332 --- /dev/null +++ b/deployment/dsc/azshcihost/cChoco/2.5.0.0/DSCResources/cChocoInstaller/cChocoInstaller.schema.mof @@ -0,0 +1,6 @@ +[ClassVersion("1.0.0"), FriendlyName("cChocoInstaller")] +class cChocoInstaller : OMI_BaseResource +{ + [Key] string InstallDir; + [Write] string ChocoInstallScriptUrl; +}; \ No newline at end of file diff --git a/deployment/dsc/azshcihost/cChoco/2.5.0.0/DSCResources/cChocoPackageInstall/cChocoPackageInstall.psm1 b/deployment/dsc/azshcihost/cChoco/2.5.0.0/DSCResources/cChocoPackageInstall/cChocoPackageInstall.psm1 new file mode 100644 index 0000000..3e9db12 --- /dev/null +++ b/deployment/dsc/azshcihost/cChoco/2.5.0.0/DSCResources/cChocoPackageInstall/cChocoPackageInstall.psm1 @@ -0,0 +1,553 @@ +# Copyright (c) 2017 Chocolatey Software, Inc. +# Copyright (c) 2013 - 2017 Lawrence Gripper & original authors/contributors from https://github.com/chocolatey/cChoco +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +function Get-TargetResource +{ + [OutputType([hashtable])] + param + ( + [parameter(Mandatory)] + [ValidateNotNullOrEmpty()] + [string] + $Name, + [ValidateNotNullOrEmpty()] + [string] + $Params, + [ValidateNotNullOrEmpty()] + [string] + $Version, + [ValidateNotNull()] + [string] + $MinimumVersion, + [ValidateNotNullOrEmpty()] + [string] + $Source + ) + + Write-Verbose -Message 'Start Get-TargetResource' + + if (-Not (Test-ChocoInstalled)) { + throw "cChocoPackageInstall requires Chocolatey to be installed, consider using cChocoInstaller with 'dependson' in dsc config" + } + + #Needs to return a hashtable that returns the current + #status of the configuration component + $Configuration = @{ + Name = $Name + Params = $Params + Version = $Version + Source = $Source + } + + return $Configuration +} + +function Set-TargetResource +{ + [CmdletBinding(SupportsShouldProcess)] + param + ( + [parameter(Mandatory)] + [ValidateNotNullOrEmpty()] + [string] + $Name, + [ValidateSet('Present','Absent')] + [string] + $Ensure='Present', + [ValidateNotNullOrEmpty()] + [string] + $Params, + [ValidateNotNullOrEmpty()] + [string] + $Version, + [ValidateNotNull()] + [string] + $MinimumVersion, + [string] + $Source, + [String] + $chocoParams, + [bool] + $AutoUpgrade = $false + ) + Write-Verbose -Message 'Start Set-TargetResource' + $isVersionPresent = $PSBoundParameters.ContainsKey('Version') + $isMinimumVersionPresent = $PSBoundParameters.ContainsKey('MinimumVersion') + if ($isVersionPresent -and $isMinimumVersionPresent ) { + throw "Cannot specify 'Version' and 'MinimumVersion' in the same configuration" + } + + if (-Not (Test-ChocoInstalled)) { + throw "cChocoPackageInstall requires Chocolatey to be installed, consider using cChocoInstaller with 'dependson' in dsc config" + } + + $isInstalled = IsPackageInstalled -pName $Name + + #Determine the correct package version to use get to desired state + if ($isVersionPresent -or $isMinimumVersionPresent) { + if ($isVersionPresent) { + $versionToInstall = $PSBoundParameters['Version'] + } + else { + $versionToInstall = $PSBoundParameters['MinimumVersion'] + } + } + + #Uninstall if Ensure is set to absent and the package is installed + if ($isInstalled) { + if ($Ensure -eq 'Absent') { + $whatIfShouldProcess = $pscmdlet.ShouldProcess("$Name", 'Remove Chocolatey package') + if ($whatIfShouldProcess) { + Write-Verbose -Message "Removing $Name as ensure is set to absent" + UninstallPackage -pName $Name -pParams $Params + } + } else { + $whatIfShouldProcess = $pscmdlet.ShouldProcess("$Name", 'Installing / upgrading package from Chocolatey') + if ($whatIfShouldProcess) { + if ($Version) { + Write-Verbose -Message "Uninstalling $Name due to version mis-match" + UninstallPackage -pName $Name -pParams $Params + Write-Verbose -Message "Re-Installing $Name with correct version $versionToInstall" + InstallPackage -pName $Name -pParams $Params -pVersion $versionToInstall -pSource $Source -cParams $chocoParams + } + elseif ($MinimumVersion) { + Write-Verbose -Message "Upgrading $Name because installed version is lower that the specified minimum" + $chocoParams += " --version='$versionToInstall'" + Upgrade-Package -pName $Name -pParams $Params -pSource $Source -cParams $chocoParams + } + elseif ($AutoUpgrade) { + Write-Verbose -Message "Upgrading $Name due to version mis-match" + Upgrade-Package -pName $Name -pParams $Params -pSource $Source -cParams $chocoParams + } + } + } + } else { + $whatIfShouldProcess = $pscmdlet.ShouldProcess("$Name", 'Install package from Chocolatey') + if ($whatIfShouldProcess) { + InstallPackage -pName $Name -pParams $Params -pVersion $versionToInstall -pSource $Source -cParams $chocoParams + } + } +} + +function Test-TargetResource +{ + [CmdletBinding(SupportsShouldProcess)] + [OutputType([bool])] + param + ( + [parameter(Mandatory)] + [ValidateNotNullOrEmpty()] + [string] + $Name, + [ValidateSet('Present','Absent')] + [string] + $Ensure='Present', + [ValidateNotNullOrEmpty()] + [string] + $Params, + [ValidateNotNullOrEmpty()] + [string] + $Version, + [ValidateNotNull()] + [string] + $MinimumVersion, + [string] + $Source, + [ValidateNotNullOrEmpty()] + [String] + $chocoParams, + [bool] + $AutoUpgrade = $false + ) + + Write-Verbose -Message 'Start Test-TargetResource' + $isVersionPresent = $PSBoundParameters.ContainsKey('Version') + $isMinimumVersionPresent = $PSBoundParameters.ContainsKey('MinimumVersion') + if ($isVersionPresent -and $isMinimumVersionPresent ) { + throw "Cannot specify 'Version' and 'MinimumVersion' in the same configuration" + } + + if (-Not (Test-ChocoInstalled)) { + return $false + } + + $isInstalled = IsPackageInstalled -pName $Name + + if ($ensure -eq 'Absent') { + if ($isInstalled -eq $false) { + return $true + } else { + return $false + } + } + + if ($version) { + Write-Verbose -Message "Checking if $Name is installed and if version matches $version" + $result = IsPackageInstalled -pName $Name -pVersion $Version + } + elseif ($MinimumVersion) { + Write-Verbose -Message "Checking if $Name is installed and version is $MinimumVersion or higher" + $result = IsPackageInstalled -pName $Name -pMinimumVersion $MinimumVersion + } + else { + Write-Verbose -Message "Checking if $Name is installed" + + if ($AutoUpgrade -and $isInstalled) { + $testParams = @{ + pName = $Name + } + if ($Source){ + $testParams.pSource = $Source + } + $result = Test-LatestVersionInstalled @testParams + } else { + $result = $isInstalled + } + } + + Return $result +} +function Test-ChocoInstalled +{ + Write-Verbose -Message 'Test-ChocoInstalled' + $env:Path = [Environment]::GetEnvironmentVariable('Path','Machine') + + Write-Verbose -Message "Env:Path contains: $env:Path" + if (Test-Command -command choco) + { + Write-Verbose -Message 'YES - Choco is Installed' + return $true + } + + Write-Verbose -Message 'NO - Choco is not Installed' + return $false +} + +Function Test-Command +{ + [CmdletBinding()] + [OutputType([bool])] + Param ( + [string]$command = 'choco' + ) + Write-Verbose -Message "Test-Command $command" + if (Get-Command -Name $command -ErrorAction SilentlyContinue) { + Write-Verbose -Message "$command exists" + return $true + } else { + Write-Verbose -Message "$command does NOT exist" + return $false + } +} + +function InstallPackage +{ + [Diagnostics.CodeAnalysis.SuppressMessage('PSAvoidUsingInvokeExpression','')] + param( + [Parameter(Position=0,Mandatory)] + [string]$pName, + [Parameter(Position=1)] + [string]$pParams, + [Parameter(Position=2)] + [string]$pVersion, + [Parameter(Position=3)] + [string]$pSource, + [Parameter(Position=4)] + [string]$cParams + ) + + $env:Path = [Environment]::GetEnvironmentVariable('Path','Machine') + + [string]$chocoParams = '-y' + if ($pParams) { + $chocoParams += " --params=`"$pParams`"" + } + if ($pVersion) { + $chocoParams += " --version=`"$pVersion`"" + } + if ($pSource) { + $chocoParams += " --source=`"$pSource`"" + } + if ($cParams) { + $chocoParams += " $cParams" + } + # Check if Chocolatey version is Greater than 0.10.4, and add --no-progress + if ((Get-ChocoVersion) -ge [System.Version]('0.10.4')){ + $chocoParams += " --no-progress" + } + + $cmd = "choco install $pName $chocoParams" + Write-Verbose -Message "Install command: '$cmd'" + $packageInstallOuput = Invoke-Expression -Command $cmd + Write-Verbose -Message "Package output $packageInstallOuput" + + # Clear Package Cache + Get-ChocoInstalledPackage -Purge + + #refresh path varaible in powershell, as choco doesn"t, to pull in git + $env:Path = [Environment]::GetEnvironmentVariable('Path','Machine') +} + +function UninstallPackage +{ + [Diagnostics.CodeAnalysis.SuppressMessage('PSAvoidUsingInvokeExpression','')] + param( + [Parameter(Position=0,Mandatory)] + [string]$pName, + [Parameter(Position=1)] + [string]$pParams + ) + + $env:Path = [Environment]::GetEnvironmentVariable('Path','Machine') + + [string]$chocoParams = "-y" + if ($pParams) { + $chocoParams += " --params=`"$pParams`"" + } + if ($pVersion) { + $chocoParams += " --version=`"$pVersion`"" + } + # Check if Chocolatey version is Greater than 0.10.4, and add --no-progress + if ((Get-ChocoVersion) -ge [System.Version]('0.10.4')){ + $chocoParams += " --no-progress" + } + + $cmd = "choco uninstall $pName $chocoParams" + Write-Verbose -Message "Uninstalling $pName with: '$cmd'" + $packageUninstallOuput = Invoke-Expression -Command $cmd + + Write-Verbose -Message "Package uninstall output $packageUninstallOuput " + + # Clear Package Cache + Get-ChocoInstalledPackage -Purge + + #refresh path varaible in powershell, as choco doesn"t, to pull in git + $env:Path = [Environment]::GetEnvironmentVariable('Path','Machine') +} + +function IsPackageInstalled +{ + [CmdletBinding(DefaultParameterSetName = 'RequiredVersion')] + [OutputType([Boolean])] + param( + [Parameter(Position=0, Mandatory)] + [string]$pName, + + [Parameter(ParameterSetName = 'RequiredVersion')] + [string]$pVersion, + + [Parameter(ParameterSetName = 'MinimumVersion')] + [string]$pMinimumVersion + ) + Write-Verbose -Message "Start IsPackageInstalled $pName" + + $env:Path = [Environment]::GetEnvironmentVariable('Path','Machine') + Write-Verbose -Message "Path variables: $($env:Path)" + + $installedPackages = Get-ChocoInstalledPackage + + if ($pVersion) { + Write-Verbose 'Comparing required version' + $installedPackages = $installedPackages | Where-Object { $_.Name -eq $pName -and $_.Version -eq $pVersion} + } + elseif ($pMinimumVersion) { + Write-Verbose 'Comparing minimum version' + # version comparison can be done with [System.Version] but this lacks the ability to compare pre-release versions + # because of this limitation MinimumVersion cannot be used in conjuction with pre-release packages + $pre = ($pMinimumVersion -split "-")[1] + if ($pre) { + throw "MinimumVersion does not support comparing pre-releases, please use Version parameter instead" + } + + $comparablePackages = $installedPackages | Where-Object { $_.Name -eq $pName} | ForEach-Object { + # as mentioned above we cant convert prerelease versions to [Sytem.Version] so we ignore anything after "-" + # leaving just the . seperated numeric version. this is loosely equivalent to "rounding down" + $parseableVersion = ($_.Version -split "-")[0] + $v = [System.Version]($parseableVersion) + $_ | Add-Member -MemberType NoteProperty -Name ComparableVersion -Value $v -PassThru + } + $installedPackages = $comparablePackages | Where-Object {$_.ComparableVersion -ge $pMinimumVersion} + } + else { + Write-Verbose "Finding packages -eq $pName" + $installedPackages = $installedPackages | Where-Object { $_.Name -eq $pName} + } + + $count = @($installedPackages).Count + Write-Verbose "Found $Count matching packages" + if ($Count -gt 0) + { + $installedPackages | ForEach-Object {Write-Verbose -Message "Found: $($_.Name) with version $($_.Version)"} + return $true + } + + return $false +} + +Function Test-LatestVersionInstalled { + [Diagnostics.CodeAnalysis.SuppressMessage('PSAvoidUsingInvokeExpression','')] + param( + [Parameter(Mandatory)] + [string]$pName, + [string]$pSource + ) + Write-Verbose -Message "Testing if $pName can be upgraded" + + [string]$chocoParams = '--noop' + if ($pSource) { + $chocoParams += " --source=`"$pSource`"" + } + + $cmd = "choco upgrade $pName $chocoParams" + Write-Verbose -Message "Testing if $pName can be upgraded: '$cmd'" + + $packageUpgradeOuput = Invoke-Expression -Command $cmd + $packageUpgradeOuput | ForEach-Object {Write-Verbose -Message $_} + + if ($packageUpgradeOuput -match "$pName.*is the latest version available based on your source") { + return $true + } + return $false +} + +##region - chocolately installer work arounds. Main issue is use of write-host +##attempting to work around the issues with Chocolatey calling Write-host in its scripts. +function global:Write-Host +{ + Param( + [Parameter(Mandatory, Position = 0)] + [Object] + $Object, + [Switch] + $NoNewLine, + [ConsoleColor] + $ForegroundColor, + [ConsoleColor] + $BackgroundColor + + ) + + #Override default Write-Host... + Write-Verbose -Message $Object +} + +Function Upgrade-Package { + [Diagnostics.CodeAnalysis.SuppressMessage('PSUseApprovedVerbs','')] + [Diagnostics.CodeAnalysis.SuppressMessage('PSAvoidUsingInvokeExpression','')] + param( + [Parameter(Position=0,Mandatory)] + [string]$pName, + [Parameter(Position=1)] + [string]$pParams, + [Parameter(Position=2)] + [string]$pSource, + [Parameter(Position=3)] + [string]$cParams + ) + + $env:Path = [Environment]::GetEnvironmentVariable('Path','Machine') + Write-Verbose -Message "Path variables: $($env:Path)" + + [string]$chocoParams = '-dv -y' + if ($pParams) { + $chocoParams += " --params=`"$pParams`"" + } + if ($pSource) { + $chocoParams += " --source=`"$pSource`"" + } + if ($cParams) { + $chocoParams += " $cParams" + } + # Check if Chocolatey version is Greater than 0.10.4, and add --no-progress + if ((Get-ChocoVersion) -ge [System.Version]('0.10.4')){ + $chocoParams += " --no-progress" + } + + $cmd = "choco upgrade $pName $chocoParams" + Write-Verbose -Message "Upgrade command: '$cmd'" + + if (-not (IsPackageInstalled -pName $pName)) + { + throw "$pName is not installed, you cannot upgrade" + } + + $packageUpgradeOuput = Invoke-Expression -Command $cmd + $packageUpgradeOuput | ForEach-Object { Write-Verbose -Message $_ } + + # Clear Package Cache + Get-ChocoInstalledPackage -Purge +} + +function Get-ChocoInstalledPackage { + [CmdletBinding()] + param ( + [switch]$Purge, + [switch]$NoCache + ) + + $ChocoInstallLP = Join-Path -Path $env:ChocolateyInstall -ChildPath 'cache' + if ( -not (Test-Path $ChocoInstallLP)){ + New-Item -Name 'cache' -Path $env:ChocolateyInstall -ItemType Directory | Out-Null + } + $ChocoInstallList = Join-Path -Path $ChocoInstallLP -ChildPath 'ChocoInstalled.xml' + + if ($Purge.IsPresent) { + Remove-Item $ChocoInstallList -Force + $res = $true + } else { + $PackageCacheSec = (Get-Date).AddSeconds('-60') + if ( $PackageCacheSec -lt (Get-Item $ChocoInstallList -ErrorAction SilentlyContinue).LastWriteTime ) { + $res = Import-Clixml $ChocoInstallList + } else { + $res = choco list -lo -r | ConvertFrom-Csv -Header 'Name', 'Version' -Delimiter "|" + if ( -not $NoCache){ + $res | Export-Clixml -Path $ChocoInstallList + } + } + } + + Return $res +} + +function Get-ChocoVersion { + [CmdletBinding()] + param ( + [switch]$Purge, + [switch]$NoCache + ) + + $chocoInstallCache = Join-Path -Path $env:ChocolateyInstall -ChildPath 'cache' + if ( -not (Test-Path $chocoInstallCache)){ + New-Item -Name 'cache' -Path $env:ChocolateyInstall -ItemType Directory | Out-Null + } + $chocoVersion = Join-Path -Path $chocoInstallCache -ChildPath 'ChocoVersion.xml' + + if ($Purge.IsPresent) { + Remove-Item $chocoVersion -Force + $res = $true + } else { + $cacheSec = (Get-Date).AddSeconds('-60') + if ( $cacheSec -lt (Get-Item $chocoVersion -ErrorAction SilentlyContinue).LastWriteTime ) { + $res = Import-Clixml $chocoVersion + } else { + $cmd = choco -v + $res = [System.Version]($cmd.Split('-')[0]) + $res | Export-Clixml -Path $chocoVersion + } + } + Return $res +} + +Export-ModuleMember -Function *-TargetResource diff --git a/deployment/dsc/azshcihost/cChoco/2.5.0.0/DSCResources/cChocoPackageInstall/cChocoPackageInstall.schema.mof b/deployment/dsc/azshcihost/cChoco/2.5.0.0/DSCResources/cChocoPackageInstall/cChocoPackageInstall.schema.mof new file mode 100644 index 0000000..e1938fc --- /dev/null +++ b/deployment/dsc/azshcihost/cChoco/2.5.0.0/DSCResources/cChocoPackageInstall/cChocoPackageInstall.schema.mof @@ -0,0 +1,12 @@ +[ClassVersion("1.0.1"), FriendlyName("cChocoPackageInstaller")] +class cChocoPackageInstall : OMI_BaseResource +{ + [Key] string Name; + [Write,ValueMap{"Present", "Absent"},Values{"Present", "Absent"}] String Ensure; + [write] string Params; + [write] string Version; + [write] string MinimumVersion; + [write] string Source; + [Write] String chocoParams; + [Write] Boolean AutoUpgrade; +}; diff --git a/deployment/dsc/azshcihost/cChoco/2.5.0.0/DSCResources/cChocoPackageInstallerSet/cChocoPackageInstallerSet.psd1 b/deployment/dsc/azshcihost/cChoco/2.5.0.0/DSCResources/cChocoPackageInstallerSet/cChocoPackageInstallerSet.psd1 new file mode 100644 index 0000000..758af2d --- /dev/null +++ b/deployment/dsc/azshcihost/cChoco/2.5.0.0/DSCResources/cChocoPackageInstallerSet/cChocoPackageInstallerSet.psd1 @@ -0,0 +1,95 @@ + +# +# Module manifest for module 'cChocoPackageInstallerSet' +# +# Generated on: 2016/05/11 +# + +@{ + +# Script module or binary module file associated with this manifest. +RootModule = 'cChocoPackageInstallerSet.schema.psm1' + +# Version number of this module. +ModuleVersion = '2.1.0.0' + +# ID used to uniquely identify this module +GUID = '028ba992-9429-4a6b-9c99-17eb4999cb23' + +# Author of this module +Author = 'Chocolatey Software, Lawrence Gripper, Javy de Koning' + +# Company or vendor of this module +CompanyName = 'Chocolatey Software, Inc' + +# Copyright statement for this module +Copyright = '(c) 2017 Chocolatey Software, Inc (c) 2013-2017 Lawrence Gripper, All rights reserved.' + +# Description of the functionality provided by this module +# Description = 'Allows install/uninstall of a group of choco packages.' + +# Minimum version of the Windows PowerShell engine required by this module +# PowerShellVersion = '' + +# Name of the Windows PowerShell host required by this module +# PowerShellHostName = '' + +# Minimum version of the Windows PowerShell host required by this module +# PowerShellHostVersion = '' + +# Minimum version of Microsoft .NET Framework required by this module +# DotNetFrameworkVersion = '' + +# Minimum version of the common language runtime (CLR) required by this module +# CLRVersion = '' + +# Processor architecture (None, X86, Amd64) required by this module +# ProcessorArchitecture = '' + +# Modules that must be imported into the global environment prior to importing this module +# RequiredModules = @() + +# Assemblies that must be loaded prior to importing this module +# RequiredAssemblies = @() + +# Script files (.ps1) that are run in the caller's environment prior to importing this module. +# ScriptsToProcess = @() + +# Type files (.ps1xml) to be loaded when importing this module +# TypesToProcess = @() + +# Format files (.ps1xml) to be loaded when importing this module +# FormatsToProcess = @() + +# Modules to import as nested modules of the module specified in RootModule/ModuleToProcess +# NestedModules = @() + +# Functions to export from this module +FunctionsToExport = '*' + +# Cmdlets to export from this module +CmdletsToExport = '*' + +# Variables to export from this module +VariablesToExport = '*' + +# Aliases to export from this module +AliasesToExport = '*' + +# List of all modules packaged with this module +# ModuleList = @() + +# List of all files packaged with this module +# FileList = @() + +# Private data to pass to the module specified in RootModule/ModuleToProcess +# PrivateData = '' + +# HelpInfo URI of this module +# HelpInfoURI = '' + +# Default prefix for commands exported from this module. Override the default prefix using Import-Module -Prefix. +# DefaultCommandPrefix = '' + +} + diff --git a/deployment/dsc/azshcihost/cChoco/2.5.0.0/DSCResources/cChocoPackageInstallerSet/cChocoPackageInstallerSet.schema.psm1 b/deployment/dsc/azshcihost/cChoco/2.5.0.0/DSCResources/cChocoPackageInstallerSet/cChocoPackageInstallerSet.schema.psm1 new file mode 100644 index 0000000..abfd486 --- /dev/null +++ b/deployment/dsc/azshcihost/cChoco/2.5.0.0/DSCResources/cChocoPackageInstallerSet/cChocoPackageInstallerSet.schema.psm1 @@ -0,0 +1,45 @@ +# Copyright (c) 2017 Chocolatey Software, Inc. +# Copyright (c) 2013 - 2017 Lawrence Gripper & original authors/contributors from https://github.com/chocolatey/cChoco +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +Configuration cChocoPackageInstallerSet +{ +<# +.SYNOPSIS +Composite DSC Resource allowing you to specify multiple choco packages in a single resource block. +#> + [CmdletBinding(SupportsShouldProcess=$true)] + param + ( + [parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String[]] + $Name, + [ValidateSet('Present','Absent')] + [System.String] + $Ensure='Present', + [parameter(Mandatory = $false)] + [ValidateNotNullOrEmpty()] + [System.String] + $Source + ) + + foreach ($pName in $Name) { + cChocoPackageInstaller "cChocoPackageInstaller_$($Ensure)_$($pName)" { + Ensure = $Ensure + Name = $pName + Source = $Source + } + } +} diff --git a/deployment/dsc/azshcihost/cChoco/2.5.0.0/DSCResources/cChocoSource/cChocoSource.psm1 b/deployment/dsc/azshcihost/cChoco/2.5.0.0/DSCResources/cChocoSource/cChocoSource.psm1 new file mode 100644 index 0000000..aae50e1 --- /dev/null +++ b/deployment/dsc/azshcihost/cChoco/2.5.0.0/DSCResources/cChocoSource/cChocoSource.psm1 @@ -0,0 +1,187 @@ +# Copyright (c) 2017 Chocolatey Software, Inc. +# Copyright (c) 2013 - 2017 Lawrence Gripper & original authors/contributors from https://github.com/chocolatey/cChoco +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +function Get-TargetResource +{ + [CmdletBinding()] + [OutputType([System.Collections.Hashtable])] + param + ( + [parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $Name, + [ValidateSet('Present','Absent')] + [System.String] + $Ensure='Present', + [parameter(Mandatory = $false)] + [UInt32] + $Priority, + [parameter(Mandatory = $false)] + [PSCredential] + $Credentials, + [parameter(Mandatory = $false)] + [System.String] + $Source + ) + + Write-Verbose "Start Get-TargetResource" + + #Needs to return a hashtable that returns the current + #status of the configuration component + $Configuration = @{ + Name = $Name + Priority = $Priority + Source = $Source + } + + return $Configuration +} + +function Set-TargetResource +{ + [CmdletBinding()] + param + ( + [parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $Name, + [ValidateSet('Present','Absent')] + [System.String] + $Ensure='Present', + [parameter(Mandatory = $false)] + [UInt32] + $Priority, + [parameter(Mandatory = $false)] + [PSCredential] + $Credentials, + [parameter(Mandatory = $false)] + [System.String] + $Source + ) + Write-Verbose "Start Set-TargetResource" + + if($Ensure -eq "Present") + { + if($Credentials -eq $null) + { + if($priority -eq $null) + { + choco sources add -n"$name" -s"$source" + } + else + { + choco sources add -n"$name" -s"$source" --priority=$priority + } + } + else + { + $username = $Credentials.UserName + $password = $Credentials.GetNetworkCredential().Password + + if($priority -eq $null) + { + choco sources add -n"$name" -s"$source" -u="$username" -p="$password" + } + else + { + choco sources add -n"$name" -s"$source" -u="$username" -p="$password" --priority=$priority + } + } + } + else + { + choco sources remove -n"$name" + } +} + +function Test-TargetResource +{ + [CmdletBinding()] + [OutputType([System.Boolean])] + param + ( + [parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $Name, + [ValidateSet('Present','Absent')] + [System.String] + $Ensure='Present', + [parameter(Mandatory = $false)] + [UInt32] + $Priority, + [parameter(Mandatory = $false)] + [PSCredential] + $Credentials, + [parameter(Mandatory = $false)] + [System.String] + $Source + ) + + Write-Verbose "Start Test-TargetResource" + + if($env:ChocolateyInstall -eq "" -or $env:ChocolateyInstall -eq $null) + { + $exe = (get-command choco).Source + $chocofolder = $exe.Substring(0,$exe.LastIndexOf("\")) + + if( $chocofolder.EndsWith("bin") ) + { + $chocofolder = $chocofolder.Substring(0,$chocofolder.LastIndexOf("\")) + } + } + else + { + $chocofolder = $env:ChocolateyInstall + } + $configfolder = "$chocofolder\config" + $configfile = Get-ChildItem $configfolder | Where-Object {$_.Name -match "chocolatey.config$"} + + $xml = [xml](Get-Content $configfile.FullName) + $sources = $xml.chocolatey.sources.source + + foreach($chocosource in $sources) + { + if($chocosource.id -eq $name -and $ensure -eq 'Present') + { + if ($chocosource.priority -eq $Priority) + { + return $true + } + else + { + return $false + } + } + elseif($chocosource.id -eq $name -and $ensure -eq 'Absent') + { + return $false + } + } + + if($Ensure -eq 'Present') + { + return $false + } + else + { + return $true + } +} + + +Export-ModuleMember -Function *-TargetResource diff --git a/deployment/dsc/azshcihost/cChoco/2.5.0.0/DSCResources/cChocoSource/cChocoSource.schema.mof b/deployment/dsc/azshcihost/cChoco/2.5.0.0/DSCResources/cChocoSource/cChocoSource.schema.mof new file mode 100644 index 0000000..20f608d --- /dev/null +++ b/deployment/dsc/azshcihost/cChoco/2.5.0.0/DSCResources/cChocoSource/cChocoSource.schema.mof @@ -0,0 +1,9 @@ +[ClassVersion("1.0.0"), FriendlyName("cChocoSource")] +class cChocoSource : OMI_BaseResource +{ + [Key] string Name; + [Write,ValueMap{"Present", "Absent"},Values{"Present", "Absent"}] String Ensure; + [write] string source; + [write,EmbeddedInstance("MSFT_Credential")] String Credentials; + [write] UInt32 Priority; +}; diff --git a/deployment/dsc/azshcihost/cChoco/2.5.0.0/ExampleConfig.ps1 b/deployment/dsc/azshcihost/cChoco/2.5.0.0/ExampleConfig.ps1 new file mode 100644 index 0000000..505e479 --- /dev/null +++ b/deployment/dsc/azshcihost/cChoco/2.5.0.0/ExampleConfig.ps1 @@ -0,0 +1,80 @@ +# Copyright (c) 2017 Chocolatey Software, Inc. +# Copyright (c) 2013 - 2017 Lawrence Gripper & original authors/contributors from https://github.com/chocolatey/cChoco +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +Configuration myChocoConfig +{ + Import-DscResource -Module cChoco + Node "localhost" + { + LocalConfigurationManager + { + DebugMode = 'ForceModuleImport' + } + cChocoInstaller installChoco + { + InstallDir = "c:\choco" + } + cChocoPackageInstaller installChrome + { + Name = "googlechrome" + DependsOn = "[cChocoInstaller]installChoco" + #This will automatically try to upgrade if available, only if a version is not explicitly specified. + AutoUpgrade = $True + } + cChocoPackageInstaller installAtomSpecificVersion + { + Name = "atom" + Version = "0.155.0" + DependsOn = "[cChocoInstaller]installChoco" + } + cChocoPackageInstaller installGit + { + Ensure = 'Present' + Name = "git" + Params = "/Someparam " + DependsOn = "[cChocoInstaller]installChoco" + } + cChocoPackageInstaller noFlashAllowed + { + Ensure = 'Absent' + Name = "flashplayerplugin" + DependsOn = "[cChocoInstaller]installChoco" + } + cChocoPackageInstallerSet installSomeStuff + { + Ensure = 'Present' + Name = @( + "git" + "skype" + "7zip" + ) + DependsOn = "[cChocoInstaller]installChoco" + } + cChocoPackageInstallerSet stuffToBeRemoved + { + Ensure = 'Absent' + Name = @( + "vlc" + "ruby" + "adobeair" + ) + DependsOn = "[cChocoInstaller]installChoco" + } + } +} + +myChocoConfig + +Start-DscConfiguration .\myChocoConfig -wait -Verbose -force diff --git a/deployment/dsc/azshcihost/cChoco/2.5.0.0/Examples/cChocoConfigExample.ps1 b/deployment/dsc/azshcihost/cChoco/2.5.0.0/Examples/cChocoConfigExample.ps1 new file mode 100644 index 0000000..d0a3693 --- /dev/null +++ b/deployment/dsc/azshcihost/cChoco/2.5.0.0/Examples/cChocoConfigExample.ps1 @@ -0,0 +1,39 @@ +# Copyright (c) 2017 Chocolatey Software, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +configuration ChocoConfig { + + Import-DscResource -ModuleName cChoco + + Node 'localhost' { + + cChocoConfig webRequestTimeoutSeconds { + ConfigName = "webRequestTimeoutSeconds" + Ensure = 'Present' + Value = 30 + } + + cChocoConfig proxy { + ConfigName = "proxy" + Ensure = 'Absent' + } + } + +} + + +$config = ChocoConfig + +Start-DscConfiguration -Path $config.psparentpath -Wait -Verbose -Force diff --git a/deployment/dsc/azshcihost/cChoco/2.5.0.0/Examples/cChocoFeatureExample.ps1 b/deployment/dsc/azshcihost/cChoco/2.5.0.0/Examples/cChocoFeatureExample.ps1 new file mode 100644 index 0000000..355b363 --- /dev/null +++ b/deployment/dsc/azshcihost/cChoco/2.5.0.0/Examples/cChocoFeatureExample.ps1 @@ -0,0 +1,41 @@ +# Copyright (c) 2017 Chocolatey Software, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +configuration ChocoFeatures { + + Import-DscResource -ModuleName cChoco + + Node 'localhost' { + + cChocoFeature allowGlobalConfirmation { + + FeatureName = "allowGlobalConfirmation" + Ensure = 'Present' + + } + + cChocoFeature powershellHost { + + FeatureName = "powershellHost" + Ensure = 'Absent' + } + } + +} + + +$config = ChocoFeatures + +Start-DscConfiguration -Path $config.psparentpath -Wait -Verbose -Force diff --git a/deployment/dsc/azshcihost/cChoco/2.5.0.0/Examples/cChocoInstaller_cChocoInstallerExample.ps1 b/deployment/dsc/azshcihost/cChoco/2.5.0.0/Examples/cChocoInstaller_cChocoInstallerExample.ps1 new file mode 100644 index 0000000..2e687cc --- /dev/null +++ b/deployment/dsc/azshcihost/cChoco/2.5.0.0/Examples/cChocoInstaller_cChocoInstallerExample.ps1 @@ -0,0 +1,36 @@ +# Copyright (c) 2017 Chocolatey Software, Inc. +# Copyright (c) 2013 - 2017 Lawrence Gripper & original authors/contributors from https://github.com/chocolatey/cChoco +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +Configuration InstallChoco +{ + Import-DscResource -Module cChoco + Node "localhost" + { + cChocoInstaller InstallChoco + { + InstallDir = "c:\choco" + } + cChocoPackageInstaller installSkypeWithChocoParams + { + Name = 'skype' + Ensure = 'Present' + DependsOn = '[cChocoInstaller]installChoco' + } + } +} + +$config = InstallChoco + +Start-DscConfiguration -Path $config.psparentpath -Wait -Verbose -Force diff --git a/deployment/dsc/azshcihost/cChoco/2.5.0.0/Examples/cChocoInstaller_cChocoPackageInstallExample.ps1 b/deployment/dsc/azshcihost/cChoco/2.5.0.0/Examples/cChocoInstaller_cChocoPackageInstallExample.ps1 new file mode 100644 index 0000000..c811730 --- /dev/null +++ b/deployment/dsc/azshcihost/cChoco/2.5.0.0/Examples/cChocoInstaller_cChocoPackageInstallExample.ps1 @@ -0,0 +1,33 @@ +# Copyright (c) 2017 Chocolatey Software, Inc. +# Copyright (c) 2013 - 2017 Lawrence Gripper & original authors/contributors from https://github.com/chocolatey/cChoco +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +Configuration InstallChoco +{ + Import-DscResource -Module cChoco + Node "localhost" + { + cChocoPackageInstaller installSkypeWithChocoParams + { + Name = 'skype' + Ensure = 'Present' + AutoUpgrade = $True + Version = 7.35.0.101 + } + } +} + +$config = InstallChoco + +Start-DscConfiguration -Path $config.psparentpath -Wait -Verbose -Force diff --git a/deployment/dsc/azshcihost/cChoco/2.5.0.0/LICENSE b/deployment/dsc/azshcihost/cChoco/2.5.0.0/LICENSE new file mode 100644 index 0000000..895657b --- /dev/null +++ b/deployment/dsc/azshcihost/cChoco/2.5.0.0/LICENSE @@ -0,0 +1,174 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. \ No newline at end of file diff --git a/deployment/dsc/azshcihost/cChoco/2.5.0.0/NOTICE b/deployment/dsc/azshcihost/cChoco/2.5.0.0/NOTICE new file mode 100644 index 0000000..7e2bd53 --- /dev/null +++ b/deployment/dsc/azshcihost/cChoco/2.5.0.0/NOTICE @@ -0,0 +1,14 @@ + Copyright (c) 2017 Chocolatey Software, Inc. + Copyright (c) 2013 - 2017 Lawrence Gripper & original authors/contributors from https://github.com/chocolatey/cChoco + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/deployment/dsc/azshcihost/cChoco/2.5.0.0/PSGetModuleInfo.xml b/deployment/dsc/azshcihost/cChoco/2.5.0.0/PSGetModuleInfo.xml new file mode 100644 index 0000000..83a99ba Binary files /dev/null and b/deployment/dsc/azshcihost/cChoco/2.5.0.0/PSGetModuleInfo.xml differ diff --git a/deployment/dsc/azshcihost/cChoco/2.5.0.0/Tests/cChocoConfig_Tests.ps1 b/deployment/dsc/azshcihost/cChoco/2.5.0.0/Tests/cChocoConfig_Tests.ps1 new file mode 100644 index 0000000..8a5e897 --- /dev/null +++ b/deployment/dsc/azshcihost/cChoco/2.5.0.0/Tests/cChocoConfig_Tests.ps1 @@ -0,0 +1,105 @@ +# Copyright (c) 2017 Chocolatey Software, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +$ResourceName = ((Split-Path $MyInvocation.MyCommand.Path -Leaf) -split '_')[0] +$ResourceFile = (Get-DscResource -Name $ResourceName).Path + +$TestsPath = (split-path -path $MyInvocation.MyCommand.Path -Parent) +$ResourceFile = Get-ChildItem -Recurse $TestsPath\.. -File | Where-Object {$_.name -eq "$ResourceName.psm1"} + +Import-Module -Name $ResourceFile.FullName + + +#---------------------------------# +# Pester tests for cChocoConfig # +#---------------------------------# +Describe "Testing cChocoConfig" { + + Context "Test-TargetResource" { + + mock -ModuleName cChocoConfig -CommandName Get-Content -MockWith {'<?xml version="1.0" encoding="utf-8"?> +<chocolatey xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> + <config> + <add key="commandExecutionTimeoutSeconds" value="1339" description="Default timeout for command execution. for infinite (starting in 0.10.4)." /> + <add key="proxy" value="" description="Explicit proxy location. Available in 0.9.9.9+." /> + </config> + <sources> + <source id="chocolatey" value="https://chocolatey.org/api/v2/" disabled="false" bypassProxy="false" selfService="false" adminOnly="false" priority="0" /> + </sources> +</chocolatey>' + } -Verifiable + + it 'Test-TargetResource returns true when Present and Configured.' { + Test-TargetResource -ConfigName 'commandExecutionTimeoutSeconds' -Ensure 'Present' -Value '1339' | Should be $true + } + + it 'Test-TargetResource returns false when Present and Not configured' { + Test-TargetResource -ConfigName 'proxy' -Ensure 'Present' -Value 'http://myproxy.url' | Should be $false + } + + it 'Test-TargetResource returns false when Present and Unknown' { + Test-TargetResource -ConfigName 'MyParam' -Ensure 'Present' -Value 'MyValue' | Should be $false + } + + it 'Test-TargetResource throws when Present and no value' { + { Test-TargetResource -ConfigName 'MyParam' -Ensure 'Present' } | Should -Throw "Missing parameter 'Value' when ensuring config is present!" + } + + it 'Test-TargetResource throws when Present and no value' { + { Test-TargetResource -ConfigName 'MyParam' -Ensure 'Present' -Value '' } | Should -Throw "Missing parameter 'Value' when ensuring config is present!" + } + + it 'Test-TargetResource throws when Present and no value' { + { Test-TargetResource -ConfigName 'MyParam' -Ensure 'Present' -Value $null } | Should -Throw "Missing parameter 'Value' when ensuring config is present!" + } + + it 'Test-TargetResource returns false when Absent and Configured' { + Test-TargetResource -ConfigName 'commandExecutionTimeoutSeconds' -Ensure 'Absent' | Should be $false + } + + it 'Test-TargetResource returns true when Absent and Not configured' { + Test-TargetResource -ConfigName 'proxy' -Ensure 'Absent' | Should be $true + } + + it 'Test-TargetResource returns true when Absent and Unknown' { + Test-TargetResource -ConfigName 'MyParam' -Ensure 'Absent' | Should be $true + } + + } + + Context "Set-TargetResource" { + + InModuleScope -ModuleName cChocoConfig -ScriptBlock { + function choco {} + mock choco {} + } + + Set-TargetResource -ConfigName "TestConfig" -Ensure "Present" -Value "MyValue" + + it "Present - Should have called choco, with set" { + Assert-MockCalled -CommandName choco -ModuleName cChocoConfig -ParameterFilter { + $args -contains "'MyValue'" + } + } + + Set-TargetResource -ConfigName "TestConfig" -Ensure "Absent" + + it "Absent - Should have called choco, with unset" { + Assert-MockCalled -CommandName choco -ModuleName cChocoConfig -ParameterFilter { + $args -contains "unset" + } + } + } +} \ No newline at end of file diff --git a/deployment/dsc/azshcihost/cChoco/2.5.0.0/Tests/cChocoFeature_Tests.ps1 b/deployment/dsc/azshcihost/cChoco/2.5.0.0/Tests/cChocoFeature_Tests.ps1 new file mode 100644 index 0000000..4c67253 --- /dev/null +++ b/deployment/dsc/azshcihost/cChoco/2.5.0.0/Tests/cChocoFeature_Tests.ps1 @@ -0,0 +1,87 @@ +# Copyright (c) 2017 Chocolatey Software, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +$ResourceName = ((Split-Path $MyInvocation.MyCommand.Path -Leaf) -split '_')[0] +$ResourceFile = (Get-DscResource -Name $ResourceName).Path + +$TestsPath = (split-path -path $MyInvocation.MyCommand.Path -Parent) +$ResourceFile = Get-ChildItem -Recurse $TestsPath\.. -File | Where-Object {$_.name -eq "$ResourceName.psm1"} + +Import-Module -Name $ResourceFile.FullName + + +#---------------------------------# +# Pester tests for cChocoInstall # +#---------------------------------# +Describe "Testing cChocoFeature" { + + Context "Test-TargetResource" { + + mock -ModuleName cChocoFeature -CommandName Get-ChocoFeature -MockWith { + @([pscustomobject]@{ + Name = "allowGlobalConfirmation" + State = "Enabled" + Description = "blah" + }, + [pscustomobject]@{ + Name = "powershellhost" + State = "Disabled" + Description = "blah" + } )| Where-Object { $_.Name -eq $FeatureName } + } -Verifiable + + + it 'Test-TargetResource returns true when Present and Enabled.' { + Test-TargetResource -FeatureName 'allowGlobalConfirmation' -Ensure 'Present' | should be $true + } + + it 'Test-TargetResource returns false when Present and Disabled' { + Test-TargetResource -FeatureName 'powershellhost' -Ensure 'Present' | should be $false + } + + it 'Test-TargetResource returns false when Absent and Enabled' { + Test-TargetResource -FeatureName 'allowGlobalConfirmation' -Ensure 'Absent' | Should be $false + } + + it 'Test-TargetResource returns true when Absent and Disabled' { + Test-TargetResource -FeatureName 'powershellhost' -Ensure 'Absent' | should be $true + } + + } + + Context "Set-TargetResource" { + + InModuleScope -ModuleName cChocoFeature -ScriptBlock { + function choco {} + mock choco {} + } + + Set-TargetResource -FeatureName "TestFeature" -Ensure "Present" + + it "Present - Should have called choco, with enable" { + Assert-MockCalled -CommandName choco -ModuleName cChocoFeature -ParameterFilter { + $args -contains "enable" + } + } + + Set-TargetResource -FeatureName "TestFeature" -Ensure "Absent" + + it "Absent - Should have called choco, with disable" { + Assert-MockCalled -CommandName choco -ModuleName cChocoFeature -ParameterFilter { + $args -contains "disable" + } + } + } +} \ No newline at end of file diff --git a/deployment/dsc/azshcihost/cChoco/2.5.0.0/Tests/cChocoInstaller_Tests.ps1 b/deployment/dsc/azshcihost/cChoco/2.5.0.0/Tests/cChocoInstaller_Tests.ps1 new file mode 100644 index 0000000..3ce85f9 --- /dev/null +++ b/deployment/dsc/azshcihost/cChoco/2.5.0.0/Tests/cChocoInstaller_Tests.ps1 @@ -0,0 +1,28 @@ +# Copyright (c) 2017 Chocolatey Software, Inc. +# Copyright (c) 2013 - 2017 Lawrence Gripper & original authors/contributors from https://github.com/chocolatey/cChoco +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +#---------------------------------# +# Pester tests for cChocoInstall # +#---------------------------------# +$ResourceName = ((Split-Path $MyInvocation.MyCommand.Path -Leaf) -split '_')[0] +$ResourceFile = (Get-DscResource -Name $ResourceName).Path + +Describe "Testing $ResourceName loaded from $ResourceFile" { + Context “Testing 'Get-TargetResource'” { + It 'DummyTest $true should be $true' { + $true | Should Be $true + } + } +} diff --git a/deployment/dsc/azshcihost/cChoco/2.5.0.0/Tests/cChocoPackageInstall_Tests.ps1 b/deployment/dsc/azshcihost/cChoco/2.5.0.0/Tests/cChocoPackageInstall_Tests.ps1 new file mode 100644 index 0000000..97b954f --- /dev/null +++ b/deployment/dsc/azshcihost/cChoco/2.5.0.0/Tests/cChocoPackageInstall_Tests.ps1 @@ -0,0 +1,195 @@ +# Copyright (c) 2017 Chocolatey Software, Inc. +# Copyright (c) 2013 - 2017 Lawrence Gripper & original authors/contributors from https://github.com/chocolatey/cChoco +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +#----------------------------------------# +# Pester tests for cChocoPackageInstall # +#----------------------------------------# +$ResourceName = ((Split-Path -Path $MyInvocation.MyCommand.Path -Leaf) -split '_')[0] +$ResourceFile = (Get-DscResource -Name $ResourceName).Path + +$TestsPath = (split-path -path $MyInvocation.MyCommand.Path -Parent) +$ResourceFile = Get-ChildItem -Recurse $TestsPath\.. -File | Where-Object {$_.name -eq "$ResourceName.psm1"} + +Import-Module -Name $ResourceFile.FullName + +Describe -Name "Testing $ResourceName loaded from $ResourceFile" -Fixture { + Context -Name "Package is not installed" -Fixture { + Mock -CommandName 'Get-ChocoInstalledPackage' -ModuleName 'cChocoPackageInstall' -MockWith { + return [pscustomobject]@{ + 'Name' = 'NotGoogleChrome' + 'Version' = '1.0.0' + } + } + + $Scenario1 = @{ + Name = 'GoogleChrome' + Ensure = 'Present' + } + It -name "Test-TargetResource -ensure 'Present' should return False" -test { + Test-TargetResource @Scenario1 | Should Be $False + } + + $Scenario2 = @{ + Name = 'GoogleChrome' + Ensure = 'Absent' + } + It -name "Test-TargetResource -ensure 'Absent' should return True" -test { + Test-TargetResource @Scenario2 | Should Be $True + } + + $Scenario3 = @{ + Name = 'GoogleChrome' + Ensure = 'Absent' + Version = '1.0.0' + } + It -name "Test-TargetResource -ensure 'Absent' -version '1.0.0' should return True" -test { + Test-TargetResource @Scenario3 | Should Be $True + } + + $Scenario4 = @{ + Name = 'GoogleChrome' + Ensure = 'Absent' + AutoUpgrade = $True + } + It -name "Test-TargetResource -ensure 'Absent' -AutoUpgrade should return True" -test { + Test-TargetResource @Scenario4 | Should Be $True + } + + $Scenario5 = @{ + Name = 'GoogleChrome' + Ensure = 'Absent' + Version = '1.0' + AutoUpgrade = $True + } + It -name "Test-TargetResource -ensure 'Absent' -version '1.0.0' -AutoUpgrade should return True" -test { + Test-TargetResource @Scenario5 | Should Be $True + } + + $Scenario6 = @{ + Name = 'GoogleChrome' + Ensure = 'Absent' + MinimumVersion = '1.0' + } + It -name "Test-TargetResource -ensure 'Absent' -MinimumVersion '1.0' should return True" -test { + Test-TargetResource @Scenario6 | Should Be $True + } + + $Scenario7 = @{ + Name = 'GoogleChrome' + Ensure = 'Present' + MinimumVersion = '1.0' + } + It -name "Test-TargetResource -ensure 'Present' -MinimumVersion '1.0' should return False" -test { + Test-TargetResource @Scenario7 | Should Be $false + } + } + + Context -Name "Package is installed with version 1.0.0" -Fixture { + Mock -CommandName 'Get-ChocoInstalledPackage' -ModuleName 'cChocoPackageInstall' -MockWith { + return [pscustomobject]@{ + 'Name' = 'GoogleChrome' + 'Version' = '1.0.0' + } + } + + $Scenario1 = @{ + Name = 'GoogleChrome' + Ensure = 'Present' + } + It -name "Test-TargetResource -ensure 'Present' should return True" -test { + Test-TargetResource @Scenario1 | Should Be $True + } + + $Scenario2 = @{ + Name = 'GoogleChrome' + Ensure = 'Absent' + } + It -name "Test-TargetResource -ensure 'Absent' should return False" -test { + Test-TargetResource @Scenario2 | Should Be $False + } + + $Scenario3 = @{ + Name = 'GoogleChrome' + Ensure = 'Present' + Version = '1.0.0' + } + + It -name "Test-TargetResource -ensure 'Present' -version '1.0.0' should return True" -test { + Test-TargetResource @Scenario3 | Should Be $True + } + + $Scenario4 = @{ + Name = 'GoogleChrome' + Ensure = 'Present' + Version = '1.0.1' + } + + It -name "Test-TargetResource -ensure 'Present' -version '1.0.1' should return False" -test { + Test-TargetResource @Scenario4 | Should Be $False + } + + $Scenario5 = @{ + Name = 'GoogleChrome' + Ensure = 'Present' + MinimumVersion = '0.9.0' + } + + It -name "Test-TargetResource -ensure 'Present' -MinimumVersion '0.9.0' should return True" -test { + Test-TargetResource @Scenario5 | Should Be $true + } + + $Scenario6 = @{ + Name = 'GoogleChrome' + Ensure = 'Present' + MinimumVersion = '1.0.1' + } + + It -name "Test-TargetResource -ensure 'Present' -MinimumVersion '1.0.1' should return False" -test { + Test-TargetResource @Scenario6 | Should Be $false + } + } + + Context -Name "Package is installed with prerelease version 1.0.0-1" -Fixture { + Mock -CommandName 'Get-ChocoInstalledPackage' -ModuleName 'cChocoPackageInstall' -MockWith { + return [pscustomobject]@{ + 'Name' = 'GoogleChrome' + 'Version' = '1.0.0-1' + } + } + + $Scenario1 = @{ + Name = 'GoogleChrome' + Ensure = 'Present' + MinimumVersion = '0.9.0' + } + + It -name "Test-TargetResource -ensure 'Present' -MinimumVersion '0.9.0' should return True" -test { + Test-TargetResource @Scenario1 | Should Be $true + } + + $Scenario2 = @{ + Name = 'GoogleChrome' + Ensure = 'Present' + MinimumVersion = '1.0.1' + } + + It -name "Test-TargetResource -ensure 'Present' -MinimumVersion '1.0.1' should return False" -test { + Test-TargetResource @Scenario2 | Should Be $false + } + } +} + +#Clean-up +Remove-Module cChocoPackageInstall diff --git a/deployment/dsc/azshcihost/cChoco/2.5.0.0/Tests/cChoco_ScriptAnalyzerTests.ps1 b/deployment/dsc/azshcihost/cChoco/2.5.0.0/Tests/cChoco_ScriptAnalyzerTests.ps1 new file mode 100644 index 0000000..f8e1be9 --- /dev/null +++ b/deployment/dsc/azshcihost/cChoco/2.5.0.0/Tests/cChoco_ScriptAnalyzerTests.ps1 @@ -0,0 +1,46 @@ +# Copyright (c) 2017 Chocolatey Software, Inc. +# Copyright (c) 2013 - 2017 Lawrence Gripper & original authors/contributors from https://github.com/chocolatey/cChoco +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +#---------------------------------# +# PSScriptAnalyzer tests # +#---------------------------------# +$Rules = Get-ScriptAnalyzerRule + +#Only run on cChocoInstaller.psm1 for now as this is the only resource that has had code adjustments for PSScriptAnalyzer rules. +$Modules = Get-ChildItem “$PSScriptRoot\..\” -Filter ‘*.psm1’ -Recurse | Where-Object {$_.FullName -match '(cChocoInstaller|cChocoPackageInstall|cChocoFeature)\.psm1$'} + +#---------------------------------# +# Run Module tests (psm1) # +#---------------------------------# +if ($Modules.count -gt 0) { + Describe ‘Testing all Modules against default PSScriptAnalyzer rule-set’ { + foreach ($module in $modules) { + Context “Testing Module '$($module.FullName)'” { + foreach ($rule in $rules) { + It “passes the PSScriptAnalyzer Rule $rule“ { + $Failures = Invoke-ScriptAnalyzer -Path $module.FullName -IncludeRule $rule.RuleName + $FailuresCount = ($Failures | Measure-Object).Count + if ($FailuresCount -gt 0) { + $Failures | ForEach-Object { + Write-Warning "Script: $($_.ScriptName), Line $($_.Line), Message $($_.Message)" + } + } + $FailuresCount | Should Be 0 + } + } + } + } + } +} diff --git a/deployment/dsc/azshcihost/cChoco/2.5.0.0/Tests/cChoco_xDscResourceTests.ps1 b/deployment/dsc/azshcihost/cChoco/2.5.0.0/Tests/cChoco_xDscResourceTests.ps1 new file mode 100644 index 0000000..57a8f32 --- /dev/null +++ b/deployment/dsc/azshcihost/cChoco/2.5.0.0/Tests/cChoco_xDscResourceTests.ps1 @@ -0,0 +1,41 @@ +# Copyright (c) 2017 Chocolatey Software, Inc. +# Copyright (c) 2013 - 2017 Lawrence Gripper & original authors/contributors from https://github.com/chocolatey/cChoco +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +#---------------------------------# +# xDscResourceTests Pester # +#---------------------------------# +$DSC = Get-DscResource | Where-Object {$_.Module.Name -eq 'cChoco'} + +Describe 'Testing all DSC resources using xDscResource designer.' { + foreach ($Resource in $DSC) + { + if (-not ($Resource.ImplementedAs -eq 'Composite') ) { + $ResourceName = $Resource.ResourceType + $Mof = Get-ChildItem “$PSScriptRoot\..\” -Filter "$resourcename.schema.mof" -Recurse + + Context “Testing DscResource '$ResourceName' using Test-xDscResource” { + It 'Test-xDscResource should return $true' { + Test-xDscResource -Name $ResourceName | Should Be $true + } + } + + Context “Testing DscSchema '$ResourceName' using Test-xDscSchema” { + It 'Test-xDscSchema should return true' { + Test-xDscSchema -Path $Mof.FullName | Should Be $true + } + } + } + } +} diff --git a/deployment/dsc/azshcihost/cChoco/2.5.0.0/TestsResults.xml b/deployment/dsc/azshcihost/cChoco/2.5.0.0/TestsResults.xml new file mode 100644 index 0000000..75e90a3 --- /dev/null +++ b/deployment/dsc/azshcihost/cChoco/2.5.0.0/TestsResults.xml @@ -0,0 +1,313 @@ +<?xml version="1.0" encoding="utf-8" standalone="no"?> +<test-results xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="nunit_schema_2.5.xsd" name="Pester" total="172" errors="0" failures="0" not-run="0" inconclusive="0" ignored="0" skipped="0" invalid="0" date="2021-02-09" time="14:51:09"> + <environment clr-version="4.0.30319.42000" user-domain="APPVYR-WIN" cwd="C:\projects\cchoco" platform="Microsoft Windows Server 2012 R2 Datacenter|C:\windows|\Device\Harddisk0\Partition5" machine-name="APPVYR-WIN" nunit-version="2.5.8.0" os-version="6.3.9600" user="appveyor" /> + <culture-info current-culture="en-US" current-uiculture="en-US" /> + <test-suite type="TestFixture" name="Pester" executed="True" result="Success" success="True" time="43.6443" asserts="0" description="Pester"> + <results> + <test-suite type="TestFixture" name="C:\projects\cchoco\tests\cChocoConfig_Tests.ps1" executed="True" result="Success" success="True" time="5.3013" asserts="0" description="C:\projects\cchoco\tests\cChocoConfig_Tests.ps1"> + <results> + <test-suite type="TestFixture" name="Testing cChocoConfig" executed="True" result="Success" success="True" time="2.7657" asserts="0" description="Testing cChocoConfig"> + <results> + <test-suite type="TestFixture" name="Test-TargetResource" executed="True" result="Success" success="True" time="2.2105" asserts="0" description="Test-TargetResource"> + <results> + <test-case description="Test-TargetResource returns true when Present and Configured." name="Testing cChocoConfig.Test-TargetResource.Test-TargetResource returns true when Present and Configured." time="0.7493" asserts="0" success="True" result="Success" executed="True" /> + <test-case description="Test-TargetResource returns false when Present and Not configured" name="Testing cChocoConfig.Test-TargetResource.Test-TargetResource returns false when Present and Not configured" time="0.0117" asserts="0" success="True" result="Success" executed="True" /> + <test-case description="Test-TargetResource returns false when Present and Unknown" name="Testing cChocoConfig.Test-TargetResource.Test-TargetResource returns false when Present and Unknown" time="0.012" asserts="0" success="True" result="Success" executed="True" /> + <test-case description="Test-TargetResource throws when Present and no value" name="Testing cChocoConfig.Test-TargetResource.Test-TargetResource throws when Present and no value" time="0.1154" asserts="0" success="True" result="Success" executed="True" /> + <test-case description="Test-TargetResource throws when Present and no value" name="Testing cChocoConfig.Test-TargetResource.Test-TargetResource throws when Present and no value" time="0.005" asserts="0" success="True" result="Success" executed="True" /> + <test-case description="Test-TargetResource throws when Present and no value" name="Testing cChocoConfig.Test-TargetResource.Test-TargetResource throws when Present and no value" time="0.0085" asserts="0" success="True" result="Success" executed="True" /> + <test-case description="Test-TargetResource returns false when Absent and Configured" name="Testing cChocoConfig.Test-TargetResource.Test-TargetResource returns false when Absent and Configured" time="0.0133" asserts="0" success="True" result="Success" executed="True" /> + <test-case description="Test-TargetResource returns true when Absent and Not configured" name="Testing cChocoConfig.Test-TargetResource.Test-TargetResource returns true when Absent and Not configured" time="0.0122" asserts="0" success="True" result="Success" executed="True" /> + <test-case description="Test-TargetResource returns true when Absent and Unknown" name="Testing cChocoConfig.Test-TargetResource.Test-TargetResource returns true when Absent and Unknown" time="0.0117" asserts="0" success="True" result="Success" executed="True" /> + </results> + </test-suite> + <test-suite type="TestFixture" name="Set-TargetResource" executed="True" result="Success" success="True" time="0.3048" asserts="0" description="Set-TargetResource"> + <results> + <test-case description="Present - Should have called choco, with set" name="Testing cChocoConfig.Set-TargetResource.Present - Should have called choco, with set" time="0.1563" asserts="0" success="True" result="Success" executed="True" /> + <test-case description="Absent - Should have called choco, with unset" name="Testing cChocoConfig.Set-TargetResource.Absent - Should have called choco, with unset" time="0.0175" asserts="0" success="True" result="Success" executed="True" /> + </results> + </test-suite> + </results> + </test-suite> + </results> + </test-suite> + <test-suite type="TestFixture" name="C:\projects\cchoco\tests\cChocoFeature_Tests.ps1" executed="True" result="Success" success="True" time="1.1879" asserts="0" description="C:\projects\cchoco\tests\cChocoFeature_Tests.ps1"> + <results> + <test-suite type="TestFixture" name="Testing cChocoFeature" executed="True" result="Success" success="True" time="0.4099" asserts="0" description="Testing cChocoFeature"> + <results> + <test-suite type="TestFixture" name="Test-TargetResource" executed="True" result="Success" success="True" time="0.2154" asserts="0" description="Test-TargetResource"> + <results> + <test-case description="Test-TargetResource returns true when Present and Enabled." name="Testing cChocoFeature.Test-TargetResource.Test-TargetResource returns true when Present and Enabled." time="0.0287" asserts="0" success="True" result="Success" executed="True" /> + <test-case description="Test-TargetResource returns false when Present and Disabled" name="Testing cChocoFeature.Test-TargetResource.Test-TargetResource returns false when Present and Disabled" time="0.0111" asserts="0" success="True" result="Success" executed="True" /> + <test-case description="Test-TargetResource returns false when Absent and Enabled" name="Testing cChocoFeature.Test-TargetResource.Test-TargetResource returns false when Absent and Enabled" time="0.01" asserts="0" success="True" result="Success" executed="True" /> + <test-case description="Test-TargetResource returns true when Absent and Disabled" name="Testing cChocoFeature.Test-TargetResource.Test-TargetResource returns true when Absent and Disabled" time="0.107" asserts="0" success="True" result="Success" executed="True" /> + </results> + </test-suite> + <test-suite type="TestFixture" name="Set-TargetResource" executed="True" result="Success" success="True" time="0.1472" asserts="0" description="Set-TargetResource"> + <results> + <test-case description="Present - Should have called choco, with enable" name="Testing cChocoFeature.Set-TargetResource.Present - Should have called choco, with enable" time="0.0097" asserts="0" success="True" result="Success" executed="True" /> + <test-case description="Absent - Should have called choco, with disable" name="Testing cChocoFeature.Set-TargetResource.Absent - Should have called choco, with disable" time="0.0584" asserts="0" success="True" result="Success" executed="True" /> + </results> + </test-suite> + </results> + </test-suite> + </results> + </test-suite> + <test-suite type="TestFixture" name="C:\projects\cchoco\tests\cChocoInstaller_Tests.ps1" executed="True" result="Success" success="True" time="0.9738" asserts="0" description="C:\projects\cchoco\tests\cChocoInstaller_Tests.ps1"> + <results> + <test-suite type="TestFixture" name="Testing cChocoInstaller loaded from C:\projects\cchoco\DSCResources\cChocoInstaller\cChocoInstaller.psm1" executed="True" result="Success" success="True" time="0.0409" asserts="0" description="Testing cChocoInstaller loaded from C:\projects\cchoco\DSCResources\cChocoInstaller\cChocoInstaller.psm1"> + <results> + <test-suite type="TestFixture" name="Testing 'Get-TargetResource'" executed="True" result="Success" success="True" time="0.0237" asserts="0" description="Testing 'Get-TargetResource'"> + <results> + <test-case description="DummyTest $true should be $true" name="Testing cChocoInstaller loaded from C:\projects\cchoco\DSCResources\cChocoInstaller\cChocoInstaller.psm1.Testing 'Get-TargetResource'.DummyTest $true should be $true" time="0.0031" asserts="0" success="True" result="Success" executed="True" /> + </results> + </test-suite> + </results> + </test-suite> + </results> + </test-suite> + <test-suite type="TestFixture" name="C:\projects\cchoco\tests\cChocoPackageInstall_Tests.ps1" executed="True" result="Success" success="True" time="1.5671" asserts="0" description="C:\projects\cchoco\tests\cChocoPackageInstall_Tests.ps1"> + <results> + <test-suite type="TestFixture" name="Testing cChocoPackageInstall loaded from cChocoPackageInstall.psm1" executed="True" result="Success" success="True" time="0.8224" asserts="0" description="Testing cChocoPackageInstall loaded from cChocoPackageInstall.psm1"> + <results> + <test-suite type="TestFixture" name="Package is not installed" executed="True" result="Success" success="True" time="0.3455" asserts="0" description="Package is not installed"> + <results> + <test-case description="Test-TargetResource -ensure 'Present' should return False" name="Testing cChocoPackageInstall loaded from cChocoPackageInstall.psm1.Package is not installed.Test-TargetResource -ensure 'Present' should return False" time="0.0937" asserts="0" success="True" result="Success" executed="True" /> + <test-case description="Test-TargetResource -ensure 'Absent' should return True" name="Testing cChocoPackageInstall loaded from cChocoPackageInstall.psm1.Package is not installed.Test-TargetResource -ensure 'Absent' should return True" time="0.0156" asserts="0" success="True" result="Success" executed="True" /> + <test-case description="Test-TargetResource -ensure 'Absent' -version '1.0.0' should return True" name="Testing cChocoPackageInstall loaded from cChocoPackageInstall.psm1.Package is not installed.Test-TargetResource -ensure 'Absent' -version '1.0.0' should return True" time="0.0202" asserts="0" success="True" result="Success" executed="True" /> + <test-case description="Test-TargetResource -ensure 'Absent' -AutoUpgrade should return True" name="Testing cChocoPackageInstall loaded from cChocoPackageInstall.psm1.Package is not installed.Test-TargetResource -ensure 'Absent' -AutoUpgrade should return True" time="0.0165" asserts="0" success="True" result="Success" executed="True" /> + <test-case description="Test-TargetResource -ensure 'Absent' -version '1.0.0' -AutoUpgrade should return True" name="Testing cChocoPackageInstall loaded from cChocoPackageInstall.psm1.Package is not installed.Test-TargetResource -ensure 'Absent' -version '1.0.0' -AutoUpgrade should return True" time="0.0143" asserts="0" success="True" result="Success" executed="True" /> + <test-case description="Test-TargetResource -ensure 'Absent' -MinimumVersion '1.0' should return True" name="Testing cChocoPackageInstall loaded from cChocoPackageInstall.psm1.Package is not installed.Test-TargetResource -ensure 'Absent' -MinimumVersion '1.0' should return True" time="0.0182" asserts="0" success="True" result="Success" executed="True" /> + <test-case description="Test-TargetResource -ensure 'Present' -MinimumVersion '1.0' should return False" name="Testing cChocoPackageInstall loaded from cChocoPackageInstall.psm1.Package is not installed.Test-TargetResource -ensure 'Present' -MinimumVersion '1.0' should return False" time="0.0841" asserts="0" success="True" result="Success" executed="True" /> + </results> + </test-suite> + <test-suite type="TestFixture" name="Package is installed with version 1.0.0" executed="True" result="Success" success="True" time="0.1934" asserts="0" description="Package is installed with version 1.0.0"> + <results> + <test-case description="Test-TargetResource -ensure 'Present' should return True" name="Testing cChocoPackageInstall loaded from cChocoPackageInstall.psm1.Package is installed with version 1.0.0.Test-TargetResource -ensure 'Present' should return True" time="0.0169" asserts="0" success="True" result="Success" executed="True" /> + <test-case description="Test-TargetResource -ensure 'Absent' should return False" name="Testing cChocoPackageInstall loaded from cChocoPackageInstall.psm1.Package is installed with version 1.0.0.Test-TargetResource -ensure 'Absent' should return False" time="0.0161" asserts="0" success="True" result="Success" executed="True" /> + <test-case description="Test-TargetResource -ensure 'Present' -version '1.0.0' should return True" name="Testing cChocoPackageInstall loaded from cChocoPackageInstall.psm1.Package is installed with version 1.0.0.Test-TargetResource -ensure 'Present' -version '1.0.0' should return True" time="0.0215" asserts="0" success="True" result="Success" executed="True" /> + <test-case description="Test-TargetResource -ensure 'Present' -version '1.0.1' should return False" name="Testing cChocoPackageInstall loaded from cChocoPackageInstall.psm1.Package is installed with version 1.0.0.Test-TargetResource -ensure 'Present' -version '1.0.1' should return False" time="0.0232" asserts="0" success="True" result="Success" executed="True" /> + <test-case description="Test-TargetResource -ensure 'Present' -MinimumVersion '0.9.0' should return True" name="Testing cChocoPackageInstall loaded from cChocoPackageInstall.psm1.Package is installed with version 1.0.0.Test-TargetResource -ensure 'Present' -MinimumVersion '0.9.0' should return True" time="0.0306" asserts="0" success="True" result="Success" executed="True" /> + <test-case description="Test-TargetResource -ensure 'Present' -MinimumVersion '1.0.1' should return False" name="Testing cChocoPackageInstall loaded from cChocoPackageInstall.psm1.Package is installed with version 1.0.0.Test-TargetResource -ensure 'Present' -MinimumVersion '1.0.1' should return False" time="0.026" asserts="0" success="True" result="Success" executed="True" /> + </results> + </test-suite> + <test-suite type="TestFixture" name="Package is installed with prerelease version 1.0.0-1" executed="True" result="Success" success="True" time="0.2461" asserts="0" description="Package is installed with prerelease version 1.0.0-1"> + <results> + <test-case description="Test-TargetResource -ensure 'Present' -MinimumVersion '0.9.0' should return True" name="Testing cChocoPackageInstall loaded from cChocoPackageInstall.psm1.Package is installed with prerelease version 1.0.0-1.Test-TargetResource -ensure 'Present' -MinimumVersion '0.9.0' should return True" time="0.0209" asserts="0" success="True" result="Success" executed="True" /> + <test-case description="Test-TargetResource -ensure 'Present' -MinimumVersion '1.0.1' should return False" name="Testing cChocoPackageInstall loaded from cChocoPackageInstall.psm1.Package is installed with prerelease version 1.0.0-1.Test-TargetResource -ensure 'Present' -MinimumVersion '1.0.1' should return False" time="0.0215" asserts="0" success="True" result="Success" executed="True" /> + </results> + </test-suite> + </results> + </test-suite> + </results> + </test-suite> + <test-suite type="TestFixture" name="C:\projects\cchoco\tests\cChoco_ScriptAnalyzerTests.ps1" executed="True" result="Success" success="True" time="25.4189" asserts="0" description="C:\projects\cchoco\tests\cChoco_ScriptAnalyzerTests.ps1"> + <results> + <test-suite type="TestFixture" name="Testing all Modules against default PSScriptAnalyzer rule-set" executed="True" result="Success" success="True" time="24.7865" asserts="0" description="Testing all Modules against default PSScriptAnalyzer rule-set"> + <results> + <test-suite type="TestFixture" name="Testing Module 'C:\projects\cchoco\DSCResources\cChocoFeature\cChocoFeature.psm1'" executed="True" result="Success" success="True" time="4.0562" asserts="0" description="Testing Module 'C:\projects\cchoco\DSCResources\cChocoFeature\cChocoFeature.psm1'"> + <results> + <test-case description="passes the PSScriptAnalyzer Rule PSAvoidUsingCmdletAliases" name="Testing all Modules against default PSScriptAnalyzer rule-set.Testing Module 'C:\projects\cchoco\DSCResources\cChocoFeature\cChocoFeature.psm1'.passes the PSScriptAnalyzer Rule PSAvoidUsingCmdletAliases" time="0.2417" asserts="0" success="True" result="Success" executed="True" /> + <test-case description="passes the PSScriptAnalyzer Rule PSAvoidDefaultValueSwitchParameter" name="Testing all Modules against default PSScriptAnalyzer rule-set.Testing Module 'C:\projects\cchoco\DSCResources\cChocoFeature\cChocoFeature.psm1'.passes the PSScriptAnalyzer Rule PSAvoidDefaultValueSwitchParameter" time="0.0472" asserts="0" success="True" result="Success" executed="True" /> + <test-case description="passes the PSScriptAnalyzer Rule PSAvoidDefaultValueForMandatoryParameter" name="Testing all Modules against default PSScriptAnalyzer rule-set.Testing Module 'C:\projects\cchoco\DSCResources\cChocoFeature\cChocoFeature.psm1'.passes the PSScriptAnalyzer Rule PSAvoidDefaultValueForMandatoryParameter" time="0.0442" asserts="0" success="True" result="Success" executed="True" /> + <test-case description="passes the PSScriptAnalyzer Rule PSAvoidUsingEmptyCatchBlock" name="Testing all Modules against default PSScriptAnalyzer rule-set.Testing Module 'C:\projects\cchoco\DSCResources\cChocoFeature\cChocoFeature.psm1'.passes the PSScriptAnalyzer Rule PSAvoidUsingEmptyCatchBlock" time="0.0463" asserts="0" success="True" result="Success" executed="True" /> + <test-case description="passes the PSScriptAnalyzer Rule PSAvoidGlobalVars" name="Testing all Modules against default PSScriptAnalyzer rule-set.Testing Module 'C:\projects\cchoco\DSCResources\cChocoFeature\cChocoFeature.psm1'.passes the PSScriptAnalyzer Rule PSAvoidGlobalVars" time="0.0436" asserts="0" success="True" result="Success" executed="True" /> + <test-case description="passes the PSScriptAnalyzer Rule PSAvoidInvokingEmptyMembers" name="Testing all Modules against default PSScriptAnalyzer rule-set.Testing Module 'C:\projects\cchoco\DSCResources\cChocoFeature\cChocoFeature.psm1'.passes the PSScriptAnalyzer Rule PSAvoidInvokingEmptyMembers" time="0.0444" asserts="0" success="True" result="Success" executed="True" /> + <test-case description="passes the PSScriptAnalyzer Rule PSAvoidNullOrEmptyHelpMessageAttribute" name="Testing all Modules against default PSScriptAnalyzer rule-set.Testing Module 'C:\projects\cchoco\DSCResources\cChocoFeature\cChocoFeature.psm1'.passes the PSScriptAnalyzer Rule PSAvoidNullOrEmptyHelpMessageAttribute" time="0.0491" asserts="0" success="True" result="Success" executed="True" /> + <test-case description="passes the PSScriptAnalyzer Rule PSAvoidUsingPositionalParameters" name="Testing all Modules against default PSScriptAnalyzer rule-set.Testing Module 'C:\projects\cchoco\DSCResources\cChocoFeature\cChocoFeature.psm1'.passes the PSScriptAnalyzer Rule PSAvoidUsingPositionalParameters" time="0.3065" asserts="0" success="True" result="Success" executed="True" /> + <test-case description="passes the PSScriptAnalyzer Rule PSReservedCmdletChar" name="Testing all Modules against default PSScriptAnalyzer rule-set.Testing Module 'C:\projects\cchoco\DSCResources\cChocoFeature\cChocoFeature.psm1'.passes the PSScriptAnalyzer Rule PSReservedCmdletChar" time="0.3055" asserts="0" success="True" result="Success" executed="True" /> + <test-case description="passes the PSScriptAnalyzer Rule PSReservedParams" name="Testing all Modules against default PSScriptAnalyzer rule-set.Testing Module 'C:\projects\cchoco\DSCResources\cChocoFeature\cChocoFeature.psm1'.passes the PSScriptAnalyzer Rule PSReservedParams" time="0.0458" asserts="0" success="True" result="Success" executed="True" /> + <test-case description="passes the PSScriptAnalyzer Rule PSAvoidShouldContinueWithoutForce" name="Testing all Modules against default PSScriptAnalyzer rule-set.Testing Module 'C:\projects\cchoco\DSCResources\cChocoFeature\cChocoFeature.psm1'.passes the PSScriptAnalyzer Rule PSAvoidShouldContinueWithoutForce" time="0.0449" asserts="0" success="True" result="Success" executed="True" /> + <test-case description="passes the PSScriptAnalyzer Rule PSAvoidUsingUserNameAndPassWordParams" name="Testing all Modules against default PSScriptAnalyzer rule-set.Testing Module 'C:\projects\cchoco\DSCResources\cChocoFeature\cChocoFeature.psm1'.passes the PSScriptAnalyzer Rule PSAvoidUsingUserNameAndPassWordParams" time="0.0572" asserts="0" success="True" result="Success" executed="True" /> + <test-case description="passes the PSScriptAnalyzer Rule PSAvoidUsingComputerNameHardcoded" name="Testing all Modules against default PSScriptAnalyzer rule-set.Testing Module 'C:\projects\cchoco\DSCResources\cChocoFeature\cChocoFeature.psm1'.passes the PSScriptAnalyzer Rule PSAvoidUsingComputerNameHardcoded" time="0.0544" asserts="0" success="True" result="Success" executed="True" /> + <test-case description="passes the PSScriptAnalyzer Rule PSAvoidUsingConvertToSecureStringWithPlainText" name="Testing all Modules against default PSScriptAnalyzer rule-set.Testing Module 'C:\projects\cchoco\DSCResources\cChocoFeature\cChocoFeature.psm1'.passes the PSScriptAnalyzer Rule PSAvoidUsingConvertToSecureStringWithPlainText" time="0.0676" asserts="0" success="True" result="Success" executed="True" /> + <test-case description="passes the PSScriptAnalyzer Rule PSAvoidUsingDeprecatedManifestFields" name="Testing all Modules against default PSScriptAnalyzer rule-set.Testing Module 'C:\projects\cchoco\DSCResources\cChocoFeature\cChocoFeature.psm1'.passes the PSScriptAnalyzer Rule PSAvoidUsingDeprecatedManifestFields" time="0.0757" asserts="0" success="True" result="Success" executed="True" /> + <test-case description="passes the PSScriptAnalyzer Rule PSAvoidUsingInvokeExpression" name="Testing all Modules against default PSScriptAnalyzer rule-set.Testing Module 'C:\projects\cchoco\DSCResources\cChocoFeature\cChocoFeature.psm1'.passes the PSScriptAnalyzer Rule PSAvoidUsingInvokeExpression" time="0.0562" asserts="0" success="True" result="Success" executed="True" /> + <test-case description="passes the PSScriptAnalyzer Rule PSAvoidUsingPlainTextForPassword" name="Testing all Modules against default PSScriptAnalyzer rule-set.Testing Module 'C:\projects\cchoco\DSCResources\cChocoFeature\cChocoFeature.psm1'.passes the PSScriptAnalyzer Rule PSAvoidUsingPlainTextForPassword" time="0.0576" asserts="0" success="True" result="Success" executed="True" /> + <test-case description="passes the PSScriptAnalyzer Rule PSAvoidUsingWMICmdlet" name="Testing all Modules against default PSScriptAnalyzer rule-set.Testing Module 'C:\projects\cchoco\DSCResources\cChocoFeature\cChocoFeature.psm1'.passes the PSScriptAnalyzer Rule PSAvoidUsingWMICmdlet" time="0.0461" asserts="0" success="True" result="Success" executed="True" /> + <test-case description="passes the PSScriptAnalyzer Rule PSAvoidUsingWriteHost" name="Testing all Modules against default PSScriptAnalyzer rule-set.Testing Module 'C:\projects\cchoco\DSCResources\cChocoFeature\cChocoFeature.psm1'.passes the PSScriptAnalyzer Rule PSAvoidUsingWriteHost" time="0.0473" asserts="0" success="True" result="Success" executed="True" /> + <test-case description="passes the PSScriptAnalyzer Rule PSMisleadingBacktick" name="Testing all Modules against default PSScriptAnalyzer rule-set.Testing Module 'C:\projects\cchoco\DSCResources\cChocoFeature\cChocoFeature.psm1'.passes the PSScriptAnalyzer Rule PSMisleadingBacktick" time="0.0415" asserts="0" success="True" result="Success" executed="True" /> + <test-case description="passes the PSScriptAnalyzer Rule PSMissingModuleManifestField" name="Testing all Modules against default PSScriptAnalyzer rule-set.Testing Module 'C:\projects\cchoco\DSCResources\cChocoFeature\cChocoFeature.psm1'.passes the PSScriptAnalyzer Rule PSMissingModuleManifestField" time="0.044" asserts="0" success="True" result="Success" executed="True" /> + <test-case description="passes the PSScriptAnalyzer Rule PSPossibleIncorrectComparisonWithNull" name="Testing all Modules against default PSScriptAnalyzer rule-set.Testing Module 'C:\projects\cchoco\DSCResources\cChocoFeature\cChocoFeature.psm1'.passes the PSScriptAnalyzer Rule PSPossibleIncorrectComparisonWithNull" time="0.0465" asserts="0" success="True" result="Success" executed="True" /> + <test-case description="passes the PSScriptAnalyzer Rule PSProvideCommentHelp" name="Testing all Modules against default PSScriptAnalyzer rule-set.Testing Module 'C:\projects\cchoco\DSCResources\cChocoFeature\cChocoFeature.psm1'.passes the PSScriptAnalyzer Rule PSProvideCommentHelp" time="0.301" asserts="0" success="True" result="Success" executed="True" /> + <test-case description="passes the PSScriptAnalyzer Rule PSUseApprovedVerbs" name="Testing all Modules against default PSScriptAnalyzer rule-set.Testing Module 'C:\projects\cchoco\DSCResources\cChocoFeature\cChocoFeature.psm1'.passes the PSScriptAnalyzer Rule PSUseApprovedVerbs" time="0.0511" asserts="0" success="True" result="Success" executed="True" /> + <test-case description="passes the PSScriptAnalyzer Rule PSUseBOMForUnicodeEncodedFile" name="Testing all Modules against default PSScriptAnalyzer rule-set.Testing Module 'C:\projects\cchoco\DSCResources\cChocoFeature\cChocoFeature.psm1'.passes the PSScriptAnalyzer Rule PSUseBOMForUnicodeEncodedFile" time="0.0693" asserts="0" success="True" result="Success" executed="True" /> + <test-case description="passes the PSScriptAnalyzer Rule PSUseCmdletCorrectly" name="Testing all Modules against default PSScriptAnalyzer rule-set.Testing Module 'C:\projects\cchoco\DSCResources\cChocoFeature\cChocoFeature.psm1'.passes the PSScriptAnalyzer Rule PSUseCmdletCorrectly" time="0.324" asserts="0" success="True" result="Success" executed="True" /> + <test-case description="passes the PSScriptAnalyzer Rule PSUseCompatibleCmdlets" name="Testing all Modules against default PSScriptAnalyzer rule-set.Testing Module 'C:\projects\cchoco\DSCResources\cChocoFeature\cChocoFeature.psm1'.passes the PSScriptAnalyzer Rule PSUseCompatibleCmdlets" time="0.0727" asserts="0" success="True" result="Success" executed="True" /> + <test-case description="passes the PSScriptAnalyzer Rule PSUseDeclaredVarsMoreThanAssignments" name="Testing all Modules against default PSScriptAnalyzer rule-set.Testing Module 'C:\projects\cchoco\DSCResources\cChocoFeature\cChocoFeature.psm1'.passes the PSScriptAnalyzer Rule PSUseDeclaredVarsMoreThanAssignments" time="0.0718" asserts="0" success="True" result="Success" executed="True" /> + <test-case description="passes the PSScriptAnalyzer Rule PSUseLiteralInitializerForHashtable" name="Testing all Modules against default PSScriptAnalyzer rule-set.Testing Module 'C:\projects\cchoco\DSCResources\cChocoFeature\cChocoFeature.psm1'.passes the PSScriptAnalyzer Rule PSUseLiteralInitializerForHashtable" time="0.0418" asserts="0" success="True" result="Success" executed="True" /> + <test-case description="passes the PSScriptAnalyzer Rule PSUseOutputTypeCorrectly" name="Testing all Modules against default PSScriptAnalyzer rule-set.Testing Module 'C:\projects\cchoco\DSCResources\cChocoFeature\cChocoFeature.psm1'.passes the PSScriptAnalyzer Rule PSUseOutputTypeCorrectly" time="0.0723" asserts="0" success="True" result="Success" executed="True" /> + <test-case description="passes the PSScriptAnalyzer Rule PSUsePSCredentialType" name="Testing all Modules against default PSScriptAnalyzer rule-set.Testing Module 'C:\projects\cchoco\DSCResources\cChocoFeature\cChocoFeature.psm1'.passes the PSScriptAnalyzer Rule PSUsePSCredentialType" time="0.049" asserts="0" success="True" result="Success" executed="True" /> + <test-case description="passes the PSScriptAnalyzer Rule PSShouldProcess" name="Testing all Modules against default PSScriptAnalyzer rule-set.Testing Module 'C:\projects\cchoco\DSCResources\cChocoFeature\cChocoFeature.psm1'.passes the PSScriptAnalyzer Rule PSShouldProcess" time="0.2451" asserts="0" success="True" result="Success" executed="True" /> + <test-case description="passes the PSScriptAnalyzer Rule PSUseShouldProcessForStateChangingFunctions" name="Testing all Modules against default PSScriptAnalyzer rule-set.Testing Module 'C:\projects\cchoco\DSCResources\cChocoFeature\cChocoFeature.psm1'.passes the PSScriptAnalyzer Rule PSUseShouldProcessForStateChangingFunctions" time="0.0794" asserts="0" success="True" result="Success" executed="True" /> + <test-case description="passes the PSScriptAnalyzer Rule PSUseSingularNouns" name="Testing all Modules against default PSScriptAnalyzer rule-set.Testing Module 'C:\projects\cchoco\DSCResources\cChocoFeature\cChocoFeature.psm1'.passes the PSScriptAnalyzer Rule PSUseSingularNouns" time="0.107" asserts="0" success="True" result="Success" executed="True" /> + <test-case description="passes the PSScriptAnalyzer Rule PSUseToExportFieldsInManifest" name="Testing all Modules against default PSScriptAnalyzer rule-set.Testing Module 'C:\projects\cchoco\DSCResources\cChocoFeature\cChocoFeature.psm1'.passes the PSScriptAnalyzer Rule PSUseToExportFieldsInManifest" time="0.0711" asserts="0" success="True" result="Success" executed="True" /> + <test-case description="passes the PSScriptAnalyzer Rule PSUseUTF8EncodingForHelpFile" name="Testing all Modules against default PSScriptAnalyzer rule-set.Testing Module 'C:\projects\cchoco\DSCResources\cChocoFeature\cChocoFeature.psm1'.passes the PSScriptAnalyzer Rule PSUseUTF8EncodingForHelpFile" time="0.0629" asserts="0" success="True" result="Success" executed="True" /> + <test-case description="passes the PSScriptAnalyzer Rule PSDSCDscExamplesPresent" name="Testing all Modules against default PSScriptAnalyzer rule-set.Testing Module 'C:\projects\cchoco\DSCResources\cChocoFeature\cChocoFeature.psm1'.passes the PSScriptAnalyzer Rule PSDSCDscExamplesPresent" time="0.0537" asserts="0" success="True" result="Success" executed="True" /> + <test-case description="passes the PSScriptAnalyzer Rule PSDSCDscTestsPresent" name="Testing all Modules against default PSScriptAnalyzer rule-set.Testing Module 'C:\projects\cchoco\DSCResources\cChocoFeature\cChocoFeature.psm1'.passes the PSScriptAnalyzer Rule PSDSCDscTestsPresent" time="0.0399" asserts="0" success="True" result="Success" executed="True" /> + <test-case description="passes the PSScriptAnalyzer Rule PSDSCReturnCorrectTypesForDSCFunctions" name="Testing all Modules against default PSScriptAnalyzer rule-set.Testing Module 'C:\projects\cchoco\DSCResources\cChocoFeature\cChocoFeature.psm1'.passes the PSScriptAnalyzer Rule PSDSCReturnCorrectTypesForDSCFunctions" time="0.0458" asserts="0" success="True" result="Success" executed="True" /> + <test-case description="passes the PSScriptAnalyzer Rule PSDSCUseIdenticalMandatoryParametersForDSC" name="Testing all Modules against default PSScriptAnalyzer rule-set.Testing Module 'C:\projects\cchoco\DSCResources\cChocoFeature\cChocoFeature.psm1'.passes the PSScriptAnalyzer Rule PSDSCUseIdenticalMandatoryParametersForDSC" time="0.0442" asserts="0" success="True" result="Success" executed="True" /> + <test-case description="passes the PSScriptAnalyzer Rule PSDSCUseIdenticalParametersForDSC" name="Testing all Modules against default PSScriptAnalyzer rule-set.Testing Module 'C:\projects\cchoco\DSCResources\cChocoFeature\cChocoFeature.psm1'.passes the PSScriptAnalyzer Rule PSDSCUseIdenticalParametersForDSC" time="0.0462" asserts="0" success="True" result="Success" executed="True" /> + <test-case description="passes the PSScriptAnalyzer Rule PSDSCStandardDSCFunctionsInResource" name="Testing all Modules against default PSScriptAnalyzer rule-set.Testing Module 'C:\projects\cchoco\DSCResources\cChocoFeature\cChocoFeature.psm1'.passes the PSScriptAnalyzer Rule PSDSCStandardDSCFunctionsInResource" time="0.0418" asserts="0" success="True" result="Success" executed="True" /> + <test-case description="passes the PSScriptAnalyzer Rule PSDSCUseVerboseMessageInDSCResource" name="Testing all Modules against default PSScriptAnalyzer rule-set.Testing Module 'C:\projects\cchoco\DSCResources\cChocoFeature\cChocoFeature.psm1'.passes the PSScriptAnalyzer Rule PSDSCUseVerboseMessageInDSCResource" time="0.0498" asserts="0" success="True" result="Success" executed="True" /> + </results> + </test-suite> + <test-suite type="TestFixture" name="Testing Module 'C:\projects\cchoco\DSCResources\cChocoInstaller\cChocoInstaller.psm1'" executed="True" result="Success" success="True" time="7.1488" asserts="0" description="Testing Module 'C:\projects\cchoco\DSCResources\cChocoInstaller\cChocoInstaller.psm1'"> + <results> + <test-case description="passes the PSScriptAnalyzer Rule PSAvoidUsingCmdletAliases" name="Testing all Modules against default PSScriptAnalyzer rule-set.Testing Module 'C:\projects\cchoco\DSCResources\cChocoInstaller\cChocoInstaller.psm1'.passes the PSScriptAnalyzer Rule PSAvoidUsingCmdletAliases" time="0.0449" asserts="0" success="True" result="Success" executed="True" /> + <test-case description="passes the PSScriptAnalyzer Rule PSAvoidDefaultValueSwitchParameter" name="Testing all Modules against default PSScriptAnalyzer rule-set.Testing Module 'C:\projects\cchoco\DSCResources\cChocoInstaller\cChocoInstaller.psm1'.passes the PSScriptAnalyzer Rule PSAvoidDefaultValueSwitchParameter" time="0.039" asserts="0" success="True" result="Success" executed="True" /> + <test-case description="passes the PSScriptAnalyzer Rule PSAvoidDefaultValueForMandatoryParameter" name="Testing all Modules against default PSScriptAnalyzer rule-set.Testing Module 'C:\projects\cchoco\DSCResources\cChocoInstaller\cChocoInstaller.psm1'.passes the PSScriptAnalyzer Rule PSAvoidDefaultValueForMandatoryParameter" time="0.038" asserts="0" success="True" result="Success" executed="True" /> + <test-case description="passes the PSScriptAnalyzer Rule PSAvoidUsingEmptyCatchBlock" name="Testing all Modules against default PSScriptAnalyzer rule-set.Testing Module 'C:\projects\cchoco\DSCResources\cChocoInstaller\cChocoInstaller.psm1'.passes the PSScriptAnalyzer Rule PSAvoidUsingEmptyCatchBlock" time="0.0446" asserts="0" success="True" result="Success" executed="True" /> + <test-case description="passes the PSScriptAnalyzer Rule PSAvoidGlobalVars" name="Testing all Modules against default PSScriptAnalyzer rule-set.Testing Module 'C:\projects\cchoco\DSCResources\cChocoInstaller\cChocoInstaller.psm1'.passes the PSScriptAnalyzer Rule PSAvoidGlobalVars" time="0.0393" asserts="0" success="True" result="Success" executed="True" /> + <test-case description="passes the PSScriptAnalyzer Rule PSAvoidInvokingEmptyMembers" name="Testing all Modules against default PSScriptAnalyzer rule-set.Testing Module 'C:\projects\cchoco\DSCResources\cChocoInstaller\cChocoInstaller.psm1'.passes the PSScriptAnalyzer Rule PSAvoidInvokingEmptyMembers" time="0.0586" asserts="0" success="True" result="Success" executed="True" /> + <test-case description="passes the PSScriptAnalyzer Rule PSAvoidNullOrEmptyHelpMessageAttribute" name="Testing all Modules against default PSScriptAnalyzer rule-set.Testing Module 'C:\projects\cchoco\DSCResources\cChocoInstaller\cChocoInstaller.psm1'.passes the PSScriptAnalyzer Rule PSAvoidNullOrEmptyHelpMessageAttribute" time="0.046" asserts="0" success="True" result="Success" executed="True" /> + <test-case description="passes the PSScriptAnalyzer Rule PSAvoidUsingPositionalParameters" name="Testing all Modules against default PSScriptAnalyzer rule-set.Testing Module 'C:\projects\cchoco\DSCResources\cChocoInstaller\cChocoInstaller.psm1'.passes the PSScriptAnalyzer Rule PSAvoidUsingPositionalParameters" time="1.4896" asserts="0" success="True" result="Success" executed="True" /> + <test-case description="passes the PSScriptAnalyzer Rule PSReservedCmdletChar" name="Testing all Modules against default PSScriptAnalyzer rule-set.Testing Module 'C:\projects\cchoco\DSCResources\cChocoInstaller\cChocoInstaller.psm1'.passes the PSScriptAnalyzer Rule PSReservedCmdletChar" time="0.3349" asserts="0" success="True" result="Success" executed="True" /> + <test-case description="passes the PSScriptAnalyzer Rule PSReservedParams" name="Testing all Modules against default PSScriptAnalyzer rule-set.Testing Module 'C:\projects\cchoco\DSCResources\cChocoInstaller\cChocoInstaller.psm1'.passes the PSScriptAnalyzer Rule PSReservedParams" time="0.0427" asserts="0" success="True" result="Success" executed="True" /> + <test-case description="passes the PSScriptAnalyzer Rule PSAvoidShouldContinueWithoutForce" name="Testing all Modules against default PSScriptAnalyzer rule-set.Testing Module 'C:\projects\cchoco\DSCResources\cChocoInstaller\cChocoInstaller.psm1'.passes the PSScriptAnalyzer Rule PSAvoidShouldContinueWithoutForce" time="0.0413" asserts="0" success="True" result="Success" executed="True" /> + <test-case description="passes the PSScriptAnalyzer Rule PSAvoidUsingUserNameAndPassWordParams" name="Testing all Modules against default PSScriptAnalyzer rule-set.Testing Module 'C:\projects\cchoco\DSCResources\cChocoInstaller\cChocoInstaller.psm1'.passes the PSScriptAnalyzer Rule PSAvoidUsingUserNameAndPassWordParams" time="0.0387" asserts="0" success="True" result="Success" executed="True" /> + <test-case description="passes the PSScriptAnalyzer Rule PSAvoidUsingComputerNameHardcoded" name="Testing all Modules against default PSScriptAnalyzer rule-set.Testing Module 'C:\projects\cchoco\DSCResources\cChocoInstaller\cChocoInstaller.psm1'.passes the PSScriptAnalyzer Rule PSAvoidUsingComputerNameHardcoded" time="0.0456" asserts="0" success="True" result="Success" executed="True" /> + <test-case description="passes the PSScriptAnalyzer Rule PSAvoidUsingConvertToSecureStringWithPlainText" name="Testing all Modules against default PSScriptAnalyzer rule-set.Testing Module 'C:\projects\cchoco\DSCResources\cChocoInstaller\cChocoInstaller.psm1'.passes the PSScriptAnalyzer Rule PSAvoidUsingConvertToSecureStringWithPlainText" time="0.0465" asserts="0" success="True" result="Success" executed="True" /> + <test-case description="passes the PSScriptAnalyzer Rule PSAvoidUsingDeprecatedManifestFields" name="Testing all Modules against default PSScriptAnalyzer rule-set.Testing Module 'C:\projects\cchoco\DSCResources\cChocoInstaller\cChocoInstaller.psm1'.passes the PSScriptAnalyzer Rule PSAvoidUsingDeprecatedManifestFields" time="0.0439" asserts="0" success="True" result="Success" executed="True" /> + <test-case description="passes the PSScriptAnalyzer Rule PSAvoidUsingInvokeExpression" name="Testing all Modules against default PSScriptAnalyzer rule-set.Testing Module 'C:\projects\cchoco\DSCResources\cChocoInstaller\cChocoInstaller.psm1'.passes the PSScriptAnalyzer Rule PSAvoidUsingInvokeExpression" time="0.0429" asserts="0" success="True" result="Success" executed="True" /> + <test-case description="passes the PSScriptAnalyzer Rule PSAvoidUsingPlainTextForPassword" name="Testing all Modules against default PSScriptAnalyzer rule-set.Testing Module 'C:\projects\cchoco\DSCResources\cChocoInstaller\cChocoInstaller.psm1'.passes the PSScriptAnalyzer Rule PSAvoidUsingPlainTextForPassword" time="0.0421" asserts="0" success="True" result="Success" executed="True" /> + <test-case description="passes the PSScriptAnalyzer Rule PSAvoidUsingWMICmdlet" name="Testing all Modules against default PSScriptAnalyzer rule-set.Testing Module 'C:\projects\cchoco\DSCResources\cChocoInstaller\cChocoInstaller.psm1'.passes the PSScriptAnalyzer Rule PSAvoidUsingWMICmdlet" time="0.0396" asserts="0" success="True" result="Success" executed="True" /> + <test-case description="passes the PSScriptAnalyzer Rule PSAvoidUsingWriteHost" name="Testing all Modules against default PSScriptAnalyzer rule-set.Testing Module 'C:\projects\cchoco\DSCResources\cChocoInstaller\cChocoInstaller.psm1'.passes the PSScriptAnalyzer Rule PSAvoidUsingWriteHost" time="0.04" asserts="0" success="True" result="Success" executed="True" /> + <test-case description="passes the PSScriptAnalyzer Rule PSMisleadingBacktick" name="Testing all Modules against default PSScriptAnalyzer rule-set.Testing Module 'C:\projects\cchoco\DSCResources\cChocoInstaller\cChocoInstaller.psm1'.passes the PSScriptAnalyzer Rule PSMisleadingBacktick" time="0.0584" asserts="0" success="True" result="Success" executed="True" /> + <test-case description="passes the PSScriptAnalyzer Rule PSMissingModuleManifestField" name="Testing all Modules against default PSScriptAnalyzer rule-set.Testing Module 'C:\projects\cchoco\DSCResources\cChocoInstaller\cChocoInstaller.psm1'.passes the PSScriptAnalyzer Rule PSMissingModuleManifestField" time="0.0458" asserts="0" success="True" result="Success" executed="True" /> + <test-case description="passes the PSScriptAnalyzer Rule PSPossibleIncorrectComparisonWithNull" name="Testing all Modules against default PSScriptAnalyzer rule-set.Testing Module 'C:\projects\cchoco\DSCResources\cChocoInstaller\cChocoInstaller.psm1'.passes the PSScriptAnalyzer Rule PSPossibleIncorrectComparisonWithNull" time="0.0451" asserts="0" success="True" result="Success" executed="True" /> + <test-case description="passes the PSScriptAnalyzer Rule PSProvideCommentHelp" name="Testing all Modules against default PSScriptAnalyzer rule-set.Testing Module 'C:\projects\cchoco\DSCResources\cChocoInstaller\cChocoInstaller.psm1'.passes the PSScriptAnalyzer Rule PSProvideCommentHelp" time="0.3017" asserts="0" success="True" result="Success" executed="True" /> + <test-case description="passes the PSScriptAnalyzer Rule PSUseApprovedVerbs" name="Testing all Modules against default PSScriptAnalyzer rule-set.Testing Module 'C:\projects\cchoco\DSCResources\cChocoInstaller\cChocoInstaller.psm1'.passes the PSScriptAnalyzer Rule PSUseApprovedVerbs" time="0.0794" asserts="0" success="True" result="Success" executed="True" /> + <test-case description="passes the PSScriptAnalyzer Rule PSUseBOMForUnicodeEncodedFile" name="Testing all Modules against default PSScriptAnalyzer rule-set.Testing Module 'C:\projects\cchoco\DSCResources\cChocoInstaller\cChocoInstaller.psm1'.passes the PSScriptAnalyzer Rule PSUseBOMForUnicodeEncodedFile" time="0.0805" asserts="0" success="True" result="Success" executed="True" /> + <test-case description="passes the PSScriptAnalyzer Rule PSUseCmdletCorrectly" name="Testing all Modules against default PSScriptAnalyzer rule-set.Testing Module 'C:\projects\cchoco\DSCResources\cChocoInstaller\cChocoInstaller.psm1'.passes the PSScriptAnalyzer Rule PSUseCmdletCorrectly" time="2.0537" asserts="0" success="True" result="Success" executed="True" /> + <test-case description="passes the PSScriptAnalyzer Rule PSUseCompatibleCmdlets" name="Testing all Modules against default PSScriptAnalyzer rule-set.Testing Module 'C:\projects\cchoco\DSCResources\cChocoInstaller\cChocoInstaller.psm1'.passes the PSScriptAnalyzer Rule PSUseCompatibleCmdlets" time="0.0685" asserts="0" success="True" result="Success" executed="True" /> + <test-case description="passes the PSScriptAnalyzer Rule PSUseDeclaredVarsMoreThanAssignments" name="Testing all Modules against default PSScriptAnalyzer rule-set.Testing Module 'C:\projects\cchoco\DSCResources\cChocoInstaller\cChocoInstaller.psm1'.passes the PSScriptAnalyzer Rule PSUseDeclaredVarsMoreThanAssignments" time="0.0598" asserts="0" success="True" result="Success" executed="True" /> + <test-case description="passes the PSScriptAnalyzer Rule PSUseLiteralInitializerForHashtable" name="Testing all Modules against default PSScriptAnalyzer rule-set.Testing Module 'C:\projects\cchoco\DSCResources\cChocoInstaller\cChocoInstaller.psm1'.passes the PSScriptAnalyzer Rule PSUseLiteralInitializerForHashtable" time="0.0548" asserts="0" success="True" result="Success" executed="True" /> + <test-case description="passes the PSScriptAnalyzer Rule PSUseOutputTypeCorrectly" name="Testing all Modules against default PSScriptAnalyzer rule-set.Testing Module 'C:\projects\cchoco\DSCResources\cChocoInstaller\cChocoInstaller.psm1'.passes the PSScriptAnalyzer Rule PSUseOutputTypeCorrectly" time="0.0874" asserts="0" success="True" result="Success" executed="True" /> + <test-case description="passes the PSScriptAnalyzer Rule PSUsePSCredentialType" name="Testing all Modules against default PSScriptAnalyzer rule-set.Testing Module 'C:\projects\cchoco\DSCResources\cChocoInstaller\cChocoInstaller.psm1'.passes the PSScriptAnalyzer Rule PSUsePSCredentialType" time="0.0576" asserts="0" success="True" result="Success" executed="True" /> + <test-case description="passes the PSScriptAnalyzer Rule PSShouldProcess" name="Testing all Modules against default PSScriptAnalyzer rule-set.Testing Module 'C:\projects\cchoco\DSCResources\cChocoInstaller\cChocoInstaller.psm1'.passes the PSScriptAnalyzer Rule PSShouldProcess" time="0.8316" asserts="0" success="True" result="Success" executed="True" /> + <test-case description="passes the PSScriptAnalyzer Rule PSUseShouldProcessForStateChangingFunctions" name="Testing all Modules against default PSScriptAnalyzer rule-set.Testing Module 'C:\projects\cchoco\DSCResources\cChocoInstaller\cChocoInstaller.psm1'.passes the PSScriptAnalyzer Rule PSUseShouldProcessForStateChangingFunctions" time="0.0403" asserts="0" success="True" result="Success" executed="True" /> + <test-case description="passes the PSScriptAnalyzer Rule PSUseSingularNouns" name="Testing all Modules against default PSScriptAnalyzer rule-set.Testing Module 'C:\projects\cchoco\DSCResources\cChocoInstaller\cChocoInstaller.psm1'.passes the PSScriptAnalyzer Rule PSUseSingularNouns" time="0.0421" asserts="0" success="True" result="Success" executed="True" /> + <test-case description="passes the PSScriptAnalyzer Rule PSUseToExportFieldsInManifest" name="Testing all Modules against default PSScriptAnalyzer rule-set.Testing Module 'C:\projects\cchoco\DSCResources\cChocoInstaller\cChocoInstaller.psm1'.passes the PSScriptAnalyzer Rule PSUseToExportFieldsInManifest" time="0.0421" asserts="0" success="True" result="Success" executed="True" /> + <test-case description="passes the PSScriptAnalyzer Rule PSUseUTF8EncodingForHelpFile" name="Testing all Modules against default PSScriptAnalyzer rule-set.Testing Module 'C:\projects\cchoco\DSCResources\cChocoInstaller\cChocoInstaller.psm1'.passes the PSScriptAnalyzer Rule PSUseUTF8EncodingForHelpFile" time="0.0396" asserts="0" success="True" result="Success" executed="True" /> + <test-case description="passes the PSScriptAnalyzer Rule PSDSCDscExamplesPresent" name="Testing all Modules against default PSScriptAnalyzer rule-set.Testing Module 'C:\projects\cchoco\DSCResources\cChocoInstaller\cChocoInstaller.psm1'.passes the PSScriptAnalyzer Rule PSDSCDscExamplesPresent" time="0.0388" asserts="0" success="True" result="Success" executed="True" /> + <test-case description="passes the PSScriptAnalyzer Rule PSDSCDscTestsPresent" name="Testing all Modules against default PSScriptAnalyzer rule-set.Testing Module 'C:\projects\cchoco\DSCResources\cChocoInstaller\cChocoInstaller.psm1'.passes the PSScriptAnalyzer Rule PSDSCDscTestsPresent" time="0.0421" asserts="0" success="True" result="Success" executed="True" /> + <test-case description="passes the PSScriptAnalyzer Rule PSDSCReturnCorrectTypesForDSCFunctions" name="Testing all Modules against default PSScriptAnalyzer rule-set.Testing Module 'C:\projects\cchoco\DSCResources\cChocoInstaller\cChocoInstaller.psm1'.passes the PSScriptAnalyzer Rule PSDSCReturnCorrectTypesForDSCFunctions" time="0.0423" asserts="0" success="True" result="Success" executed="True" /> + <test-case description="passes the PSScriptAnalyzer Rule PSDSCUseIdenticalMandatoryParametersForDSC" name="Testing all Modules against default PSScriptAnalyzer rule-set.Testing Module 'C:\projects\cchoco\DSCResources\cChocoInstaller\cChocoInstaller.psm1'.passes the PSScriptAnalyzer Rule PSDSCUseIdenticalMandatoryParametersForDSC" time="0.0446" asserts="0" success="True" result="Success" executed="True" /> + <test-case description="passes the PSScriptAnalyzer Rule PSDSCUseIdenticalParametersForDSC" name="Testing all Modules against default PSScriptAnalyzer rule-set.Testing Module 'C:\projects\cchoco\DSCResources\cChocoInstaller\cChocoInstaller.psm1'.passes the PSScriptAnalyzer Rule PSDSCUseIdenticalParametersForDSC" time="0.0444" asserts="0" success="True" result="Success" executed="True" /> + <test-case description="passes the PSScriptAnalyzer Rule PSDSCStandardDSCFunctionsInResource" name="Testing all Modules against default PSScriptAnalyzer rule-set.Testing Module 'C:\projects\cchoco\DSCResources\cChocoInstaller\cChocoInstaller.psm1'.passes the PSScriptAnalyzer Rule PSDSCStandardDSCFunctionsInResource" time="0.0432" asserts="0" success="True" result="Success" executed="True" /> + <test-case description="passes the PSScriptAnalyzer Rule PSDSCUseVerboseMessageInDSCResource" name="Testing all Modules against default PSScriptAnalyzer rule-set.Testing Module 'C:\projects\cchoco\DSCResources\cChocoInstaller\cChocoInstaller.psm1'.passes the PSScriptAnalyzer Rule PSDSCUseVerboseMessageInDSCResource" time="0.0385" asserts="0" success="True" result="Success" executed="True" /> + </results> + </test-suite> + <test-suite type="TestFixture" name="Testing Module 'C:\projects\cchoco\DSCResources\cChocoPackageInstall\cChocoPackageInstall.psm1'" executed="True" result="Success" success="True" time="13.5444" asserts="0" description="Testing Module 'C:\projects\cchoco\DSCResources\cChocoPackageInstall\cChocoPackageInstall.psm1'"> + <results> + <test-case description="passes the PSScriptAnalyzer Rule PSAvoidUsingCmdletAliases" name="Testing all Modules against default PSScriptAnalyzer rule-set.Testing Module 'C:\projects\cchoco\DSCResources\cChocoPackageInstall\cChocoPackageInstall.psm1'.passes the PSScriptAnalyzer Rule PSAvoidUsingCmdletAliases" time="0.0534" asserts="0" success="True" result="Success" executed="True" /> + <test-case description="passes the PSScriptAnalyzer Rule PSAvoidDefaultValueSwitchParameter" name="Testing all Modules against default PSScriptAnalyzer rule-set.Testing Module 'C:\projects\cchoco\DSCResources\cChocoPackageInstall\cChocoPackageInstall.psm1'.passes the PSScriptAnalyzer Rule PSAvoidDefaultValueSwitchParameter" time="0.0456" asserts="0" success="True" result="Success" executed="True" /> + <test-case description="passes the PSScriptAnalyzer Rule PSAvoidDefaultValueForMandatoryParameter" name="Testing all Modules against default PSScriptAnalyzer rule-set.Testing Module 'C:\projects\cchoco\DSCResources\cChocoPackageInstall\cChocoPackageInstall.psm1'.passes the PSScriptAnalyzer Rule PSAvoidDefaultValueForMandatoryParameter" time="0.053" asserts="0" success="True" result="Success" executed="True" /> + <test-case description="passes the PSScriptAnalyzer Rule PSAvoidUsingEmptyCatchBlock" name="Testing all Modules against default PSScriptAnalyzer rule-set.Testing Module 'C:\projects\cchoco\DSCResources\cChocoPackageInstall\cChocoPackageInstall.psm1'.passes the PSScriptAnalyzer Rule PSAvoidUsingEmptyCatchBlock" time="0.0433" asserts="0" success="True" result="Success" executed="True" /> + <test-case description="passes the PSScriptAnalyzer Rule PSAvoidGlobalVars" name="Testing all Modules against default PSScriptAnalyzer rule-set.Testing Module 'C:\projects\cchoco\DSCResources\cChocoPackageInstall\cChocoPackageInstall.psm1'.passes the PSScriptAnalyzer Rule PSAvoidGlobalVars" time="0.0637" asserts="0" success="True" result="Success" executed="True" /> + <test-case description="passes the PSScriptAnalyzer Rule PSAvoidInvokingEmptyMembers" name="Testing all Modules against default PSScriptAnalyzer rule-set.Testing Module 'C:\projects\cchoco\DSCResources\cChocoPackageInstall\cChocoPackageInstall.psm1'.passes the PSScriptAnalyzer Rule PSAvoidInvokingEmptyMembers" time="0.0794" asserts="0" success="True" result="Success" executed="True" /> + <test-case description="passes the PSScriptAnalyzer Rule PSAvoidNullOrEmptyHelpMessageAttribute" name="Testing all Modules against default PSScriptAnalyzer rule-set.Testing Module 'C:\projects\cchoco\DSCResources\cChocoPackageInstall\cChocoPackageInstall.psm1'.passes the PSScriptAnalyzer Rule PSAvoidNullOrEmptyHelpMessageAttribute" time="0.0832" asserts="0" success="True" result="Success" executed="True" /> + <test-case description="passes the PSScriptAnalyzer Rule PSAvoidUsingPositionalParameters" name="Testing all Modules against default PSScriptAnalyzer rule-set.Testing Module 'C:\projects\cchoco\DSCResources\cChocoPackageInstall\cChocoPackageInstall.psm1'.passes the PSScriptAnalyzer Rule PSAvoidUsingPositionalParameters" time="4.4079" asserts="0" success="True" result="Success" executed="True" /> + <test-case description="passes the PSScriptAnalyzer Rule PSReservedCmdletChar" name="Testing all Modules against default PSScriptAnalyzer rule-set.Testing Module 'C:\projects\cchoco\DSCResources\cChocoPackageInstall\cChocoPackageInstall.psm1'.passes the PSScriptAnalyzer Rule PSReservedCmdletChar" time="0.3085" asserts="0" success="True" result="Success" executed="True" /> + <test-case description="passes the PSScriptAnalyzer Rule PSReservedParams" name="Testing all Modules against default PSScriptAnalyzer rule-set.Testing Module 'C:\projects\cchoco\DSCResources\cChocoPackageInstall\cChocoPackageInstall.psm1'.passes the PSScriptAnalyzer Rule PSReservedParams" time="0.0693" asserts="0" success="True" result="Success" executed="True" /> + <test-case description="passes the PSScriptAnalyzer Rule PSAvoidShouldContinueWithoutForce" name="Testing all Modules against default PSScriptAnalyzer rule-set.Testing Module 'C:\projects\cchoco\DSCResources\cChocoPackageInstall\cChocoPackageInstall.psm1'.passes the PSScriptAnalyzer Rule PSAvoidShouldContinueWithoutForce" time="0.0576" asserts="0" success="True" result="Success" executed="True" /> + <test-case description="passes the PSScriptAnalyzer Rule PSAvoidUsingUserNameAndPassWordParams" name="Testing all Modules against default PSScriptAnalyzer rule-set.Testing Module 'C:\projects\cchoco\DSCResources\cChocoPackageInstall\cChocoPackageInstall.psm1'.passes the PSScriptAnalyzer Rule PSAvoidUsingUserNameAndPassWordParams" time="0.0485" asserts="0" success="True" result="Success" executed="True" /> + <test-case description="passes the PSScriptAnalyzer Rule PSAvoidUsingComputerNameHardcoded" name="Testing all Modules against default PSScriptAnalyzer rule-set.Testing Module 'C:\projects\cchoco\DSCResources\cChocoPackageInstall\cChocoPackageInstall.psm1'.passes the PSScriptAnalyzer Rule PSAvoidUsingComputerNameHardcoded" time="0.047" asserts="0" success="True" result="Success" executed="True" /> + <test-case description="passes the PSScriptAnalyzer Rule PSAvoidUsingConvertToSecureStringWithPlainText" name="Testing all Modules against default PSScriptAnalyzer rule-set.Testing Module 'C:\projects\cchoco\DSCResources\cChocoPackageInstall\cChocoPackageInstall.psm1'.passes the PSScriptAnalyzer Rule PSAvoidUsingConvertToSecureStringWithPlainText" time="0.0508" asserts="0" success="True" result="Success" executed="True" /> + <test-case description="passes the PSScriptAnalyzer Rule PSAvoidUsingDeprecatedManifestFields" name="Testing all Modules against default PSScriptAnalyzer rule-set.Testing Module 'C:\projects\cchoco\DSCResources\cChocoPackageInstall\cChocoPackageInstall.psm1'.passes the PSScriptAnalyzer Rule PSAvoidUsingDeprecatedManifestFields" time="0.0454" asserts="0" success="True" result="Success" executed="True" /> + <test-case description="passes the PSScriptAnalyzer Rule PSAvoidUsingInvokeExpression" name="Testing all Modules against default PSScriptAnalyzer rule-set.Testing Module 'C:\projects\cchoco\DSCResources\cChocoPackageInstall\cChocoPackageInstall.psm1'.passes the PSScriptAnalyzer Rule PSAvoidUsingInvokeExpression" time="0.0653" asserts="0" success="True" result="Success" executed="True" /> + <test-case description="passes the PSScriptAnalyzer Rule PSAvoidUsingPlainTextForPassword" name="Testing all Modules against default PSScriptAnalyzer rule-set.Testing Module 'C:\projects\cchoco\DSCResources\cChocoPackageInstall\cChocoPackageInstall.psm1'.passes the PSScriptAnalyzer Rule PSAvoidUsingPlainTextForPassword" time="0.0549" asserts="0" success="True" result="Success" executed="True" /> + <test-case description="passes the PSScriptAnalyzer Rule PSAvoidUsingWMICmdlet" name="Testing all Modules against default PSScriptAnalyzer rule-set.Testing Module 'C:\projects\cchoco\DSCResources\cChocoPackageInstall\cChocoPackageInstall.psm1'.passes the PSScriptAnalyzer Rule PSAvoidUsingWMICmdlet" time="0.0639" asserts="0" success="True" result="Success" executed="True" /> + <test-case description="passes the PSScriptAnalyzer Rule PSAvoidUsingWriteHost" name="Testing all Modules against default PSScriptAnalyzer rule-set.Testing Module 'C:\projects\cchoco\DSCResources\cChocoPackageInstall\cChocoPackageInstall.psm1'.passes the PSScriptAnalyzer Rule PSAvoidUsingWriteHost" time="0.0819" asserts="0" success="True" result="Success" executed="True" /> + <test-case description="passes the PSScriptAnalyzer Rule PSMisleadingBacktick" name="Testing all Modules against default PSScriptAnalyzer rule-set.Testing Module 'C:\projects\cchoco\DSCResources\cChocoPackageInstall\cChocoPackageInstall.psm1'.passes the PSScriptAnalyzer Rule PSMisleadingBacktick" time="0.0471" asserts="0" success="True" result="Success" executed="True" /> + <test-case description="passes the PSScriptAnalyzer Rule PSMissingModuleManifestField" name="Testing all Modules against default PSScriptAnalyzer rule-set.Testing Module 'C:\projects\cchoco\DSCResources\cChocoPackageInstall\cChocoPackageInstall.psm1'.passes the PSScriptAnalyzer Rule PSMissingModuleManifestField" time="0.0541" asserts="0" success="True" result="Success" executed="True" /> + <test-case description="passes the PSScriptAnalyzer Rule PSPossibleIncorrectComparisonWithNull" name="Testing all Modules against default PSScriptAnalyzer rule-set.Testing Module 'C:\projects\cchoco\DSCResources\cChocoPackageInstall\cChocoPackageInstall.psm1'.passes the PSScriptAnalyzer Rule PSPossibleIncorrectComparisonWithNull" time="0.0474" asserts="0" success="True" result="Success" executed="True" /> + <test-case description="passes the PSScriptAnalyzer Rule PSProvideCommentHelp" name="Testing all Modules against default PSScriptAnalyzer rule-set.Testing Module 'C:\projects\cchoco\DSCResources\cChocoPackageInstall\cChocoPackageInstall.psm1'.passes the PSScriptAnalyzer Rule PSProvideCommentHelp" time="0.3015" asserts="0" success="True" result="Success" executed="True" /> + <test-case description="passes the PSScriptAnalyzer Rule PSUseApprovedVerbs" name="Testing all Modules against default PSScriptAnalyzer rule-set.Testing Module 'C:\projects\cchoco\DSCResources\cChocoPackageInstall\cChocoPackageInstall.psm1'.passes the PSScriptAnalyzer Rule PSUseApprovedVerbs" time="0.0855" asserts="0" success="True" result="Success" executed="True" /> + <test-case description="passes the PSScriptAnalyzer Rule PSUseBOMForUnicodeEncodedFile" name="Testing all Modules against default PSScriptAnalyzer rule-set.Testing Module 'C:\projects\cchoco\DSCResources\cChocoPackageInstall\cChocoPackageInstall.psm1'.passes the PSScriptAnalyzer Rule PSUseBOMForUnicodeEncodedFile" time="0.0979" asserts="0" success="True" result="Success" executed="True" /> + <test-case description="passes the PSScriptAnalyzer Rule PSUseCmdletCorrectly" name="Testing all Modules against default PSScriptAnalyzer rule-set.Testing Module 'C:\projects\cchoco\DSCResources\cChocoPackageInstall\cChocoPackageInstall.psm1'.passes the PSScriptAnalyzer Rule PSUseCmdletCorrectly" time="5.0352" asserts="0" success="True" result="Success" executed="True" /> + <test-case description="passes the PSScriptAnalyzer Rule PSUseCompatibleCmdlets" name="Testing all Modules against default PSScriptAnalyzer rule-set.Testing Module 'C:\projects\cchoco\DSCResources\cChocoPackageInstall\cChocoPackageInstall.psm1'.passes the PSScriptAnalyzer Rule PSUseCompatibleCmdlets" time="0.0885" asserts="0" success="True" result="Success" executed="True" /> + <test-case description="passes the PSScriptAnalyzer Rule PSUseDeclaredVarsMoreThanAssignments" name="Testing all Modules against default PSScriptAnalyzer rule-set.Testing Module 'C:\projects\cchoco\DSCResources\cChocoPackageInstall\cChocoPackageInstall.psm1'.passes the PSScriptAnalyzer Rule PSUseDeclaredVarsMoreThanAssignments" time="0.0826" asserts="0" success="True" result="Success" executed="True" /> + <test-case description="passes the PSScriptAnalyzer Rule PSUseLiteralInitializerForHashtable" name="Testing all Modules against default PSScriptAnalyzer rule-set.Testing Module 'C:\projects\cchoco\DSCResources\cChocoPackageInstall\cChocoPackageInstall.psm1'.passes the PSScriptAnalyzer Rule PSUseLiteralInitializerForHashtable" time="0.0945" asserts="0" success="True" result="Success" executed="True" /> + <test-case description="passes the PSScriptAnalyzer Rule PSUseOutputTypeCorrectly" name="Testing all Modules against default PSScriptAnalyzer rule-set.Testing Module 'C:\projects\cchoco\DSCResources\cChocoPackageInstall\cChocoPackageInstall.psm1'.passes the PSScriptAnalyzer Rule PSUseOutputTypeCorrectly" time="0.0752" asserts="0" success="True" result="Success" executed="True" /> + <test-case description="passes the PSScriptAnalyzer Rule PSUsePSCredentialType" name="Testing all Modules against default PSScriptAnalyzer rule-set.Testing Module 'C:\projects\cchoco\DSCResources\cChocoPackageInstall\cChocoPackageInstall.psm1'.passes the PSScriptAnalyzer Rule PSUsePSCredentialType" time="0.0851" asserts="0" success="True" result="Success" executed="True" /> + <test-case description="passes the PSScriptAnalyzer Rule PSShouldProcess" name="Testing all Modules against default PSScriptAnalyzer rule-set.Testing Module 'C:\projects\cchoco\DSCResources\cChocoPackageInstall\cChocoPackageInstall.psm1'.passes the PSScriptAnalyzer Rule PSShouldProcess" time="0.8663" asserts="0" success="True" result="Success" executed="True" /> + <test-case description="passes the PSScriptAnalyzer Rule PSUseShouldProcessForStateChangingFunctions" name="Testing all Modules against default PSScriptAnalyzer rule-set.Testing Module 'C:\projects\cchoco\DSCResources\cChocoPackageInstall\cChocoPackageInstall.psm1'.passes the PSScriptAnalyzer Rule PSUseShouldProcessForStateChangingFunctions" time="0.056" asserts="0" success="True" result="Success" executed="True" /> + <test-case description="passes the PSScriptAnalyzer Rule PSUseSingularNouns" name="Testing all Modules against default PSScriptAnalyzer rule-set.Testing Module 'C:\projects\cchoco\DSCResources\cChocoPackageInstall\cChocoPackageInstall.psm1'.passes the PSScriptAnalyzer Rule PSUseSingularNouns" time="0.0589" asserts="0" success="True" result="Success" executed="True" /> + <test-case description="passes the PSScriptAnalyzer Rule PSUseToExportFieldsInManifest" name="Testing all Modules against default PSScriptAnalyzer rule-set.Testing Module 'C:\projects\cchoco\DSCResources\cChocoPackageInstall\cChocoPackageInstall.psm1'.passes the PSScriptAnalyzer Rule PSUseToExportFieldsInManifest" time="0.0539" asserts="0" success="True" result="Success" executed="True" /> + <test-case description="passes the PSScriptAnalyzer Rule PSUseUTF8EncodingForHelpFile" name="Testing all Modules against default PSScriptAnalyzer rule-set.Testing Module 'C:\projects\cchoco\DSCResources\cChocoPackageInstall\cChocoPackageInstall.psm1'.passes the PSScriptAnalyzer Rule PSUseUTF8EncodingForHelpFile" time="0.0489" asserts="0" success="True" result="Success" executed="True" /> + <test-case description="passes the PSScriptAnalyzer Rule PSDSCDscExamplesPresent" name="Testing all Modules against default PSScriptAnalyzer rule-set.Testing Module 'C:\projects\cchoco\DSCResources\cChocoPackageInstall\cChocoPackageInstall.psm1'.passes the PSScriptAnalyzer Rule PSDSCDscExamplesPresent" time="0.0489" asserts="0" success="True" result="Success" executed="True" /> + <test-case description="passes the PSScriptAnalyzer Rule PSDSCDscTestsPresent" name="Testing all Modules against default PSScriptAnalyzer rule-set.Testing Module 'C:\projects\cchoco\DSCResources\cChocoPackageInstall\cChocoPackageInstall.psm1'.passes the PSScriptAnalyzer Rule PSDSCDscTestsPresent" time="0.0465" asserts="0" success="True" result="Success" executed="True" /> + <test-case description="passes the PSScriptAnalyzer Rule PSDSCReturnCorrectTypesForDSCFunctions" name="Testing all Modules against default PSScriptAnalyzer rule-set.Testing Module 'C:\projects\cchoco\DSCResources\cChocoPackageInstall\cChocoPackageInstall.psm1'.passes the PSScriptAnalyzer Rule PSDSCReturnCorrectTypesForDSCFunctions" time="0.0473" asserts="0" success="True" result="Success" executed="True" /> + <test-case description="passes the PSScriptAnalyzer Rule PSDSCUseIdenticalMandatoryParametersForDSC" name="Testing all Modules against default PSScriptAnalyzer rule-set.Testing Module 'C:\projects\cchoco\DSCResources\cChocoPackageInstall\cChocoPackageInstall.psm1'.passes the PSScriptAnalyzer Rule PSDSCUseIdenticalMandatoryParametersForDSC" time="0.0576" asserts="0" success="True" result="Success" executed="True" /> + <test-case description="passes the PSScriptAnalyzer Rule PSDSCUseIdenticalParametersForDSC" name="Testing all Modules against default PSScriptAnalyzer rule-set.Testing Module 'C:\projects\cchoco\DSCResources\cChocoPackageInstall\cChocoPackageInstall.psm1'.passes the PSScriptAnalyzer Rule PSDSCUseIdenticalParametersForDSC" time="0.0454" asserts="0" success="True" result="Success" executed="True" /> + <test-case description="passes the PSScriptAnalyzer Rule PSDSCStandardDSCFunctionsInResource" name="Testing all Modules against default PSScriptAnalyzer rule-set.Testing Module 'C:\projects\cchoco\DSCResources\cChocoPackageInstall\cChocoPackageInstall.psm1'.passes the PSScriptAnalyzer Rule PSDSCStandardDSCFunctionsInResource" time="0.045" asserts="0" success="True" result="Success" executed="True" /> + <test-case description="passes the PSScriptAnalyzer Rule PSDSCUseVerboseMessageInDSCResource" name="Testing all Modules against default PSScriptAnalyzer rule-set.Testing Module 'C:\projects\cchoco\DSCResources\cChocoPackageInstall\cChocoPackageInstall.psm1'.passes the PSScriptAnalyzer Rule PSDSCUseVerboseMessageInDSCResource" time="0.0454" asserts="0" success="True" result="Success" executed="True" /> + </results> + </test-suite> + </results> + </test-suite> + </results> + </test-suite> + <test-suite type="TestFixture" name="C:\projects\cchoco\tests\cChoco_xDscResourceTests.ps1" executed="True" result="Success" success="True" time="9.1952" asserts="0" description="C:\projects\cchoco\tests\cChoco_xDscResourceTests.ps1"> + <results> + <test-suite type="TestFixture" name="Testing all DSC resources using xDscResource designer." executed="True" result="Success" success="True" time="7.6797" asserts="0" description="Testing all DSC resources using xDscResource designer."> + <results> + <test-suite type="TestFixture" name="Testing DscResource 'cChocoConfig' using Test-xDscResource" executed="True" result="Success" success="True" time="1.9147" asserts="0" description="Testing DscResource 'cChocoConfig' using Test-xDscResource"> + <results> + <test-case description="Test-xDscResource should return $true" name="Testing all DSC resources using xDscResource designer..Testing DscResource 'cChocoConfig' using Test-xDscResource.Test-xDscResource should return $true" time="1.8619" asserts="0" success="True" result="Success" executed="True" /> + </results> + </test-suite> + <test-suite type="TestFixture" name="Testing DscSchema 'cChocoConfig' using Test-xDscSchema" executed="True" result="Success" success="True" time="0.1559" asserts="0" description="Testing DscSchema 'cChocoConfig' using Test-xDscSchema"> + <results> + <test-case description="Test-xDscSchema should return true" name="Testing all DSC resources using xDscResource designer..Testing DscSchema 'cChocoConfig' using Test-xDscSchema.Test-xDscSchema should return true" time="0.1303" asserts="0" success="True" result="Success" executed="True" /> + </results> + </test-suite> + <test-suite type="TestFixture" name="Testing DscResource 'cChocoFeature' using Test-xDscResource" executed="True" result="Success" success="True" time="1.2323" asserts="0" description="Testing DscResource 'cChocoFeature' using Test-xDscResource"> + <results> + <test-case description="Test-xDscResource should return $true" name="Testing all DSC resources using xDscResource designer..Testing DscResource 'cChocoFeature' using Test-xDscResource.Test-xDscResource should return $true" time="1.1303" asserts="0" success="True" result="Success" executed="True" /> + </results> + </test-suite> + <test-suite type="TestFixture" name="Testing DscSchema 'cChocoFeature' using Test-xDscSchema" executed="True" result="Success" success="True" time="0.1547" asserts="0" description="Testing DscSchema 'cChocoFeature' using Test-xDscSchema"> + <results> + <test-case description="Test-xDscSchema should return true" name="Testing all DSC resources using xDscResource designer..Testing DscSchema 'cChocoFeature' using Test-xDscSchema.Test-xDscSchema should return true" time="0.1299" asserts="0" success="True" result="Success" executed="True" /> + </results> + </test-suite> + <test-suite type="TestFixture" name="Testing DscResource 'cChocoInstaller' using Test-xDscResource" executed="True" result="Success" success="True" time="1.294" asserts="0" description="Testing DscResource 'cChocoInstaller' using Test-xDscResource"> + <results> + <test-case description="Test-xDscResource should return $true" name="Testing all DSC resources using xDscResource designer..Testing DscResource 'cChocoInstaller' using Test-xDscResource.Test-xDscResource should return $true" time="1.1917" asserts="0" success="True" result="Success" executed="True" /> + </results> + </test-suite> + <test-suite type="TestFixture" name="Testing DscSchema 'cChocoInstaller' using Test-xDscSchema" executed="True" result="Success" success="True" time="0.2672" asserts="0" description="Testing DscSchema 'cChocoInstaller' using Test-xDscSchema"> + <results> + <test-case description="Test-xDscSchema should return true" name="Testing all DSC resources using xDscResource designer..Testing DscSchema 'cChocoInstaller' using Test-xDscSchema.Test-xDscSchema should return true" time="0.2444" asserts="0" success="True" result="Success" executed="True" /> + </results> + </test-suite> + <test-suite type="TestFixture" name="Testing DscResource 'cChocoPackageInstall' using Test-xDscResource" executed="True" result="Success" success="True" time="0.9523" asserts="0" description="Testing DscResource 'cChocoPackageInstall' using Test-xDscResource"> + <results> + <test-case description="Test-xDscResource should return $true" name="Testing all DSC resources using xDscResource designer..Testing DscResource 'cChocoPackageInstall' using Test-xDscResource.Test-xDscResource should return $true" time="0.9034" asserts="0" success="True" result="Success" executed="True" /> + </results> + </test-suite> + <test-suite type="TestFixture" name="Testing DscSchema 'cChocoPackageInstall' using Test-xDscSchema" executed="True" result="Success" success="True" time="0.1452" asserts="0" description="Testing DscSchema 'cChocoPackageInstall' using Test-xDscSchema"> + <results> + <test-case description="Test-xDscSchema should return true" name="Testing all DSC resources using xDscResource designer..Testing DscSchema 'cChocoPackageInstall' using Test-xDscSchema.Test-xDscSchema should return true" time="0.1173" asserts="0" success="True" result="Success" executed="True" /> + </results> + </test-suite> + <test-suite type="TestFixture" name="Testing DscResource 'cChocoSource' using Test-xDscResource" executed="True" result="Success" success="True" time="1.2323" asserts="0" description="Testing DscResource 'cChocoSource' using Test-xDscResource"> + <results> + <test-case description="Test-xDscResource should return $true" name="Testing all DSC resources using xDscResource designer..Testing DscResource 'cChocoSource' using Test-xDscResource.Test-xDscResource should return $true" time="1.2046" asserts="0" success="True" result="Success" executed="True" /> + </results> + </test-suite> + <test-suite type="TestFixture" name="Testing DscSchema 'cChocoSource' using Test-xDscSchema" executed="True" result="Success" success="True" time="0.2431" asserts="0" description="Testing DscSchema 'cChocoSource' using Test-xDscSchema"> + <results> + <test-case description="Test-xDscSchema should return true" name="Testing all DSC resources using xDscResource designer..Testing DscSchema 'cChocoSource' using Test-xDscSchema.Test-xDscSchema should return true" time="0.2161" asserts="0" success="True" result="Success" executed="True" /> + </results> + </test-suite> + </results> + </test-suite> + </results> + </test-suite> + </results> + </test-suite> +</test-results> \ No newline at end of file diff --git a/deployment/dsc/azshcihost/cChoco/2.5.0.0/appveyor.yml b/deployment/dsc/azshcihost/cChoco/2.5.0.0/appveyor.yml new file mode 100644 index 0000000..bfe6a65 --- /dev/null +++ b/deployment/dsc/azshcihost/cChoco/2.5.0.0/appveyor.yml @@ -0,0 +1,48 @@ +#---------------------------------# +# environment configuration # +#---------------------------------# +version: 2.5.0.{build} +os: WMF 5 +install: + - ps: . .\AppVeyor\AppVeyorInstall.ps1 + +environment: + nugetKey: + secure: hwpi8ydkuOj0J22HAalIy+mRq4kxDKH7EjtCqwQ5V96svUwfostPY00EIp2HtNhZ + github_access_token: + secure: Xk7jTkdE82qskQy9WPZK0zSafSjT6gK1SYxH8m4Aa8muCMQ4ACkkztJ3toh9DFlS + ModuleName: cChoco +#---------------------------------# +# build configuration # +#---------------------------------# +build_script: + - ps: . .\AppVeyor\AppVeyorBuild.ps1 + +#---------------------------------# +# test configuration # +#---------------------------------# +test_script: + - ps: . .\AppVeyor\AppVeyorTest.ps1 + +#---------------------------------# +# deployment configuration # +#---------------------------------# +deploy_script: + - ps: . .\AppVeyor\AppveyorDeploy.ps1 + +#---------------------------------# +# notification configuration # +#---------------------------------# +notifications: +- provider: Email + to: + - chocolatey-build-status@googlegroups.com + subject: AppVeyor - cChoco DSC Resource Build Notification + on_build_success: false + on_build_failure: true + on_build_status_changed: true +- provider: Webhook + url: https://webhooks.gitter.im/e/f764ee3ccd6ed7348224 + on_build_success: true + on_build_failure: true + on_build_status_changed: true diff --git a/deployment/dsc/azshcihost/cChoco/2.5.0.0/cChoco.psd1 b/deployment/dsc/azshcihost/cChoco/2.5.0.0/cChoco.psd1 new file mode 100644 index 0000000..1def741 Binary files /dev/null and b/deployment/dsc/azshcihost/cChoco/2.5.0.0/cChoco.psd1 differ diff --git a/deployment/dsc/azshcihost/cChoco/2.5.0.0/readme.md b/deployment/dsc/azshcihost/cChoco/2.5.0.0/readme.md new file mode 100644 index 0000000..b8c6883 --- /dev/null +++ b/deployment/dsc/azshcihost/cChoco/2.5.0.0/readme.md @@ -0,0 +1,58 @@ +| Branch | Status | +| ------------- | ------------- | +| master | [![Build status](https://ci.appveyor.com/api/projects/status/qma3jnh23w5vjt46/branch/master?svg=true&passingText=master%20-%20OK&pendingText=master%20-%20PENDING&failingText=master%20-%20FAILED)](https://ci.appveyor.com/project/LawrenceGripper/cchoco/branch/master) | +| development | [![Build status](https://ci.appveyor.com/api/projects/status/qma3jnh23w5vjt46/branch/development?svg=true&passingText=development%20-%20OK&pendingText=development%20-%20PENDING&failingText=development%20-%20FAILED)](https://ci.appveyor.com/project/LawrenceGripper/cchoco/branch/development) | + +# Community Chocolatey DSC Resource + +[![Join the chat at https://gitter.im/chocolatey/cChoco](https://badges.gitter.im/chocolatey/cChoco.svg)](https://gitter.im/chocolatey/cChoco?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) + +This resource is aimed at getting and installing packages using Chocolatey. + +The resource takes the name of the package and will then install that package. + +See [ExampleConfig.ps1](ExampleConfig.ps1) for example usage. + +See list of packages here: https://chocolatey.org/packages + +## Contributing + +Happy to accept new features and fixes. Outstanding issues which can be worked on tagged `Up For Grabs` under issues. + +### Submitting a PR + +Here's the general process of fixing an issue in the DSC Resource Kit: +1. Fork the repository. +3. Clone your fork to your machine. +4. It's preferred to create a non-master working branch where you store updates. +5. Make changes. +6. Write pester tests to ensure that the issue is fixed. +7. Submit a pull request to the development branch. +8. Make sure all tests are passing in AppVeyor for your pull request. +9. Make sure your code does not contain merge conflicts. +10. Address comments (if any). + +### Build and Publishing + +AppVeyor is used to package up the resource and publish to the PowerShell Gallery (on successful build from a newly pushed tag only). + +The AppVeyor scripts do the following: +- Test the resources using 'xDSCResourceDesigner' +- Verify best practises using 'PSScriptAnalyzer' +- Update the version in the manifest file +- Publish the module to the PowerShell gallery +- Check in updated manifest file to GitHub + +To build: + +1. Update `ModuleVersion` in `cChoco.psd1` - use `major.minor.patch.0`; +2. Update `version` in `appveyor.yml` - use `major.minor.patch.{build}`; +3. Merge development branch to master - `git checkout master`, `git merge development`; +4. Tag master with new version - `git tag v<major.minor.patch>`; +5. Push changes with tag `git push v<major.minor.patch>` + +## Known Issues / Troubleshooting + +### WS-Management - Exceeds the maximum envelope size allowed + +The maximum envelope size for WinRM is not sufficient for installing large packages. To increase the envelope size use `winrm set winrm/config @{MaxEnvelopeSizekb=”153600″}` - this exampe will increase it to 150MB. \ No newline at end of file diff --git a/deployment/dsc/azshcihost/cHyper-V/3.0.0.0/DSCResources/cVMIPAddress/README.md b/deployment/dsc/azshcihost/cHyper-V/3.0.0.0/DSCResources/cVMIPAddress/README.md new file mode 100644 index 0000000..eddff87 --- /dev/null +++ b/deployment/dsc/azshcihost/cHyper-V/3.0.0.0/DSCResources/cVMIPAddress/README.md @@ -0,0 +1,51 @@ +#cVMIPAddress# +This DSC resource helps in injecting IP addresses into a running VM on the Hyper-V host. This is especially useful in a bootstrap scenario where there is no DHCP server or unattended XML method to configure an IP Address inside the guest OS. I use this quite a lot for building VMs from template VHDx files. + +![](http://i.imgur.com/S3UVP7F.png) + +The *Id* property is used to uniquely differentiate a VM network adapter that needs configuration. This is not a adapter property but a property that identifies the resource instance. This is a mandatory property. + +The *NetAdapterName* property identifies the VM network adapter. This is a mandatory property. + +The *VMName* property identifies the VM to which the network adapter is connected to. This is a mandatory property. + +The *IPAddress* property is the IP address that will be assigned to the network adapter. This is a mandatory property. If you want to remove a configured IP address or reset the IP address to DHCP, specify 'DHCP' as the value of this parameter. + +The *DefaultGateway* property is the default gateway address that will be assigned to the network adapter. + +The *Subnet* property is the subnet mask that will be assigned to the network adapter. + +The *DnsServer* property is the DNS Server address that will be assigned to the network adapter. + +Here are some examples that demonstrates how to use this resource. + +##Assigning an IP address to a VM adapter## + Configuration VMIPAddress + { + Import-DscResource -ModuleName cHyper-V -Name cVMIPAddress + Import-DscResource -ModuleName PSDesiredStateConfiguration + + cVMIPAddress VMAdapter1IPAddress { + Id = 'VMMgmt-NIC' + NetAdapterName = 'VMMgmt-NIC' + VMName = 'SQLVM01' + IPAddress = '172.16.101.101' + DefaultGateway = '172.16.101.1' + Subnet = '255.255.255.0' + DnsServer = '172.16.101.2' + } + } + +##Removing an IP address assigned to a VM adapter## + Configuration VMIPAddress + { + Import-DscResource -ModuleName cHyper-V -Name cVMIPAddress + Import-DscResource -ModuleName PSDesiredStateConfiguration + + cVMIPAddress VMAdapter1IPAddress { + Id = 'VMMgmt-NIC' + NetAdapterName = 'VMMgmt-NIC' + VMName = 'SQLVM01' + IPAddress = 'DHCP' + } + } \ No newline at end of file diff --git a/deployment/dsc/azshcihost/cHyper-V/3.0.0.0/DSCResources/cVMIPAddress/cVMIPAddress.psm1 b/deployment/dsc/azshcihost/cHyper-V/3.0.0.0/DSCResources/cVMIPAddress/cVMIPAddress.psm1 new file mode 100644 index 0000000..ac07934 Binary files /dev/null and b/deployment/dsc/azshcihost/cHyper-V/3.0.0.0/DSCResources/cVMIPAddress/cVMIPAddress.psm1 differ diff --git a/deployment/dsc/azshcihost/cHyper-V/3.0.0.0/DSCResources/cVMIPAddress/cVMIPAddress.schema.mof b/deployment/dsc/azshcihost/cHyper-V/3.0.0.0/DSCResources/cVMIPAddress/cVMIPAddress.schema.mof new file mode 100644 index 0000000..aa0536b Binary files /dev/null and b/deployment/dsc/azshcihost/cHyper-V/3.0.0.0/DSCResources/cVMIPAddress/cVMIPAddress.schema.mof differ diff --git a/deployment/dsc/azshcihost/cHyper-V/3.0.0.0/DSCResources/cVMIPAddress/en-US/cVMIPAddress.psd1 b/deployment/dsc/azshcihost/cHyper-V/3.0.0.0/DSCResources/cVMIPAddress/en-US/cVMIPAddress.psd1 new file mode 100644 index 0000000..96c4acf Binary files /dev/null and b/deployment/dsc/azshcihost/cHyper-V/3.0.0.0/DSCResources/cVMIPAddress/en-US/cVMIPAddress.psd1 differ diff --git a/deployment/dsc/azshcihost/cHyper-V/3.0.0.0/DSCResources/cVMNetworkAdapter/Readme.md b/deployment/dsc/azshcihost/cHyper-V/3.0.0.0/DSCResources/cVMNetworkAdapter/Readme.md new file mode 100644 index 0000000..520496e --- /dev/null +++ b/deployment/dsc/azshcihost/cHyper-V/3.0.0.0/DSCResources/cVMNetworkAdapter/Readme.md @@ -0,0 +1,111 @@ +#Using cVMNetworkAdapter resource# +This DSC resource can be used to attach network adapters to VM switches and add them to either management OS or virtual machines on the host. + +![](http://i.imgur.com/KVSWBo8.png) + +The *Id* property is the unique key within this DSC resource. This property isn't related to any VM network adapter configuration but instead used as a way to uniquely identify each VM network adapter resource in a configuration. This property was introduced in an earlier version of this resource to ensure you can add netowrk adapter with the same name to different virtual machines. Make a note of that. **This won't still let you create two network adapters with the same name and attach them to same VM.** When configuring management OS or VM with multiple network adapters, you should still use unique name for each network adapter. *DO NOT USE A GUID AS AN ARGUMENT. ESPECIALLY, DYNAMICALLY GENERATED GUIDs* + +In the previous version of this DSC resource, *Name* property was the key property. Having *Name* as the key property prevented creating network adapters with the same name and attach them to different VMs. So, if you need to add a network adapter to the management OS, specify *VMName* as 'ManagementOS'. If the value of *VMName* property is not 'ManagementOS', it will be considered a Virtual Machine configuration and a network adapter will be added to VM on the Hyper-V host. + +The *Name* property and *SwitchName* property identify the name to be assigned to the VM network adapter and the name of the switch to attach to. + +If you want to assign a static MAC address to the VM network adapter, you can use the *MacAddress* property. Providing a value to this property sets the static MAC address on the VM network adapter. Removing this value will make it a dynamic MAC address. This can be used only with the virtual machine adapters and not management OS. + +The following examples demonstrate how to use this resource module. + +## Create a management OS adapter on the Hyper-V host ## + Configuration HostOSAdapter + { + Import-DscResource -ModuleName cHyper-V -Name cVMNetworkAdapter + Import-DscResource -ModuleName PSDesiredStateConfiguration + + cVMNetworkAdapter HostOSAdapter { + Id = 'Management-NIC' + Name = 'Management-NIC' + SwitchName = 'SETSwitch' + VMName = 'ManagementOS' + Ensure = 'Present' + } + } + +## Create multiple management OS adapters on the Hyper-V host ## + Configuration HostOSAdapter + { + Import-DscResource -ModuleName cHyper-V -Name cVMNetworkAdapter + Import-DscResource -ModuleName PSDesiredStateConfiguration + + cVMNetworkAdapter ManagementAdapter { + Id = 'Management-NIC' + Name = 'Management-NIC' + SwitchName = 'SETSwitch' + VMName = 'ManagementOS' + Ensure = 'Present' + } + + cVMNetworkAdapter ClusterAdapter { + Id = 'Cluster-NIC' + Name = 'Cluster-NIC' + SwitchName = 'SETSwitch' + VMName = 'ManagementOS' + Ensure = 'Present' + } + } + +## Create multiple management OS adapters on the Hyper-V host ## + Configuration VMAdapter + { + Import-DscResource -ModuleName cHyper-V -Name cVMNetworkAdapter + Import-DscResource -ModuleName PSDesiredStateConfiguration + + cVMNetworkAdapter MyVM01NIC { + Id = 'MyVM01-NIC' + Name = 'MyVM01-NIC' + SwitchName = 'SETSwitch' + VMName = 'MyVM01' + Ensure = 'Present' + } + + cVMNetworkAdapter MyVM02NIC { + Id = 'MyVM02-NIC' + Name = 'NetAdapter' + SwitchName = 'SETSwitch' + VMName = 'MyVM02' + Ensure = 'Present' + } + + #The following resource configuration shows that the different VMs can have network adapters with the same name. This is possible because Id is the key property and not Name. + cVMNetworkAdapter MyVM03NIC { + Id = 'MyVM03-NIC' + Name = 'NetAdapter' + SwitchName = 'SETSwitch' + VMName = 'MyVM03' + Ensure = 'Present' + } + } + +##Create multiple management OS adapters with static MAC address on the Hyper-V host ## + Configuration VMAdapter + { + Import-DscResource -ModuleName cHyper-V -Name cVMNetworkAdapter + Import-DscResource -ModuleName PSDesiredStateConfiguration + + cVMNetworkAdapter MyVM01NIC { + Id = 'MyVM01-NIC' + Name = 'MyVM01-NIC' + SwitchName = 'SETSwitch' + MacAddress = '001523be0c' + VMName = 'MyVM01' + Ensure = 'Present' + } + + cVMNetworkAdapter MyVM02NIC { + Id = 'MyVM02-NIC' + Name = 'MyVM02-NIC' + SwitchName = 'SETSwitch' + MacAddress = '001523be0d' + VMName = 'MyVM02' + Ensure = 'Present' + } + } + +If you want to remove a network adapter either from the management OS or virtual machine, you need to set the *Ensure* property to *Absent*. \ No newline at end of file diff --git a/deployment/dsc/azshcihost/cHyper-V/3.0.0.0/DSCResources/cVMNetworkAdapter/cVMNetworkAdapter.psm1 b/deployment/dsc/azshcihost/cHyper-V/3.0.0.0/DSCResources/cVMNetworkAdapter/cVMNetworkAdapter.psm1 new file mode 100644 index 0000000..836d6ac --- /dev/null +++ b/deployment/dsc/azshcihost/cHyper-V/3.0.0.0/DSCResources/cVMNetworkAdapter/cVMNetworkAdapter.psm1 @@ -0,0 +1,308 @@ +#region localizeddata +if (Test-Path "${PSScriptRoot}\${PSUICulture}") +{ + Import-LocalizedData -BindingVariable LocalizedData -filename cVMNetworkAdapter.psd1 ` + -BaseDirectory "${PSScriptRoot}\${PSUICulture}" +} +else +{ + #fallback to en-US + Import-LocalizedData -BindingVariable LocalizedData -filename cVMNetworkAdapter.psd1 ` + -BaseDirectory "${PSScriptRoot}\en-US" +} +#endregion + +Function Get-TargetResource +{ + [CmdletBinding()] + [OutputType([System.Collections.Hashtable])] + Param ( + [Parameter(Mandatory)] + [String] $Id, + + [Parameter(Mandatory)] + [String] $Name, + + [Parameter(Mandatory)] + [String] $SwitchName, + + [Parameter(Mandatory)] + [String] $VMName + ) + + $configuration = @{ + Id = $Id + Name = $Name + SwitchName = $SwitchName + VMName = $VMName + } + + $arguments = @{ + Name = $Name + } + + if ($VMName -ne 'ManagementOS') + { + $arguments.Add('VMName',$VMName) + } + else + { + $arguments.Add('ManagementOS', $true) + $arguments.Add('SwitchName', $SwitchName) + } + + Write-Verbose -Message $localizedData.GetVMNetAdapter + $netAdapter = Get-VMNetworkAdapter @arguments -ErrorAction SilentlyContinue + + if ($netAdapter) + { + Write-Verbose $localizedData.FoundVMNetAdapter + if ($VMName -eq 'ManagementOS') + { + $configuration.Add('MacAddress', $netAdapter.MacAddress) + $configuration.Add('DynamicMacAddress', $false) + } + elseif ($netAdapter.VMName) + { + $configuration.Add('MacAddress', $netAdapter.MacAddress) + $configuration.Add('DynamicMacAddress', $netAdapter.DynamicMacAddressEnabled) + } + $configuration.Add('Ensure','Present') + } + else + { + Write-Verbose -Message $localizedData.NoVMNetAdapterFound + $configuration.Add('Ensure','Absent') + } + + return $configuration +} + +Function Set-TargetResource +{ + [CmdletBinding()] + Param ( + [Parameter(Mandatory)] + [String] $Id, + + [Parameter(Mandatory)] + [String] $Name, + + [Parameter(Mandatory)] + [String] $SwitchName, + + [Parameter(Mandatory)] + [String] $VMName, + + [Parameter()] + [String] $MacAddress, + + [Parameter()] + [ValidateSet('Present','Absent')] + [String] $Ensure='Present' + ) + + $arguments = @{ + Name = $Name + } + + if ($VMName -ne 'ManagementOS') + { + $arguments.Add('VMName',$VMName) + } + else + { + $arguments.Add('ManagementOS', $true) + $arguments.Add('SwitchName', $SwitchName) + } + + Write-Verbose $localizedData.GetVMNetAdapter + $netAdapterExists = Get-VMNetworkAdapter @arguments -ErrorAction SilentlyContinue + + if ($Ensure -eq 'Present') + { + if ($netAdapterExists) + { + Write-Verbose $localizedData.FoundVMNetAdapter + if (($VMName -ne 'ManagementOS')) + { + if ($MacAddress) + { + if ($netAdapterExists.DynamicMacAddressEnabled) + { + Write-Verbose -Message $localizedData.EnableStaticMacAddress + $updateMacAddress = $true + } + elseif ($MacAddress -ne $netAdapterExists.StaicMacAddress) + { + Write-Verbose -Message $localizedData.EnableStaticMacAddress + $updateMacAddress = $true + } + } + else + { + if (-not $netAdapterExists.DynamicMacAddressEnabled) + { + Write-Verbose $localizedData.EnableDynamicMacAddress + $updateMacAddress = $true + } + } + + if ($netAdapterExists.SwitchName -ne $SwitchName) + { + Write-Verbose $localizedData.PerformSwitchConnect + Connect-VMNetworkAdapter -VMNetworkAdapter $netAdapterExists -SwitchName $SwitchName -ErrorAction Stop -Verbose + } + + if (($updateMacAddress)) + { + Write-Verbose $localizedData.PerformVMNetModify + + $setArguments = @{ } + $setArguments.Add('VMNetworkAdapter',$netAdapterExists) + if ($MacAddress) + { + $setArguments.Add('StaticMacAddress',$MacAddress) + } + else + { + $setArguments.Add('DynamicMacAddress', $true) + } + Set-VMNetworkAdapter @setArguments -ErrorAction Stop + } + } + } + else + { + if ($VMName -ne 'ManagementOS') + { + if (-not $MacAddress) + { + $arguments.Add('DynamicMacAddress',$true) + } + else + { + $arguments.Add('StaticMacAddress',$MacAddress) + } + $arguments.Add('SwitchName',$SwitchName) + } + Write-Verbose $localizedData.AddVMNetAdapter + Add-VMNetworkAdapter @arguments -ErrorAction Stop + } + } + else + { + Write-Verbose $localizedData.RemoveVMNetAdapter + Remove-VMNetworkAdapter @arguments -ErrorAction Stop + } +} + +Function Test-TargetResource +{ + [CmdletBinding()] + [OutputType([System.Boolean])] + Param ( + [Parameter(Mandatory)] + [String] $Id, + + [Parameter(Mandatory)] + [String] $Name, + + [Parameter(Mandatory)] + [String] $SwitchName, + + [Parameter(Mandatory)] + [String] $VMName, + + [Parameter()] + [String] $MacAddress, + + [Parameter()] + [ValidateSet('Present','Absent')] + [String] $Ensure='Present' + ) + + $arguments = @{ + Name = $Name + } + + if ($VMName -ne 'ManagementOS') + { + $arguments.Add('VMName',$VMName) + } + else + { + $arguments.Add('ManagementOS', $true) + $arguments.Add('SwitchName', $SwitchName) + } + + Write-Verbose $localizedData.GetVMNetAdapter + $netAdapterExists = Get-VMNetworkAdapter @arguments -ErrorAction SilentlyContinue + + if ($Ensure -eq 'Present') + { + if ($netAdapterExists) + { + if ($VMName -ne 'ManagementOS') + { + if ($MacAddress) + { + if ($netAdapterExists.DynamicMacAddressEnabled) + { + Write-Verbose $localizedData.EnableStaticMacAddress + return $false + } + elseif ($netAdapterExists.MacAddress -ne $MacAddress) + { + Write-Verbose $localizedData.StaticAddressDoesNotMatch + return $false + } + } + else + { + if (-not $netAdapterExists.DynamicMacAddressEnabled) + { + Write-Verbose $localizedData.EnableDynamicMacAddress + return $false + } + } + + if ($netAdapterExists.SwitchName -ne $SwitchName) + { + Write-Verbose $localizedData.SwitchIsDifferent + return $false + } + else + { + Write-Verbose $localizedData.VMNetAdapterExistsNoActionNeeded + return $true + } + } + else + { + Write-Verbose $localizedData.VMNetAdapterExistsNoActionNeeded + return $true + } + } + else + { + Write-Verbose $localizedData.VMNetAdapterDoesNotExistShouldAdd + return $false + } + } + else + { + if ($netAdapterExists) + { + Write-Verbose $localizedData.VMNetAdapterExistsShouldRemove + return $false + } + else + { + Write-Verbose $localizedData.VMNetAdapterDoesNotExistNoActionNeeded + return $true + } + } +} + +Export-ModuleMember -Function *-TargetResource diff --git a/deployment/dsc/azshcihost/cHyper-V/3.0.0.0/DSCResources/cVMNetworkAdapter/cVMNetworkAdapter.schema.mof b/deployment/dsc/azshcihost/cHyper-V/3.0.0.0/DSCResources/cVMNetworkAdapter/cVMNetworkAdapter.schema.mof new file mode 100644 index 0000000..92b2208 Binary files /dev/null and b/deployment/dsc/azshcihost/cHyper-V/3.0.0.0/DSCResources/cVMNetworkAdapter/cVMNetworkAdapter.schema.mof differ diff --git a/deployment/dsc/azshcihost/cHyper-V/3.0.0.0/DSCResources/cVMNetworkAdapter/en-US/cVMNetworkAdapter.psd1 b/deployment/dsc/azshcihost/cHyper-V/3.0.0.0/DSCResources/cVMNetworkAdapter/en-US/cVMNetworkAdapter.psd1 new file mode 100644 index 0000000..c60f3ff --- /dev/null +++ b/deployment/dsc/azshcihost/cHyper-V/3.0.0.0/DSCResources/cVMNetworkAdapter/en-US/cVMNetworkAdapter.psd1 @@ -0,0 +1,23 @@ +ConvertFrom-StringData @' + VMNameAndManagementTogether=VMName cannot be provided when ManagementOS is set to True. + MustProvideVMName=Must provide VMName parameter when ManagementOS is set to False. + GetVMNetAdapter=Getting VM Network Adapter information. + FoundVMNetAdapter=Found VM Network Adapter. + NoVMNetAdapterFound=No VM Network Adapter found. + StaticMacAddressChosen=Static MAC Address has been specified. + StaticAddressDoesNotMatch=Staic MAC address on the VM Network Adapter does not match. + ModifyVMNetAdapter=VM Network Adapter exists with different configuration. This will be modified. + EnableDynamicMacAddress=VM Network Adapter exists but without Dynamic MAC address setting. + EnableStaticMacAddress=VM Network Adapter exists but without static MAC address setting. + PerformVMNetModify=Performing VM Network Adapter configuration changes. + CannotChangeHostAdapterMacAddress=VM Network adapter in configuration is a host adapter. Its configuration cannot be modified. + AddVMNetAdapter=Adding VM Network Adapter. + RemoveVMNetAdapter=Removing VM Network Adapter. + VMNetAdapterExistsNoActionNeeded=VM Network Adapter exists with requested configuration. No action needed. + VMNetAdapterDoesNotExistShouldAdd=VM Network Adapter does not exist. It will be added. + VMNetAdapterExistsShouldRemove=VM Network Adapter Exists. It will be removed. + VMNetAdapterDoesNotExistNoActionNeeded=VM Network adapter does not exist. No action needed. + StaticMacExists=StaicMacAddress configuration exists as desired. + SwitchIsDifferent=Net Adapter is not connected to the requested switch. + PerformSwitchConnect=Connecting VM Net adapter to the right switch. +'@ diff --git a/deployment/dsc/azshcihost/cHyper-V/3.0.0.0/DSCResources/cVMNetworkAdapterSettings/README.md b/deployment/dsc/azshcihost/cHyper-V/3.0.0.0/DSCResources/cVMNetworkAdapterSettings/README.md new file mode 100644 index 0000000..336a44f --- /dev/null +++ b/deployment/dsc/azshcihost/cHyper-V/3.0.0.0/DSCResources/cVMNetworkAdapterSettings/README.md @@ -0,0 +1,78 @@ +#Using cVMNetworkAdapterSettings resource# +Once the VM network adapters are created, we can assign the bandwidth reservation or priority settings as needed. If we set the *MinimumBandwidthMode* to *Weight* during VM switch creation, we need to specify the percentage of bandwidth reservation for each adapter. We can this DSC resource for the purpose of updating the VM network adapter settings. This DSC resource can used for many other settings such as *DhcpGuard*, *RouterGuard*, *DeviceNaming*, and so on. + +![](http://i.imgur.com/tZ1d4Fv.png) + +The *Id* property is mandatory as a unique key for the resource configuration. This identifies the right resource instance of the network adapter in the system. This was chosen as an input property because the VM network adapter name can be the same for multiple adapters connected to the same VM or management OS. + +The *Name* property identifies the name of the virtual network adapter. This is a required property. + +The *SwitchName* property is used to specify where (the VM switch) the network adapter is connected. This is a required property too. + +The *VMName* property is used to if a network adapter is connected to a VM or Management OS. If you need to add a network adapter to the management OS, specify *VMName* as 'ManagementOS'. If the value of *VMName* property is not 'ManagementOS', it will be considered a VM configuration and the network adapter attached to the VM will be configured for specified settings. + +The *MaximumBandwidth* property is used to specify the maximum bandwidth, in bits per second, for the VM network adapter. + +The *MinimumBandwidthAbsolute* specifies the minimum bandwidth, in bits per second, for the virtual network adapter. By default, these properties are set to zero which means those parameters within the network adapter are disabled. + +The *MinimumBandwidthWeight* specifies the minimum bandwidth, in terms of relative weight, for the virtual network adapter. The weight describes how much bandwidth to provide to the virtual network adapter relative to other virtual network adapters connected to the same virtual switch. + +If you want allow teaming of network adapters in the guest OS, you can set the *AllowTeaming* property to On. By default, this is set to *Off* and therefore disallows network teaming inside guest OS. + +Similar to this, there are other settings of a VM network adapter that you can configure. These properties include *DhcpGuard*, *MacAddressSpoofing*, *PortMirroring*, *RouterGuard*, *IeeePriorityTag*, *DeviceNaming*, and *VmqWeight*. These properties are self explanatory and are left to defaults for a VM network adapter. + +The following examples demonstrate how to use this DSC resource. + +##Setting MinimumBandwidthWeight for a VM adapter in management OS## + Configuration HostOSAdapterSettings + { + Import-DscResource -ModuleName cHyper-V -Name cVMNetworkAdapterSettings + Import-DscResource -ModuleName PSDesiredStateConfiguration + + cVMNetworkAdapterSettings HostOSAdapterSettings { + Id = 'Management-NIC' + Name = 'Management-NIC' + VMName = 'ManagementOS' + SwitchName = 'SETSwitch' + MinimumBandwidthWeight = 20 + } + } + +##Setting DHCP guard for a VM adapter connected to a VM## + Configuration VMAdapterSettings + { + Import-DscResource -ModuleName cHyper-V -Name cVMNetworkAdapterSettings + Import-DscResource -ModuleName PSDesiredStateConfiguration + + cVMNetworkAdapterSettings VMAdapterSettings { + Id = 'Management-NIC' + Name = 'Management-NIC' + VMName = 'DHCPVM01' + SwitchName = 'SETSwitch' + DhcpGuard = 'On' + } + } + +##Setting DHCPGuard and DeviceNaming on multiple VM network adapters connected to the same VM## + Configuration VMAdapterSettings + { + Import-DscResource -ModuleName cHyper-V -Name cVMNetworkAdapterSettings + Import-DscResource -ModuleName PSDesiredStateConfiguration + + cVMNetworkAdapterSettings VMAdapterSettings01 { + Id = 'Management-NIC' + Name = 'Management-NIC' + VMName = 'DHCPVM01' + SwitchName = 'SETSwitch' + DhcpGuard = 'On' + DeviceNaming = 'On' + } + + cVMNetworkAdapterSettings VMAdapterSettings02 { + Id = 'App-NIC' + Name = 'App-NIC' + VMName = 'DHCPVM01' + SwitchName = 'SETSwitch' + DeviceNaming = 'On' + } + } \ No newline at end of file diff --git a/deployment/dsc/azshcihost/cHyper-V/3.0.0.0/DSCResources/cVMNetworkAdapterSettings/cVMNetworkAdapterSettings.psm1 b/deployment/dsc/azshcihost/cHyper-V/3.0.0.0/DSCResources/cVMNetworkAdapterSettings/cVMNetworkAdapterSettings.psm1 new file mode 100644 index 0000000..b8872a6 --- /dev/null +++ b/deployment/dsc/azshcihost/cHyper-V/3.0.0.0/DSCResources/cVMNetworkAdapterSettings/cVMNetworkAdapterSettings.psm1 @@ -0,0 +1,297 @@ +#region localizeddata +if (Test-Path "${PSScriptRoot}\${PSUICulture}") +{ + Import-LocalizedData -BindingVariable LocalizedData -filename cVMNetworkAdapterSettings.psd1 ` + -BaseDirectory "${PSScriptRoot}\${PSUICulture}" +} +else +{ + #fallback to en-US + Import-LocalizedData -BindingVariable LocalizedData -filename cVMNetworkAdapterSettings.psd1 ` + -BaseDirectory "${PSScriptRoot}\en-US" +} +#endregion + +Function Get-TargetResource +{ + [CmdletBinding()] + [OutputType([System.Collections.Hashtable])] + Param ( + [Parameter(Mandatory)] + [String] $Id, + + [Parameter(Mandatory)] + [String] $Name, + + [Parameter(Mandatory)] + [String] $SwitchName, + + [Parameter(Mandatory)] + [String] $VMName + ) + + if(!(Get-Module -ListAvailable -Name Hyper-V)) + { + Throw $localizedData.HyperVModuleNotFound + } + + $configuration = @{ + Id = $Id + Name = $Name + SwitchName = $SwitchName + } + + $arguments = @{ + Name = $Name + } + + if ($VMName -ne 'ManagementOS') + { + $arguments.Add('VMName',$VMName) + } + else + { + $arguments.Add('ManagementOS', $true) + $arguments.Add('SwitchName', $SwitchName) + } + + Write-Verbose $localizedData.GetVMNetAdapter + $netAdapter = Get-VMNetworkAdapter @arguments -ErrorAction SilentlyContinue + + if ($netAdapter) + { + Write-Verbose $localizedData.FoundVMNetAdapter + $configuration.Add('MacAddressSpoofing', $netAdapter.MacAddressSpoofing) + $configuration.Add('DhcpGuard', $netAdapter.DhcpGuard) + $configuration.Add('RouterGuard', $netAdapter.RouterGuard) + $configuration.Add('AllowTeaming', $netAdapter.AllowTeaming) + $configuration.Add('VmqWeight', $netAdapter.VmqWeight) + $configuration.Add('MaximumBandwidth',$netAdapter.BandwidthSetting.MaximumBandwidth) + $configuration.Add('MinimumBandwidthWeight',$netAdapter.BandwidthSetting.MinimumBandwidthWeight) + $configuration.Add('MinimumBandwidthAbsolute',$netAdapter.BandwidthSetting.MinimumBandwidthAbsolute) + $configuration.Add('IeeePriorityTag',$netAdapter.IeeePriorityTag) + $configuration.Add('PortMirroring',$netAdapter.PortMirroringMode) + $configuration.Add('DeviceNaming',$netAdapter.DeviceNaming) + } + else + { + Write-Warning $localizedData.NoVMNetAdapterFound + } + + return $configuration +} + +Function Set-TargetResource +{ + [CmdletBinding()] + Param ( + [Parameter(Mandatory)] + [String] $Id, + + [Parameter(Mandatory)] + [String] $Name, + + [Parameter(Mandatory)] + [String] $SwitchName, + + [Parameter(Mandatory)] + [String] $VMName, + + [Parameter()] + [ValidateSet('On','Off')] + [String] $MacAddressSpoofing = 'Off', + + [Parameter()] + [ValidateSet('On','Off')] + [String] $DhcpGuard = 'Off', + + [Parameter()] + [ValidateSet('On','Off')] + [String] $IeeePriorityTag = 'Off', + + [Parameter()] + [ValidateSet('On','Off')] + [String] $RouterGuard = 'Off', + + [Parameter()] + [ValidateSet('On','Off')] + [String] $AllowTeaming = 'Off', + + [Parameter()] + [ValidateSet('On','Off')] + [String] $DeviceNaming = 'On', + + [Parameter()] + [uint64] $MaximumBandwidth = 0, + + [Parameter()] + [ValidateRange(0,100)] + [uint32] $MinimumBandwidthWeight = 0, + + [Parameter()] + [uint32] $MinimumBandwidthAbsolute, + + [Parameter()] + [ValidateRange(0,100)] + [uint32] $VmqWeight = 100, + + [Parameter()] + [ValidateSet('None','Source','Destination')] + [String] $PortMirroring = 'None' + ) + + if(!(Get-Module -ListAvailable -Name Hyper-V)) + { + Throw $localizedData.HyperVModuleNotFound + } + + $arguments = @{ + Name = $Name + } + + if ($VMName -ne 'ManagementOS') + { + $arguments.Add('VMName',$VMName) + } + else + { + $arguments.Add('ManagementOS', $true) + $arguments.Add('SwitchName', $SwitchName) + } + + Write-Verbose $localizedData.GetVMNetAdapter + $netAdapter = Get-VMNetworkAdapter @arguments -ErrorAction SilentlyContinue + + $setArguments = @{ + VMNetworkAdapter = $netAdapter + MacAddressSpoofing = $MacAddressSpoofing + DhcpGuard = $DhcpGuard + RouterGuard = $RouterGuard + VmqWeight = $VmqWeight + MaximumBandwidth = $MaximumBandwidth + MinimumBandwidthWeight = $MinimumBandwidthWeight + MinimumBandwidthAbsolute= $MinimumBandwidthAbsolute + IeeePriorityTag = $IeeePriorityTag + AllowTeaming = $AllowTeaming + PortMirroring = $PortMirroring + DeviceNaming = $DeviceNaming + } + + Write-Verbose $localizedData.PerformVMNetModify + Set-VMNetworkAdapter @setArguments -ErrorAction Stop +} + +Function Test-TargetResource +{ + [CmdletBinding()] + [OutputType([System.Boolean])] + Param ( + [Parameter(Mandatory)] + [String] $Id, + + [Parameter(Mandatory)] + [String] $Name, + + [Parameter(Mandatory)] + [String] $SwitchName, + + [Parameter(Mandatory)] + [String] $VMName, + + [Parameter()] + [ValidateSet('On','Off')] + [String] $MacAddressSpoofing = 'Off', + + [Parameter()] + [ValidateSet('On','Off')] + [String] $DhcpGuard = 'Off', + + [Parameter()] + [ValidateSet('On','Off')] + [String] $IeeePriorityTag = 'Off', + + [Parameter()] + [ValidateSet('On','Off')] + [String] $RouterGuard = 'Off', + + [Parameter()] + [ValidateSet('On','Off')] + [String] $AllowTeaming = 'Off', + + [Parameter()] + [ValidateSet('On','Off')] + [String] $DeviceNaming = 'On', + + [Parameter()] + [uint64] $MaximumBandwidth = 0, + + [Parameter()] + [ValidateRange(0,100)] + [uint32] $MinimumBandwidthWeight = 0, + + [Parameter()] + [uint32] $MinimumBandwidthAbsolute, + + [Parameter()] + [ValidateRange(0,100)] + [uint32] $VmqWeight = 100, + + [Parameter()] + [ValidateSet('None','Source','Destination')] + [String] $PortMirroring = 'None' + ) + + if(!(Get-Module -ListAvailable -Name Hyper-V)) + { + Throw $localizedData.HyperVModuleNotFound + } + + $arguments = @{ + Name = $Name + } + + if ($VMName -ne 'ManagementOS') + { + $arguments.Add('VMName',$VMName) + } + else + { + $arguments.Add('ManagementOS', $true) + $arguments.Add('SwitchName', $SwitchName) + } + + Write-Verbose $localizedData.GetVMNetAdapter + $adapterExists = Get-VMNetworkAdapter @arguments -ErrorAction SilentlyContinue + + if ($adapterExists) + { + Write-Verbose $localizedData.FoundVMNetAdapter + if ($adapterExists.MacAddressSpoofing -eq $MacAddressSpoofing ` + -and $adapterExists.RouterGuard -eq $RouterGuard ` + -and $adapterExists.DhcpGuard -eq $DhcpGuard ` + -and $adapterExists.IeeePriorityTag -eq $IeeePriorityTag ` + -and $adapterExists.AllowTeaming -eq $AllowTeaming ` + -and $adapterExists.BandwidthSetting.MaximumBandwidth -eq $MaximumBandwidth ` + -and $adapterExists.BandwidthSetting.MinimumBandwidthWeight -eq $MinimumBandwidthWeight ` + -and $adapterExists.BandwidthSetting.MinimumBandwidthAbsolute -eq $MinimumBandwidthAbsolute ` + -and $adapterExists.VMQWeight -eq $VMQWeight ` + -and $adapterExists.PortMirroringMode -eq $PortMirroring ` + -and $adapterExists.DeviceNaming -eq $DeviceNaming + ) + { + Write-Verbose $localizedData.VMNetAdapterExistsNoActionNeeded + return $true + } + else + { + Write-Verbose $localizedData.VMNetAdapterExistsWithDifferentConfiguration + return $false + } + } + else + { + throw $localizedData.VMNetAdapterDoesNotExist + } +} + +Export-ModuleMember -Function *-TargetResource diff --git a/deployment/dsc/azshcihost/cHyper-V/3.0.0.0/DSCResources/cVMNetworkAdapterSettings/cVMNetworkAdapterSettings.schema.mof b/deployment/dsc/azshcihost/cHyper-V/3.0.0.0/DSCResources/cVMNetworkAdapterSettings/cVMNetworkAdapterSettings.schema.mof new file mode 100644 index 0000000..9dc2582 Binary files /dev/null and b/deployment/dsc/azshcihost/cHyper-V/3.0.0.0/DSCResources/cVMNetworkAdapterSettings/cVMNetworkAdapterSettings.schema.mof differ diff --git a/deployment/dsc/azshcihost/cHyper-V/3.0.0.0/DSCResources/cVMNetworkAdapterSettings/en-US/cVMNetworkAdapterSettings.psd1 b/deployment/dsc/azshcihost/cHyper-V/3.0.0.0/DSCResources/cVMNetworkAdapterSettings/en-US/cVMNetworkAdapterSettings.psd1 new file mode 100644 index 0000000..229b830 --- /dev/null +++ b/deployment/dsc/azshcihost/cHyper-V/3.0.0.0/DSCResources/cVMNetworkAdapterSettings/en-US/cVMNetworkAdapterSettings.psd1 @@ -0,0 +1,12 @@ +ConvertFrom-StringData @' + HyperVModuleNotFound=Hyper-V PowerShell Module not found. + VMNameAndManagementTogether=VMName cannot be provided when ManagementOS is set to True. + MustProvideVMName=Must provide VMName parameter when ManagementOS is set to False. + GetVMNetAdapter=Getting VM Network Adapter information. + FoundVMNetAdapter=Found VM Network Adapter. + NoVMNetAdapterFound=No VM Network Adapter found. + PerformVMNetModify=Performing VM Network Adapter configuration changes. + VMNetAdapterExistsNoActionNeeded=VM Network Adapter exists with requested configuration. No action needed. + VMNetAdapterDoesNotExist=VM Network adapter does not exist. + VMNetAdapterExistsWithDifferentConfiguration=VM Network Adapter exists but different configuration. This will be fixed. +'@ diff --git a/deployment/dsc/azshcihost/cHyper-V/3.0.0.0/DSCResources/cVMNetworkAdapterVlan/Readme.md b/deployment/dsc/azshcihost/cHyper-V/3.0.0.0/DSCResources/cVMNetworkAdapterVlan/Readme.md new file mode 100644 index 0000000..1264430 --- /dev/null +++ b/deployment/dsc/azshcihost/cHyper-V/3.0.0.0/DSCResources/cVMNetworkAdapterVlan/Readme.md @@ -0,0 +1,94 @@ +#Using cVMNetworkAdapterVlan resource# +This DSC resource can be used to assign VLAN information to a NIC that is created attached to either the management OS or a virtual machine. There are several possibilities here. + +![](http://i.imgur.com/GMsXDyK.png) + +This resource has three mandatory properties. + +The *Id* property is a unique identifier to differentiate between multiple VMs containing the same network adapter name or same VM having multiple adapters named same. + +The *Name* property identifies the name of the network adapter for which the VLAN information needs to be configured. + +The *VMName* property identifies where the network adapter is connected. You can specify host OS by specifying a value *ManagementOS*. If the value of *VMName* property is not *ManagementOS*, it will be considered a Virtual Machine configuration and the network adapter attached to the VM will be configured for VLAN settings. + +The *AdapterMode* property specifies the operation mode of the adapter and is by default set to *Untagged* which means there is no VLAN configuration. The possible and valid values for this property are *Untagged*, *Access*, *Trunk*, *Community*, *Isolated*, and *Promiscuous*. Each of these modes have a corresponding VLAN property that is mandatory. + +If you set the *AdapterMode* property to *Access*, then it is mandatory to provide *VlanId* property. + +If you set the *AdapterMode* to *Trunk*, the *NativeVlanId* property must be specified. + +The following examples demonstrate how to use this DSC resource. + +##Simple Management OS NIC VLAN configuration for Access VLAN## + + Configuration HostOSAdapterVlan + { + Import-DscResource -ModuleName cHyper-V -Name cVMNetworkAdapterVlan + Import-DscResource -ModuleName PSDesiredStateConfiguration + + cVMNetworkAdapterVlan HostOSAdapterVlan { + Id = 'Management-NIC' + Name = 'Management-NIC' + VMName = 'ManagementOS' + AdapterMode = 'Access' + VlanId = 10 + } + } + +##Multiple Management OS NIC VLAN configuration for Access and Untagged VLAN## + + Configuration HostOSAdapterVlan + { + Import-DscResource -ModuleName cHyper-V -Name cVMNetworkAdapterVlan + Import-DscResource -ModuleName PSDesiredStateConfiguration + + cVMNetworkAdapterVlan HostOSAdapterVlan { + Id = 'Management-NIC' + Name = 'Management-NIC' + VMName = 'ManagementOS' + AdapterMode = 'Access' + VlanId = 10 + } + + cVMNetworkAdapterVlan ClusterAdapterVlan { + Id = 'Cluster-NIC' + Name = 'Cluster-NIC' + VMName = 'ManagementOS' + AdapterMode = 'Access' + VlanId = 20 + } + + #The following configuration removes any VLAN setting, if present. + cVMNetworkAdapterVlan JustAnotherAdapterVlan { + Id = 'JustAnother-NIC' + Name = 'JustAnother-NIC' + VMName = 'ManagementOS' + AdapterMode = 'Untagged' + } + } + +In the above example, setting the *AdapterMode* to *Untagged* removes any VLAN configuration on the NIC, if present. By default, all VM network adapters will be in Untagged mode. + +##Multiple Management OS NIC VLAN configuration for Access and Untagged VLAN## + + Configuration HostOSAdapterVlan + { + Import-DscResource -ModuleName cHyper-V -Name cVMNetworkAdapterVlan + Import-DscResource -ModuleName PSDesiredStateConfiguration + + cVMNetworkAdapterVlan VMMgmtAdapterVlan { + Id = 'VMManagement-NIC' + Name = 'VMManagement-NIC' + VMName = 'SQLVM01' + AdapterMode = 'Access' + VlanId = 10 + } + + cVMNetworkAdapterVlan VMiSCSIAdapterVlan { + Id = 'VMiSCSI-NIC' + Name = 'VMiSCSI-NIC' + VMName = 'SQLVM01' + AdapterMode = 'Untagged' + } + } + diff --git a/deployment/dsc/azshcihost/cHyper-V/3.0.0.0/DSCResources/cVMNetworkAdapterVlan/cVMNetworkAdapterVlan.psm1 b/deployment/dsc/azshcihost/cHyper-V/3.0.0.0/DSCResources/cVMNetworkAdapterVlan/cVMNetworkAdapterVlan.psm1 new file mode 100644 index 0000000..9d42a50 --- /dev/null +++ b/deployment/dsc/azshcihost/cHyper-V/3.0.0.0/DSCResources/cVMNetworkAdapterVlan/cVMNetworkAdapterVlan.psm1 @@ -0,0 +1,444 @@ +#region localizeddata +if (Test-Path "${PSScriptRoot}\${PSUICulture}") +{ + Import-LocalizedData -BindingVariable localizedData -filename cVMNetworkAdapterVlan.psd1 ` + -BaseDirectory "${PSScriptRoot}\${PSUICulture}" +} +else +{ + #fallback to en-US + Import-LocalizedData -BindingVariable localizedData -filename cVMNetworkAdapterVlan.psd1 ` + -BaseDirectory "${PSScriptRoot}\en-US" +} +#endregion + +Function Get-TargetResource +{ + [CmdletBinding()] + [OutputType([System.Collections.Hashtable])] + Param ( + [Parameter(Mandatory)] + [String] $Id, + + [Parameter(Mandatory)] + [String] $Name, + + [Parameter(Mandatory)] + [String] $VMName + ) + + if(!(Get-Module -ListAvailable -Name Hyper-V)) + { + Throw $localizedData.HyperVModuleNotFound + } + + $configuration = @{ + Id = $Id + Name = $Name + VMName = $VMName + } + + $arguments = @{ + Name = $Name + } + + if ($VMName -ne 'ManagementOS') + { + $arguments.Add('VMName',$VMName) + } + else + { + $arguments.Add('ManagementOS', $true) + } + + try { + Write-Verbose $localizedData.GetVMNetAdapter + $adapterExists = Get-VMNetworkAdapter @arguments -ErrorAction SilentlyContinue + + if ($adapterExists) + { + Write-Verbose $localizedData.FoundVMNetAdapter + $configuration.Add('AdapterMode',$adapterExists.VlanSetting.OperationMode) + $configuration.Add('VlanId',$adapterExists.VlanSetting.AccessVlanId) + $configuration.Add('NativeVlanId',$adapterExists.VlanSetting.NativeVlanId) + $configuration.Add('PrimaryVlanId',$adapterExists.VlanSetting.PrimaryVlanId) + $configuration.Add('SecondaryVlanId',$adapterExists.VlanSetting.SecondaryVlanId) + $configuration.Add('SecondaryVlanIdList',$adapterExists.VlanSetting.SecondaryVlanIdListString) + $configuration.Add('AllowedVlanIdList',$adapterExists.VlanSetting.AllowedVlanIdListString) + } + } + catch + { + Write-Error $_ + } + + return $configuration +} + +Function Set-TargetResource +{ + [CmdletBinding()] + Param ( + [Parameter(Mandatory)] + [String] $Id, + + [Parameter(Mandatory)] + [String] $Name, + + [Parameter(Mandatory)] + [String] $VMName, + + [Parameter()] + [ValidateSet('Untagged','Access','Trunk','Communnity','Isolated','Promiscuous')] + [String] $AdapterMode = 'Untagged', + + [Parameter()] + [uint32] $VlanId, + + [Parameter()] + [uint32] $NativeVlanId, + + [Parameter()] + [String] $AllowedVlanIdList, + + [Parameter()] + [uint32] $PrimaryVlanId, + + [Parameter()] + [uint32] $SecondaryVlanId, + + [Parameter()] + [String] $SecondaryVlanIdList + ) + + if(!(Get-Module -ListAvailable -Name Hyper-V)) + { + Throw $localizedData.HyperVModuleNotFound + } + + $arguments = @{ + Name = $Name + } + + if ($VMName -ne 'ManagementOS') + { + $arguments.Add('VMName',$VMName) + } + else + { + $arguments.Add('ManagementOS', $true) + } + + try + { + Write-Verbose $localizedData.GetVMNetAdapter + $adapterExists = Get-VMNetworkAdapter @arguments -ErrorAction SilentlyContinue + if ($adapterExists) + { + Write-Verbose $localizedData.FoundVMNetAdapter + $setArguments = $arguments + $setArguments.Remove('Name') + $setArguments.Add('VMNetworkAdapterName',$Name) + switch ($AdapterMode) + { + 'Untagged' + { + $setArguments.Add('Untagged',$true) + break + } + + 'Access' + { + $setArguments.Add('Access',$true) + $setArguments.Add('VlanId',$VlanId) + break + } + + 'Trunk' + { + $setArguments.Add('Trunk',$true) + $setArguments.Add('NativeVlanId',$NativeVlanId) + if ($AllowedVlanIdList) + { + $setArguments.Add('AllowedVlanIdList',$AllowedVlanIdList) + } + break + } + + 'Community' + { + $setArguments.Add('Community',$true) + $setArguments.Add('PrimaryVlanId',$PrimaryVlanId) + if ($SecondaryVlanId) + { + $setArguments.Add('SecondaryVlanId',$SecondaryVlanId) + } + break + } + + 'Isolated' + { + $setArguments.Add('Isolated',$true) + $setArguments.Add('PrimaryVlanId',$PrimaryVlanId) + if ($SecondaryVlanId) + { + $setArguments.Add('SecondaryVlanId',$SecondaryVlanId) + } + break + } + + 'Promiscuous' + { + $setArguments.Add('Promiscuous',$true) + $setArguments.Add('PrimaryVlanId', $PrimaryVlanId) + if ($SecondaryVlanIdList) + { + $setArguments.Add('SecondaryVlanIdList', $SecondaryVlanIdList) + } + break + } + } + Write-Verbose $localizedData.PerformVMVlanSet + Set-VMNetworkAdapterVlan @setArguments -ErrorAction Stop + } + else + { + throw $localizedData.NoVMNetAdapterFound + } + } + catch + { + Write-Error $_ + } +} + +Function Test-TargetResource +{ + [CmdletBinding()] + [OutputType([System.Boolean])] + Param ( + [Parameter(Mandatory)] + [String] $Id, + + [Parameter(Mandatory)] + [String] $Name, + + [Parameter(Mandatory)] + [String] $VMName, + + [Parameter()] + [ValidateSet('Untagged','Access','Trunk','Communnity','Isolated','Promiscuous')] + [String] $AdapterMode = 'Untagged', + + [Parameter()] + [uint32] $VlanId, + + [Parameter()] + [uint32] $NativeVlanId, + + [Parameter()] + [String] $AllowedVlanIdList, + + [Parameter()] + [uint32] $PrimaryVlanId, + + [Parameter()] + [uint32] $SecondaryVlanId, + + [Parameter()] + [String] $SecondaryVlanIdList + ) + + if(!(Get-Module -ListAvailable -Name Hyper-V)) + { + Throw $localizedData.HyperVModuleNotFound + } + + $arguments = @{ + Name = $Name + } + + if ($VMName -ne 'ManagementOS') + { + $arguments.Add('VMName',$VMName) + } + else + { + $arguments.Add('ManagementOS', $true) + } + + switch ($AdapterMode) + { + 'Untagged' + { + if ($VlanId -or $NativeVlanId -or $PrimaryVlanId -or $SecondaryVlanId -or $AllowedVlanIdList -or $SecondaryVlanIdList) + { + Write-Verbose $localizedData.IgnoreVlan + } + break + } + + 'Access' + { + if (-not $VlanId) + { + throw $localizedData.VlanIdRequiredInAccess + } + break + } + + 'Trunk' + { + if (-not $NativeVlanId) + { + throw $localizedData.MustProvideNativeVlanId + } + break + } + + 'Community' + { + if (-not $PrimaryVlanId) + { + throw $localizedData.PrimaryVlanIdRequired + } + break + } + + 'Isolated' + { + if (-not $PrimaryVlanId) + { + throw $localizedData.PrimaryVlanIdRequired + } + break + } + + 'Promiscuous' + { + if (-not $PrimaryVlanId) + { + throw $localizedData.PrimaryVlanIdRequired + } + break + } + } + + try + { + #There is a remote timing issue that occurs when VLAN is set just after creating a VM Adapter. This needs more investigation. Sleep until then. + Start-Sleep -Seconds 10 + Write-Verbose $localizedData.GetVMNetAdapter + $adapterExists = Get-VMNetworkAdapter @arguments -ErrorAction SilentlyContinue + + if ($adapterExists) + { + Write-Verbose $localizedData.FoundVMNetAdapter + if ($adapterExists.VlanSetting.OperationMode -eq $AdapterMode) + { + switch ($adapterExists.VlanSetting.OperationMode) + { + 'Access' + { + if ($VlanId -ne $adapterExists.VlanSetting.AccessVlanId) + { + Write-Verbose $localizedData.AccessVlanMustChange + return $false + } + else + { + Write-Verbose $localizedData.AdaptersExistsWithVlan + return $true + } + break + } + + 'Trunk' + { + if ($NativeVlanId -ne $adapterExists.VlanSetting.NativeVlanId) + { + Write-Verbose $localizedData.NativeVlanMustChange + return $false + } + elseif ($AllowedVlanIdList -ne $AdapterMode.VlanSetting.AllowedVlanIdListString) + { + Write-Verbose $localizedData.AllowedVlanListMustChange + return $false + } + else + { + Write-Verbose $localizedData.AdaptersExistsWithVlan + return $true + } + break + } + + 'Untagged' + { + if ($AdapterMode -eq 'Untagged') + { + Write-Verbose $localizedData.AdaptersExistsWithVlan + Write-Verbose $localizedData.IgnoreVlan + return $true + } + break + } + + ('Community' -or 'isolated') + { + if ($PrimaryVlanId -ne $adapterExists.VlanSetting.PrimaryVlanId) + { + Write-Verbose $localizedData.PrimaryVlanMustChange + return $false + } + elseif ($SecondaryVlanId -ne $adapterExists.VlanSetting.SecondaryVlanId) + { + Write-Verbose $localizedData.SecondaryVlanMustChange + return $false + } + else + { + Write-Verbose $localizedData.AdaptersExistsWithVlan + return $true + } + break + } + + 'Promiscuous' + { + if ($PrimaryVlanId -ne $adapterExists.VlanSetting.PrimaryVlanId) + { + Write-Verbose $localizedData.PrimaryVlanMustChange + return $false + } + elseif ($SecondaryVlanIdList -ne $adapterExists.VlanSetting.SecondaryVlanIdListString) + { + Write-Verbose $localizedData.SecondaryVlanListMustChange + return $false + } + else + { + Write-Verbose $localizedData.AdaptersExistsWithVlan + return $true + } + } + } + } + else + { + Write-Verbose $localizedData.AdapterExistsWithDifferentVlanMode + return $false + } + } + else + { + throw $localizedData.VMNetAdapterDoesNotExist + } + } + catch + { + Write-Error $_ + } +} + +Export-ModuleMember -Function *-TargetResource diff --git a/deployment/dsc/azshcihost/cHyper-V/3.0.0.0/DSCResources/cVMNetworkAdapterVlan/cVMNetworkAdapterVlan.schema.mof b/deployment/dsc/azshcihost/cHyper-V/3.0.0.0/DSCResources/cVMNetworkAdapterVlan/cVMNetworkAdapterVlan.schema.mof new file mode 100644 index 0000000..5b4eeaa Binary files /dev/null and b/deployment/dsc/azshcihost/cHyper-V/3.0.0.0/DSCResources/cVMNetworkAdapterVlan/cVMNetworkAdapterVlan.schema.mof differ diff --git a/deployment/dsc/azshcihost/cHyper-V/3.0.0.0/DSCResources/cVMNetworkAdapterVlan/en-US/cVMNetworkAdapterVlan.psd1 b/deployment/dsc/azshcihost/cHyper-V/3.0.0.0/DSCResources/cVMNetworkAdapterVlan/en-US/cVMNetworkAdapterVlan.psd1 new file mode 100644 index 0000000..5edc6ad --- /dev/null +++ b/deployment/dsc/azshcihost/cHyper-V/3.0.0.0/DSCResources/cVMNetworkAdapterVlan/en-US/cVMNetworkAdapterVlan.psd1 @@ -0,0 +1,22 @@ +ConvertFrom-StringData @' + HyperVModuleNotFound=Hyper-V PowerShell Module not found. + VMNameAndManagementTogether=VMName cannot be provided when ManagementOS is set to True. + MustProvideVMName=Must provide VMName parameter when ManagementOS is set to False. + GetVMNetAdapter=Getting VM Network Adapter information. + FoundVMNetAdapter=Found VM Network Adapter. + NoVMNetAdapterFound=No VM Network Adapter found. + VMNetAdapterDoesNotExist=VM Network Adapter does not exist. + PerformVMVlanSet=Perfoming VM Network Adapter VLAN setting configuration. + IgnoreVlan=Ignoring VLAN configuration when the opeartion mode chosen is Untagged. + VlanIdRequiredInAccess=VlanId must be specified when chosen operation mode is Access. + MustProvideNativeVlanId=NativeVlanId must be specified when chosen operation mode is Trunk. + PrimaryVlanIdRequired=PrimaryVlanId is required when the chosen operation mode is Community or Isolated or Promiscuous. + AccessVlanMustChange=VlanId in Access mode is different. It will be changed. + AdaptersExistsWithVlan=VM Network adapter exists with required VLAN configuration. + NativeVlanMustChange=NativeVlanId in Trunk mode is different and it wil be changed. + AllowedVlanListMustChange=AllowedVlanIdList is different in trunk mode. It will be changed. + PrimaryVlanMustChange=PrimaryVlanId is different and must be changed. + SecondaryVlanMustChange=SecondaryVlanId is different and must be changed. + SecondaryVlanListMustChange=SecondaryVlanIdList is different and must be changed. + AdapterExistsWithDifferentVlanMode=VM Network adapter exists with different Vlan configuration. It will be fixed. +'@ diff --git a/deployment/dsc/azshcihost/cHyper-V/3.0.0.0/DSCResources/cVMSwitch/README.md b/deployment/dsc/azshcihost/cHyper-V/3.0.0.0/DSCResources/cVMSwitch/README.md new file mode 100644 index 0000000..becbf50 --- /dev/null +++ b/deployment/dsc/azshcihost/cHyper-V/3.0.0.0/DSCResources/cVMSwitch/README.md @@ -0,0 +1,98 @@ +#cVMSwitch PowerShell DSC Resource# +This resource module is a fork from Microsoft's xHyper-V resource module. I have added the capability to manage bandwidth settings of the VM switch and to deploy Switch Embedded teaming in Windows Server 2016. + +![](http://i.imgur.com/odgNbD3.png) + +When using this DSC resource, the *Name* and *Type* are mandatory properties where *Name* is the unique key properties. + +The *AllowManagementOS* property can be used to add a VM network adapter attached to the VM switch we are creating in the management OS. + +The *EnableIoV* property lets us enable SR-IOV capability on the VM switch. + +The *MinimumBandwidthMode* and *EnableIoV* properties are mutually exclusive. We cannot configure both at the same time. + +The *MinimumBandwidthMode* property can be used to configure a converged network switch on Hyper-V. + +The *NetAdapterName* parameter is used when creating a VM switch of *External* type. If you pass multiple adapter names to this parameter, a switch embedded team will be provisioned. You can specify a comma-separated list of physical network adapters. Although, it is technically possible to create a SET team with just one adapter, from a design point of view, I simplified this by putting a constraint on at least two adapters to create a SET team. + +The *LoadBalancingAlgorigthm* specifies the LB mode for the SET team. At the time of writing this resource module, SET supports only *SwitchIndepedent* load balancing algorithm. This applies only when multiple network adapters are specified. + +The *TeamingMode* can be set of either *HyperVPort* or *Dynamic*. The default is *Dynamic*. This applies only when multiple network adapters are specified. + +The following examples demonstrate how to use this resource module. +### Create a simple VM Switch from a native network team on the Hyper-V host ### + Configuration SimpleHostTeamvSwitch + { + Import-DscResource -ModuleName cHyper-V -Name cVMSwitch + Import-DscResource -ModuleName PSDesiredStateConfiguration + + cVMSwitch HostSwitch { + Name = 'HostSwitch' + Type = 'External' + AllowManagementOS = $true + MinimumBandwidthMode = 'Weight' + NetAdapterName = 'HostTeam' + Ensure = 'Present' + } + } + +## Create a simple VM Switch using a network adapter on the Hyper-V host ## + Configuration SimpleNetAdaptervSwitch + { + Import-DscResource -ModuleName cHyper-V -Name cVMSwitch + Import-DscResource -ModuleName PSDesiredStateConfiguration + + cVMSwitch HostSwitch { + Name = 'HostSwitch' + Type = 'External' + AllowManagementOS = $true + NetAdapterName = 'NIC1' + Ensure = 'Present' + } + } + +## Create a Switch Embedded Team switch using four network adapters on the Hyper-V host ## + Configuration SETTeamSwitch + { + Import-DscResource -ModuleName cHyper-V -Name cVMSwitch + Import-DscResource -ModuleName PSDesiredStateConfiguration + + cVMSwitch HostSwitch { + Name = 'HostSwitch' + Type = 'External' + AllowManagementOS = $true + MinimumBandwidthMode = 'Weight' + TeamingMode = 'SwitchIndependent' + LoadBalancingAlgorithm = 'HyperVPort' + NetAdapterName = 'NIC1','NIC2','NIC3','NIC4' + Ensure = 'Present' + } + } + +## Create a private VM switch on the Hyper-V host ## + Configuration PrivateSwitch + { + Import-DscResource -ModuleName cHyper-V -Name cVMSwitch + Import-DscResource -ModuleName PSDesiredStateConfiguration + + cVMSwitch HostSwitch { + Name = 'HostSwitch' + Type = 'Private' + Ensure = 'Present' + } + } + +## Create a internal VM switch on the Hyper-V host ## + Configuration InternalSwitch + { + Import-DscResource -ModuleName cHyper-V -Name cVMSwitch + Import-DscResource -ModuleName PSDesiredStateConfiguration + + cVMSwitch HostSwitch { + Name = 'HostSwitch' + Type = 'Internal' + Ensure = 'Present' + } + } + +To remove any of these switches, you can simply switch the *Ensure* property to *Absent*. \ No newline at end of file diff --git a/deployment/dsc/azshcihost/cHyper-V/3.0.0.0/DSCResources/cVMSwitch/cVMSwitch.psm1 b/deployment/dsc/azshcihost/cHyper-V/3.0.0.0/DSCResources/cVMSwitch/cVMSwitch.psm1 new file mode 100644 index 0000000..f804cc8 --- /dev/null +++ b/deployment/dsc/azshcihost/cHyper-V/3.0.0.0/DSCResources/cVMSwitch/cVMSwitch.psm1 @@ -0,0 +1,618 @@ +#region localizeddata +if (Test-Path "${PSScriptRoot}\${PSUICulture}") +{ + Import-LocalizedData -BindingVariable localizedData -filename cVMSwitch.psd1 ` + -BaseDirectory "${PSScriptRoot}\${PSUICulture}" +} +else +{ + #fallback to en-US + Import-LocalizedData -BindingVariable localizedData -filename cVMSwitch.psd1 ` + -BaseDirectory "${PSScriptRoot}\en-US" +} +#endregion + +function Get-TargetResource +{ + [CmdletBinding()] + [OutputType([System.Collections.Hashtable])] + param + ( + [parameter(Mandatory)] + [String]$Name, + + [parameter(Mandatory)] + [ValidateSet("External","Internal","Private")] + [String]$Type + ) + + if(!(Get-Module -ListAvailable -Name Hyper-V)) + { + Throw $localizedData.HyperVModuleNotFound + } + + $configuration = @{ + Name = $Name + Type = $Type + } + + $switch = Get-VMSwitch -Name $Name -SwitchType $Type -ErrorAction SilentlyContinue + if ($switch) + { + Write-Verbose -Message $localizedData.FoundSwitch + if ($switch.SwitchType -eq 'External') + { + Write-Verbose -Message $localizedData.FoundExternalSwitch + + #SET specific properties for External switch type + if ($switch.EmbeddedTeamingEnabled) + { + Write-Verbose -Message $localizedData.FoundSetTeam + $switchTeam = Get-VMSwitchTeam -Name $Name + + $netAdapterName = $( + $switchTeam.NetAdapterInterfaceDescription | + Foreach-Object { + (Get-NetAdapter -InterfaceDescription $_).Name + } + ) + $configuration.Add('TeamingMode',$switchTeam.TeamingMode) + $configuration.Add('LoadBalancingAlgorithm',$switchTeam.LoadBalancingAlgorithm) + } + else + { + $netAdapterName = $( + if($switch.NetAdapterInterfaceDescription) + { + (Get-NetAdapter -InterfaceDescription $switch.NetAdapterInterfaceDescription).Name + } + ) + } + } + else + { + Write-Verbose -Message ($localizedData.FoundIntORPvtSwitch -f $switch.SwitchType) + } + + $configuration.Add('NetAdapterName', $netAdapterName) + $configuration.Add('NetAdapterInterfaceDescription',$switch.NetAdapterInterfaceDescriptions) + $configuration.Add('EmbeddedTeamingEnabled',$switch.EmbeddedTeamingEnabled) + $configuration.Add('AllowManagementOS',$switch.AllowManagementOS) + $configuration.Add('Id',$switch.Id) + $configuration.Add('EnableIoV',$switch.IovEnabled) + $configuration.Add('EnablePacketDirect',$switch.PacketDirectEnabled) + $configuration.Add('MinimumBandwidthMode',$switch.BandwidthReservationMode) + $configuration.Add('Ensure','Present') + } + else + { + Write-Verbose -Message $localizedData.NoSwitchFound + $configuration.Add('Ensure','Absent') + } + + return $configuration +} + +function Set-TargetResource +{ + [CmdletBinding()] + param + ( + [parameter(Mandatory)] + [String] $Name, + + [parameter(Mandatory)] + [ValidateSet('External','Internal','Private')] + [String] $Type, + + [Parameter()] + [ValidateNotNullOrEmpty()] + [String[]] $NetAdapterName, + + [Parameter()] + [Boolean] $AllowManagementOS, + + [Parameter()] + [Boolean] $EnableIov, + + [Parameter()] + [ValidateSet('None', 'Default', 'Weight', 'Absolute')] + [String] $MinimumBandwidthMode='Absolute', + + [parameter()] + [ValidateSet('SwitchIndependent')] + [String] $TeamingMode, + + [parameter()] + [ValidateSet('Dynamic','HyperVPort')] + [String] $LoadBalancingAlgorithm, + + [Parameter()] + [Boolean]$EnablePacketDirect, + + [ValidateSet("Present","Absent")] + [String] $Ensure = "Present" + ) + + # Check if Hyper-V module is present for Hyper-V cmdlets + if(!(Get-Module -ListAvailable -Name Hyper-V)) + { + Throw $localizedData.HyperVModuleNotFound + } + + if ((($Type -eq 'Internal') -or ($Type -eq 'Private')) -and $AllowManagementOS) + { + throw $localizedData.InternalPrivateWithAllowManagementOS + } + + if ($Type -eq 'External' -and !($NetAdapterName)) + { + throw $localizedData.NetAdapterNameForExternal + } + + if ($Type -ne 'External' -and $NetAdapterName) + { + throw $localizedData.NoNetAdapterInternalPrivate + } + + if (($TeamingMode -or $LoadBalancingAlgorithm) -and ($Type -ne 'External')) + { + throw $localizedData.NoSETForInternalPrivate + } + + if ($EnableIov -and $EnablePacketDirect) + { + throw $localizedData.IOVPDTogether + } + + if ($MinimumBandwidthMode -and ($EnableIov -and ($NetAdapterName.Count -gt 1))) + { + throw $localizedData.IOVMBwithSET + } + + if ($MinimumBandwidthMode -and ($EnablePacketDirect -and ($NetAdapterName.Count -gt 1))) + { + throw $localizedData.PDMBwithSET + } + + if($Ensure -eq 'Present') + { + $switch = (Get-VMSwitch -Name $Name -SwitchType $Type -ErrorAction SilentlyContinue) + + # If switch is present and it is external type, that means it doesn't have right properties (TEST code ensures that) + if($switch) + { + Write-Verbose -Message $localizedData.FoundSwitch + if ($switch.SwitchType -eq 'External') + { + Write-Verbose -Message $localizedData.FoundExternalSwitch + + #Check if there are multiple network adapters specified; it should be a SET Team + if ($NetAdapterName.Count -gt 1) + { + #We need a SET Team + Write-Verbose -Message $localizedData.NeedASET + if (-not $switch.EmbeddedTeamingEnabled) + { + #We dont have a SET team; delete and re-create the team + Write-Verbose -Message $localizedData.ReCreateSET + + $switch | Remove-VMSwitch -Force + $arguments = @{ + Name = $Name + NetAdapterName = $NetAdapterName + MinimumBandwidthMode = $MinimumBandwidthMode + } + + if ($PSBoundParameters.ContainsKey('AllowManagementOS')) + { + $arguments['AllowManagementOS']=$AllowManagementOS + } + + if ($PSBoundParameters.ContainsKey('TeamingMode')) + { + $arguments['TeamingMode']=$TeamingMode + } + + if ($PSBoundParameters.ContainsKey('LoadBalancingAlgorithm')) + { + $arguments['LoadBalancingAlgorithm']=$LoadBalancingAlgorithm + } + + if ($PSBoundParameters.ContainsKey("EnableIov")) + { + $arguments['EnableIov']=$EnableIov + } + + if ($PSBoundParameters.ContainsKey("EnablePacketDirect")) + { + $arguments['EnablePacketDirect']=$EnablePacketDirect + } + + Write-Verbose -Message $localizedData.CreateSwitch + $null = New-VMSwitch @arguments + } + else + { + #We have a SET Team; check network adapters and other properties + Write-Verbose -Message $localizedData.SETFoundCheckNetAdapter + + $switchTeam = Get-VMSwitchTeam -VMSwitch $switch + $existngNetAdapters = $switchTeam.NetAdapterInterfaceDescription | + ForEach-Object { + (Get-NetAdapter -InterfaceDescription $_).Name + } + $switchTeamMembers = Compare-Object -ReferenceObject $NetAdapterName -DifferenceObject $existngNetAdapters + + $setTeamArguments = @{ + VMSwitch = $switch + } + + if ($null -ne $switchTeamMembers) + { + #We have a difference in the compared objects + Write-Verbose -Message $localizedData.SETMembersDontMatch + + $setTeamArguments['NetAdapterName'] = $NetAdapterName + $updateTeam = $true + } + + #check other propeties of the SET Team as well + if ($PSBoundParameters.ContainsKey('TeamingMode')) + { + if ($switchTeam.TeamingMode -ne $TeamingMode) + { + $setTeamArguments['TeamingMode']=$TeamingMode + $updateTeam = $true + } + } + + if ($PSBoundParameters.ContainsKey('LoadBalancingAlgorithm')) + { + if ($switchTeam.LoadBalancingAlgorithm -ne $LoadBalancingAlgorithm) + { + $setTeamArguments['LoadBalancingAlgorithm']=$LoadBalancingAlgorithm + $updateTeam = $true + } + } + + if ($updateTeam) + { + Write-Verbose -Message $localizedData.UpdateSETTeam + $null = Set-VMSwitchTeam @setTeamArguments + } + + #Finally, check if if we need set AllowManagementOS + if($PSBoundParameters.ContainsKey("AllowManagementOS")) + { + Write-Verbose -Message "Checking if Switch $Name has AllowManagementOS set correctly..." + if(($switch.AllowManagementOS -ne $AllowManagementOS)) + { + Write-Verbose -Message $localizedData.UpdateSwitch + $null = Set-VMSwitch -VMSwitch $switch -AllowManagementOS $AllowManagementOS + } + } + } + } + else + { + #We don't need a SET Team + if ($switch.EmbeddedTeamingEnabled) + { + #We have SET team; need to delete and re-create a normal switch + Write-Verbose -Message $localizedData.NeedANormalSwitch + Write-Verbose -Message $localizedData.RemovingSwitch + $switch | Remove-VMSwitch -Force + + $switchArguments = @{ + Name = $Name + NetAdapterName = $NetAdapterName + MinimumBandwidthMode = $MinimumBandwidthMode + } + + if ($PSBoundParameters.ContainsKey('AllowManagementOS')) + { + $switchArguments['AllowManagementOS']=$AllowManagementOS + } + + if ($PSBoundParameters.ContainsKey("EnableIov")) + { + $switchArguments['EnableIov']=$EnableIov + } + if ($PSBoundParameters.ContainsKey("EnablePacketDirect")) + { + $switchArguments['EnablePacketDirect']=$EnablePacketDirect + } + + Write-Verbose -Message $localizedData.CreateSwitch + $null = New-VMSwitch @switchArguments + } + else + { + #We have a normal switch; Check other properties + $switchUpdateArguments = @{ + VMSwitch = $switch + } + + if((Get-NetAdapter -Name $NetAdapterName).InterfaceDescription -ne $switch.NetAdapterInterfaceDescription) + { + #Network Adapter is not matching; we can set this without deleting the switch + $switchUpdateArguments['NetAdapterName'] = $NetAdapterName + $updateSwitch = $true + } + + if($PSBoundParameters.ContainsKey("AllowManagementOS")) + { + Write-Verbose -Message "Checking if Switch $Name has AllowManagementOS set correctly..." + if(($switch.AllowManagementOS -ne $AllowManagementOS)) + { + $switchUpdateArguments['AllowManagementOS'] = $AllowManagementOS + $updateSwitch = $true + } + } + + if ($updateSwitch) + { + Write-Verbose -Message $localizedData.UpdateSwitch + $null = Set-VMSwitch @switchUpdateArguments + } + } + } + } + else + { + #We have an internal or private switch; we cannot update any properties + Write-Verbose -Message $localizedData.WeShouldNeverReachHere + } + } + else + { + # If the switch is not present, create one + $parameters = @{} + $parameters["Name"] = $Name + if($NetAdapterName) + { + $parameters["NetAdapterName"] = $NetAdapterName + $parameters["MinimumBandwidthMode"] = $MinimumBandwidthMode + if($PSBoundParameters.ContainsKey("AllowManagementOS")) + { + $parameters["AllowManagementOS"] = $AllowManagementOS + } + } + else + { + $parameters["SwitchType"] = $Type + } + + Write-Verbose -Message $localizedData.CreateSwitch + $null = New-VMSwitch @parameters + } + } + # Ensure is set to "Absent", remove the switch + else + { + Write-Verbose -Message $localizedData.RemovingSwitch + Get-VMSwitch $Name -ErrorAction SilentlyContinue | Remove-VMSwitch -Force + } +} + +function Test-TargetResource +{ + [CmdletBinding()] + [OutputType([System.Boolean])] + param + ( + [parameter(Mandatory)] + [String] $Name, + + [parameter(Mandatory)] + [ValidateSet('External','Internal','Private')] + [String] $Type, + + [Parameter()] + [ValidateNotNullOrEmpty()] + [String[]] $NetAdapterName, + + [Parameter()] + [Boolean] $AllowManagementOS, + + [Parameter()] + [Boolean] $EnableIov, + + [Parameter()] + [ValidateSet('None', 'Default', 'Weight', 'Absolute')] + [String] $MinimumBandwidthMode='Absolute', + + [parameter()] + [ValidateSet('SwitchIndependent')] + [String] $TeamingMode, + + [parameter()] + [ValidateSet('Dynamic','HyperVPort')] + [String] $LoadBalancingAlgorithm, + + [Parameter()] + [Boolean]$EnablePacketDirect, + + [ValidateSet("Present","Absent")] + [String] $Ensure = "Present" + ) + + # Check if Hyper-V module is present for Hyper-V cmdlets + if(!(Get-Module -ListAvailable -Name Hyper-V)) + { + Throw $localizedData.HyperVModuleNotFound + } + + if ((($Type -eq 'Internal') -or ($Type -eq 'Private')) -and $AllowManagementOS) + { + throw $localizedData.InternalPrivateWithAllowManagementOS + } + + if ($Type -eq 'External' -and !($NetAdapterName)) + { + throw $localizedData.NetAdapterNameForExternal + } + + if ($Type -ne 'External' -and $NetAdapterName) + { + throw $localizedData.NoNetAdapterInternalPrivate + } + + if (($TeamingMode -or $LoadBalancingAlgorithm) -and ($Type -ne 'External')) + { + throw $localizedData.NoSETForInternalPrivate + } + + if ($EnableIov -and $EnablePacketDirect) + { + throw $localizedData.IOVPDTogether + } + + if ($MinimumBandwidthMode -and ($EnableIov -and ($NetAdapterName.Count -gt 1))) + { + throw $localizedData.IOVMBwithSET + } + + if ($MinimumBandwidthMode -and ($EnablePacketDirect -and ($NetAdapterName.Count -gt 1))) + { + throw $localizedData.PDMBwithSET + } + + try + { + $switch = Get-VMSwitch -Name $Name -SwitchType $Type -ErrorAction Stop + + if($switch) + { + Write-Verbose -Message $localizedData.FoundSwitch + if ($Ensure -eq 'Present') + { + if ($NetAdapterName.Count -gt 1) + { + Write-Verbose -Message $localizedData.NeedASET + #We need a SET team + if (-not $switch.EmbeddedTeamingEnabled) + { + Write-Verbose -Message $localizedData.ReCreateSET + return $false + } + else + { + #We have a SET team; need to compare the properties + $switchTeam = Get-VMSwitchTeam -Name $Name + + if ($Type -eq 'External') + { + #Compare network adapters in the SET + Write-Verbose -Message $localizedData.SETFoundCheckNetAdapter + $existngNetAdapters = $switchTeam.NetAdapterInterfaceDescription | + ForEach-Object { + (Get-NetAdapter -InterfaceDescription $_).Name + } + $switchTeamMembers = Compare-Object -ReferenceObject $NetAdapterName ` + -DifferenceObject $existngNetAdapters + + if ($null -ne $switchTeamMembers) + { + #We have a difference in the compared objects + Write-Verbose -Message $localizedData.SETMembersDontMatch + return $false + } + } + + if ($switchTeam.LoadBalancingAlgorithm -ne $LoadBalancingAlgorithm) + { + Write-Verbose -Message $localizedData.LBDifferent + return $false + } + + if ($switchTeam.TeamingMode -ne $TeamingMode) + { + Write-Verbose -Message $localizedData.TeamingDifferent + return $false + } + } + } + else + { + #We need a normal VM switch + if ($switch.EmbeddedTeamingEnabled) + { + Write-Verbose -Message $localizedData.NeedANormalSwitch + return $false + } + + if ($Type -eq 'External') + { + if((Get-NetAdapter -Name $NetAdapterName -ErrorAction SilentlyContinue).InterfaceDescription -ne $switch.NetAdapterInterfaceDescription) + { + Write-Verbose -Message $localizedData.NetAdapterDifferent + return $false + } + } + } + + #Check for common properties + if($PSBoundParameters.ContainsKey("AllowManagementOS") -and $Type -eq 'External') + { + if(($switch.AllowManagementOS -ne $AllowManagementOS)) + { + Write-Verbose -Message $localizedData.AllowMgmtOSDifferent + return $false + } + } + + if ($EnablePacketDirect) + { + if (-not $switch.EnablePacketDirect) { + Write-Warning -Message $localizedData.EPDCannotChange + } + } + + if ($EnableIov) + { + if (-not $switch.EnableIov) { + Write-Warning -Message $localizedData.IOVCannotChange + } + } + + if ($MinimumBandwidthMode -ne $switch.BandwidthReservationMode) + { + Write-Warning -Message $localizedData.MBCannotChange + } + + #If we have reached this far, the switch exists with necessary configuration + Write-Verbose -Message $localizedData.SwitchExistsNoAction + return $true + } + else + { + Write-Verbose -Message $localizedData.SwitchExistsItShouldnot + return $false + } + } + else + { + if ($Ensure -eq 'Present') + { + Write-Verbose -Message $localizedData.SwitchShouldExist + return $false + } + else + { + Write-Verbose -Message $localizedData.SwitchDoesNotExistNoAction + return $true + } + } + } + + # If no switch was present + catch [System.Management.Automation.ActionPreferenceStopException] + { + Write-Verbose -Message $localizedData.NoSwitchFound + return ($Ensure -eq 'Absent') + } +} + +Export-ModuleMember -Function *-TargetResource diff --git a/deployment/dsc/azshcihost/cHyper-V/3.0.0.0/DSCResources/cVMSwitch/cVMSwitch.schema.mof b/deployment/dsc/azshcihost/cHyper-V/3.0.0.0/DSCResources/cVMSwitch/cVMSwitch.schema.mof new file mode 100644 index 0000000..c82668f Binary files /dev/null and b/deployment/dsc/azshcihost/cHyper-V/3.0.0.0/DSCResources/cVMSwitch/cVMSwitch.schema.mof differ diff --git a/deployment/dsc/azshcihost/cHyper-V/3.0.0.0/DSCResources/cVMSwitch/en-US/cVMSwitch.psd1 b/deployment/dsc/azshcihost/cHyper-V/3.0.0.0/DSCResources/cVMSwitch/en-US/cVMSwitch.psd1 new file mode 100644 index 0000000..3fa5ace --- /dev/null +++ b/deployment/dsc/azshcihost/cHyper-V/3.0.0.0/DSCResources/cVMSwitch/en-US/cVMSwitch.psd1 @@ -0,0 +1,36 @@ +ConvertFrom-StringData @' + HyperVModuleNotFound=Hyper-V Module was not found on this system. + FoundSetTeam=VM Switch is an embedded team. + NoSwitchFound=No VM Switch provided details found. + FoundSwitch=Found a VM switch with provided details. + FoundExternalSwitch=VM switch is an external switch. + InternalPrivateWithAllowManagementOS=AllowManagementOS cannot be specified when the switch type is set to Internal or Private. + NetAdapterNameForExternal=For external switch type, NetAdapterName must be specified. + NoNetAdapterInternalPrivate=For Internal or Private switch type, NetAdapterName should not be specified. + NoSETForInternalPrivate=In this release, TeamingMode and/or LoadBalancingAlgorithm can be set only when Switch type is external. + IOVPDTogether=EnablePacketDirect and EnableIoV cannot specified together. + IOVMBwithSET=EnableIOV and SET cannot be cannot specified when using MinimumBandwidthMode. + PDMBwithSET=EnablePacketDirect and EnableEmbeddedTeaming cannot specified when using MinimumBandwidthMode. + FoundIntORPvtSwitch=Found a VM switch of type {0} + NeedASET=Multiple Network Adapters specified. We need a SET Team. + ReCreateSET=Switch found is not a SET Team. We need to recreate it. + CreateSwitch=Creating a new VM switch. + SETFoundCheckNetAdapter=SET Team found. Checking network adapter members. + SETMembersDontMatch=SET Team members not matching. We need to update the VM Switch. + UpdateSETTeam=Updating SET Team. + NeedANormalSwitch=A SET team was found. We need a normal switch. + RemovingSwitch=Removing VM switch. + UpdateSwitch=Updating VM Switch properties. + WeShouldNeverReachHere=VM Switch is either internal or private. The control should never reach here. + LBDifferent=Load Balancing mode different. + TeamingDifferent=Teaming mode is different. + NetAdapterDifferent=Network adapter did not match. + AllowMgmtOSDifferent=Allow Management OS property does not match. + EPDCannotChange=PacketDirect is not enabled and this configuration cannot be updated once switch is created unless we destroy and re-create. Consider doing this manually. + IOVCannotChange=IOV is not enabled and this configuration cannot be updated once switch is created unless we destroy and re-create. Consider doing this manually. + MBCannotChange=MinimumBandwidthMode is not matching and this configuration cannot be updated once switch is created unless we destroy and re-create. Consider doing this manually. + SwitchExistsNoAction=Switch exists. No action needed. + SwitchExistsItShouldnot=Switch exists while it should not. + SwitchShouldExist=Switch does not exist while it should. + SwitchDoesNotExistNoAction=Switch does not exist. No action needed. +'@ diff --git a/deployment/dsc/azshcihost/cHyper-V/3.0.0.0/DSCResources/cWaitForVMGuestIntegration/README.md b/deployment/dsc/azshcihost/cHyper-V/3.0.0.0/DSCResources/cWaitForVMGuestIntegration/README.md new file mode 100644 index 0000000..7026b12 --- /dev/null +++ b/deployment/dsc/azshcihost/cHyper-V/3.0.0.0/DSCResources/cWaitForVMGuestIntegration/README.md @@ -0,0 +1,42 @@ +#cWaitForVMGuestIntegration# +This DSC resource is especially helpful when a VM resource configuration requires that the VM integration components are up and running. You can use this resource to wait for the guest integration components. For example, to be able to inject an IP address (using cVMIPAddress DSC resource) into a newly created Windows VM, the VM should have integration components installed and running. In such a configuration scenario, you can use this resource to wait until the integration components change into running state. + +![](http://i.imgur.com/rtiyk4B.png) + +The *Id* property is an instance identifier and key property in the resource configuration. This does not identify any VM property instead provides a way to wait for the integration components of that VM multiple times in a single configuration document. + +The *VMName* property identifies the virtual machine in which the guest integration components should be in running state. This is a mandatory property. + +The *RetryCount* property identifies how many times the resource should try to test for guest integration component state. Default value is 5. This is an optional property. + +The *RetryIntervalSec* property identifies interval (in seconds) between retries. Default Value is 10 seconds. This is an optional property. + +Here are some examples that demonstrates how to use this resource. + +##Wait For VM IC with default retry values## +Configuration WaitForIC + { + Import-DscResource -Name cWaitForVMGuestIntegration -ModuleName cHyper-V + + cWaitForVMGuestIntegration VM01 + { + Id = 'VM01-IC01' + VMName = 'VM01' + } + } + +##Wait for VM IC with custom retry values## + Configuration WaitForIC + { + Import-DscResource -Name cWaitForVMGuestIntegration -ModuleName cHyper-V + + cWaitForVMGuestIntegration VM01 + { + Id = 'VM01-IC01' + VMName = 'VM01' + RetryIntervalSec = 20 + RetryCount = 10 + } + } + + \ No newline at end of file diff --git a/deployment/dsc/azshcihost/cHyper-V/3.0.0.0/DSCResources/cWaitForVMGuestIntegration/cWaitForVMGuestIntegration.psm1 b/deployment/dsc/azshcihost/cHyper-V/3.0.0.0/DSCResources/cWaitForVMGuestIntegration/cWaitForVMGuestIntegration.psm1 new file mode 100644 index 0000000..1c7107a Binary files /dev/null and b/deployment/dsc/azshcihost/cHyper-V/3.0.0.0/DSCResources/cWaitForVMGuestIntegration/cWaitForVMGuestIntegration.psm1 differ diff --git a/deployment/dsc/azshcihost/cHyper-V/3.0.0.0/DSCResources/cWaitForVMGuestIntegration/cWaitForVMGuestIntegration.schema.mof b/deployment/dsc/azshcihost/cHyper-V/3.0.0.0/DSCResources/cWaitForVMGuestIntegration/cWaitForVMGuestIntegration.schema.mof new file mode 100644 index 0000000..5e5aa8e Binary files /dev/null and b/deployment/dsc/azshcihost/cHyper-V/3.0.0.0/DSCResources/cWaitForVMGuestIntegration/cWaitForVMGuestIntegration.schema.mof differ diff --git a/deployment/dsc/azshcihost/cHyper-V/3.0.0.0/DSCResources/cWaitForVMGuestIntegration/en-US/cWaitForVMGuestIntegration.psd1 b/deployment/dsc/azshcihost/cHyper-V/3.0.0.0/DSCResources/cWaitForVMGuestIntegration/en-US/cWaitForVMGuestIntegration.psd1 new file mode 100644 index 0000000..a93c312 Binary files /dev/null and b/deployment/dsc/azshcihost/cHyper-V/3.0.0.0/DSCResources/cWaitForVMGuestIntegration/en-US/cWaitForVMGuestIntegration.psd1 differ diff --git a/deployment/dsc/azshcihost/cHyper-V/3.0.0.0/Examples/Resources/cVMIPAddress/ResetIPAddress.Example.ps1 b/deployment/dsc/azshcihost/cHyper-V/3.0.0.0/Examples/Resources/cVMIPAddress/ResetIPAddress.Example.ps1 new file mode 100644 index 0000000..0de6ebe --- /dev/null +++ b/deployment/dsc/azshcihost/cHyper-V/3.0.0.0/Examples/Resources/cVMIPAddress/ResetIPAddress.Example.ps1 @@ -0,0 +1,12 @@ +Configuration VMIPAddress +{ + Import-DscResource -ModuleName cHyper-V -Name cVMIPAddress + Import-DscResource -ModuleName PSDesiredStateConfiguration + + cVMIPAddress VMAdapter1IPAddress { + Id = 'VMMgmt-NIC' + NetAdapterName = 'VMMgmt-NIC' + VMName = 'SQLVM01' + IPAddress = 'DHCP' + } +} diff --git a/deployment/dsc/azshcihost/cHyper-V/3.0.0.0/Examples/Resources/cVMIPAddress/SetIPAddress.Example.ps1 b/deployment/dsc/azshcihost/cHyper-V/3.0.0.0/Examples/Resources/cVMIPAddress/SetIPAddress.Example.ps1 new file mode 100644 index 0000000..7f6a7a5 --- /dev/null +++ b/deployment/dsc/azshcihost/cHyper-V/3.0.0.0/Examples/Resources/cVMIPAddress/SetIPAddress.Example.ps1 @@ -0,0 +1,15 @@ +Configuration VMIPAddress +{ + Import-DscResource -ModuleName cHyper-V -Name cVMIPAddress + Import-DscResource -ModuleName PSDesiredStateConfiguration + + cVMIPAddress VMAdapter1IPAddress { + Id = 'VMMgmt-NIC' + NetAdapterName = 'VMMgmt-NIC' + VMName = 'SQLVM01' + IPAddress = '172.16.101.101' + DefaultGateway = '172.16.101.1' + Subnet = '255.255.255.0' + DnsServer = '172.16.101.2' + } +} diff --git a/deployment/dsc/azshcihost/cHyper-V/3.0.0.0/Examples/Resources/cVMNetworkAdapter/CreateVMNetworkAdapterManagementOS.Example.ps1 b/deployment/dsc/azshcihost/cHyper-V/3.0.0.0/Examples/Resources/cVMNetworkAdapter/CreateVMNetworkAdapterManagementOS.Example.ps1 new file mode 100644 index 0000000..d939de4 --- /dev/null +++ b/deployment/dsc/azshcihost/cHyper-V/3.0.0.0/Examples/Resources/cVMNetworkAdapter/CreateVMNetworkAdapterManagementOS.Example.ps1 @@ -0,0 +1,13 @@ +Configuration HostOSAdapter +{ + Import-DscResource -ModuleName cHyper-V -Name cVMNetworkAdapter + Import-DscResource -ModuleName PSDesiredStateConfiguration + + cVMNetworkAdapter HostOSAdapter { + Id = 'Management-NIC' + Name = 'Management-NIC' + SwitchName = 'SETSwitch' + VMName = 'ManagementOS' + Ensure = 'Present' + } +} diff --git a/deployment/dsc/azshcihost/cHyper-V/3.0.0.0/Examples/Resources/cVMNetworkAdapter/CreateVMNetworkAdapterMultipleManagementOS.Example.ps1 b/deployment/dsc/azshcihost/cHyper-V/3.0.0.0/Examples/Resources/cVMNetworkAdapter/CreateVMNetworkAdapterMultipleManagementOS.Example.ps1 new file mode 100644 index 0000000..bf031b5 --- /dev/null +++ b/deployment/dsc/azshcihost/cHyper-V/3.0.0.0/Examples/Resources/cVMNetworkAdapter/CreateVMNetworkAdapterMultipleManagementOS.Example.ps1 @@ -0,0 +1,21 @@ +Configuration HostOSAdapter +{ + Import-DscResource -ModuleName cHyper-V -Name cVMNetworkAdapter + Import-DscResource -ModuleName PSDesiredStateConfiguration + + cVMNetworkAdapter ManagementAdapter { + Id = 'Management-NIC' + Name = 'Management-NIC' + SwitchName = 'SETSwitch' + VMName = 'ManagementOS' + Ensure = 'Present' + } + + cVMNetworkAdapter ClusterAdapter { + Id = 'Cluster-NIC' + Name = 'Cluster-NIC' + SwitchName = 'SETSwitch' + VMName = 'ManagementOS' + Ensure = 'Present' + } +} diff --git a/deployment/dsc/azshcihost/cHyper-V/3.0.0.0/Examples/Resources/cVMNetworkAdapter/CreateVMNetworkAdapterMultipleVM.Example.ps1 b/deployment/dsc/azshcihost/cHyper-V/3.0.0.0/Examples/Resources/cVMNetworkAdapter/CreateVMNetworkAdapterMultipleVM.Example.ps1 new file mode 100644 index 0000000..1720dea --- /dev/null +++ b/deployment/dsc/azshcihost/cHyper-V/3.0.0.0/Examples/Resources/cVMNetworkAdapter/CreateVMNetworkAdapterMultipleVM.Example.ps1 @@ -0,0 +1,29 @@ +Configuration VMAdapter +{ + Import-DscResource -ModuleName cHyper-V -Name cVMNetworkAdapter + Import-DscResource -ModuleName PSDesiredStateConfiguration + + cVMNetworkAdapter MyVM01NIC { + Id = 'MyVM01-NIC' + Name = 'MyVM01-NIC' + SwitchName = 'SETSwitch' + VMName = 'MyVM01' + Ensure = 'Present' + } + + cVMNetworkAdapter MyVM02NIC { + Id = 'MyVM02-NIC' + Name = 'NetAdapter' + SwitchName = 'SETSwitch' + VMName = 'MyVM02' + Ensure = 'Present' + } + + cVMNetworkAdapter MyVM03NIC { + Id = 'MyVM03-NIC' + Name = 'NetAdapter' + SwitchName = 'SETSwitch' + VMName = 'MyVM03' + Ensure = 'Present' + } +} diff --git a/deployment/dsc/azshcihost/cHyper-V/3.0.0.0/Examples/Resources/cVMNetworkAdapter/CreateVMNetworkAdapterMultipleVMMACAddress.Example.ps1 b/deployment/dsc/azshcihost/cHyper-V/3.0.0.0/Examples/Resources/cVMNetworkAdapter/CreateVMNetworkAdapterMultipleVMMACAddress.Example.ps1 new file mode 100644 index 0000000..c23ad31 --- /dev/null +++ b/deployment/dsc/azshcihost/cHyper-V/3.0.0.0/Examples/Resources/cVMNetworkAdapter/CreateVMNetworkAdapterMultipleVMMACAddress.Example.ps1 @@ -0,0 +1,23 @@ +Configuration VMAdapter +{ + Import-DscResource -ModuleName cHyper-V -Name cVMNetworkAdapter + Import-DscResource -ModuleName PSDesiredStateConfiguration + + cVMNetworkAdapter MyVM01NIC { + Id = 'MyVM01-NIC' + Name = 'MyVM01-NIC' + SwitchName = 'SETSwitch' + MacAddress = '001523be0c' + VMName = 'MyVM01' + Ensure = 'Present' + } + + cVMNetworkAdapter MyVM02NIC { + Id = 'MyVM02-NIC' + Name = 'MyVM02-NIC' + SwitchName = 'SETSwitch' + MacAddress = '001523be0d' + VMName = 'MyVM02' + Ensure = 'Present' + } +} diff --git a/deployment/dsc/azshcihost/cHyper-V/3.0.0.0/Examples/Resources/cVMNetworkAdapterSettings/VMNetworkAdapterSettingsManagementOS.Example.ps1 b/deployment/dsc/azshcihost/cHyper-V/3.0.0.0/Examples/Resources/cVMNetworkAdapterSettings/VMNetworkAdapterSettingsManagementOS.Example.ps1 new file mode 100644 index 0000000..4fdb44e --- /dev/null +++ b/deployment/dsc/azshcihost/cHyper-V/3.0.0.0/Examples/Resources/cVMNetworkAdapterSettings/VMNetworkAdapterSettingsManagementOS.Example.ps1 @@ -0,0 +1,13 @@ +Configuration HostOSAdapterSettings +{ + Import-DscResource -ModuleName cHyper-V -Name cVMNetworkAdapterSettings + Import-DscResource -ModuleName PSDesiredStateConfiguration + + cVMNetworkAdapterSettings HostOSAdapterSettings { + Id = 'Management-NIC' + Name = 'Management-NIC' + VMName = 'ManagementOS' + SwitchName = 'SETSwitch' + MinimumBandwidthWeight = 20 + } +} diff --git a/deployment/dsc/azshcihost/cHyper-V/3.0.0.0/Examples/Resources/cVMNetworkAdapterSettings/VMNetworkAdapterSettingsVMMultiple.Example.ps1 b/deployment/dsc/azshcihost/cHyper-V/3.0.0.0/Examples/Resources/cVMNetworkAdapterSettings/VMNetworkAdapterSettingsVMMultiple.Example.ps1 new file mode 100644 index 0000000..accc528 --- /dev/null +++ b/deployment/dsc/azshcihost/cHyper-V/3.0.0.0/Examples/Resources/cVMNetworkAdapterSettings/VMNetworkAdapterSettingsVMMultiple.Example.ps1 @@ -0,0 +1,22 @@ +Configuration VMAdapterSettings +{ + Import-DscResource -ModuleName cHyper-V -Name cVMNetworkAdapterSettings + Import-DscResource -ModuleName PSDesiredStateConfiguration + + cVMNetworkAdapterSettings VMAdapterSettings01 { + Id = 'Management-NIC' + Name = 'Management-NIC' + VMName = 'DHCPVM01' + SwitchName = 'SETSwitch' + DhcpGuard = 'On' + DeviceNaming = 'On' + } + + cVMNetworkAdapterSettings VMAdapterSettings02 { + Id = 'App-NIC' + Name = 'App-NIC' + VMName = 'DHCPVM01' + SwitchName = 'SETSwitch' + DeviceNaming = 'On' + } +} diff --git a/deployment/dsc/azshcihost/cHyper-V/3.0.0.0/Examples/Resources/cVMNetworkAdapterSettings/VMNetworkAdapterSettingsVMSimple.Example.ps1 b/deployment/dsc/azshcihost/cHyper-V/3.0.0.0/Examples/Resources/cVMNetworkAdapterSettings/VMNetworkAdapterSettingsVMSimple.Example.ps1 new file mode 100644 index 0000000..3e50559 --- /dev/null +++ b/deployment/dsc/azshcihost/cHyper-V/3.0.0.0/Examples/Resources/cVMNetworkAdapterSettings/VMNetworkAdapterSettingsVMSimple.Example.ps1 @@ -0,0 +1,13 @@ +Configuration VMAdapterSettings +{ + Import-DscResource -ModuleName cHyper-V -Name cVMNetworkAdapterSettings + Import-DscResource -ModuleName PSDesiredStateConfiguration + + cVMNetworkAdapterSettings VMAdapterSettings { + Id = 'Management-NIC' + Name = 'Management-NIC' + VMName = 'DHCPVM01' + SwitchName = 'SETSwitch' + DhcpGuard = 'On' + } +} diff --git a/deployment/dsc/azshcihost/cHyper-V/3.0.0.0/Examples/Resources/cVMNetworkAdapterVlan/VMNetworkAdapterVlanManagementOS.Example.ps1 b/deployment/dsc/azshcihost/cHyper-V/3.0.0.0/Examples/Resources/cVMNetworkAdapterVlan/VMNetworkAdapterVlanManagementOS.Example.ps1 new file mode 100644 index 0000000..a1cf270 --- /dev/null +++ b/deployment/dsc/azshcihost/cHyper-V/3.0.0.0/Examples/Resources/cVMNetworkAdapterVlan/VMNetworkAdapterVlanManagementOS.Example.ps1 @@ -0,0 +1,13 @@ +Configuration HostOSAdapterVlan +{ + Import-DscResource -ModuleName cHyper-V -Name cVMNetworkAdapterVlan + Import-DscResource -ModuleName PSDesiredStateConfiguration + + cVMNetworkAdapterVlan HostOSAdapterVlan { + Id = 'Management-NIC' + Name = 'Management-NIC' + VMName = 'ManagementOS' + AdapterMode = 'Access' + VlanId = 10 + } +} diff --git a/deployment/dsc/azshcihost/cHyper-V/3.0.0.0/Examples/Resources/cVMNetworkAdapterVlan/VMNetworkAdapterVlanMultipleManagementOS.Example.ps1 b/deployment/dsc/azshcihost/cHyper-V/3.0.0.0/Examples/Resources/cVMNetworkAdapterVlan/VMNetworkAdapterVlanMultipleManagementOS.Example.ps1 new file mode 100644 index 0000000..62312b7 --- /dev/null +++ b/deployment/dsc/azshcihost/cHyper-V/3.0.0.0/Examples/Resources/cVMNetworkAdapterVlan/VMNetworkAdapterVlanMultipleManagementOS.Example.ps1 @@ -0,0 +1,29 @@ +Configuration HostOSAdapterVlan +{ + Import-DscResource -ModuleName cHyper-V -Name cVMNetworkAdapterVlan + Import-DscResource -ModuleName PSDesiredStateConfiguration + + cVMNetworkAdapterVlan HostOSAdapterVlan { + Id = 'Management-NIC' + Name = 'Management-NIC' + VMName = 'ManagementOS' + AdapterMode = 'Access' + VlanId = 10 + } + + cVMNetworkAdapterVlan ClusterAdapterVlan { + Id = 'Cluster-NIC' + Name = 'Cluster-NIC' + VMName = 'ManagementOS' + AdapterMode = 'Access' + VlanId = 20 + } + + #The following configuration removes any VLAN setting, if present. + cVMNetworkAdapterVlan JustAnotherAdapterVlan { + Id = 'JustAnother-NIC' + Name = 'JustAnother-NIC' + VMName = 'ManagementOS' + AdapterMode = 'Untagged' + } +} diff --git a/deployment/dsc/azshcihost/cHyper-V/3.0.0.0/Examples/Resources/cVMNetworkAdapterVlan/VMNetworkAdapterVlanMultipleVM.Example.ps1 b/deployment/dsc/azshcihost/cHyper-V/3.0.0.0/Examples/Resources/cVMNetworkAdapterVlan/VMNetworkAdapterVlanMultipleVM.Example.ps1 new file mode 100644 index 0000000..0715fd4 --- /dev/null +++ b/deployment/dsc/azshcihost/cHyper-V/3.0.0.0/Examples/Resources/cVMNetworkAdapterVlan/VMNetworkAdapterVlanMultipleVM.Example.ps1 @@ -0,0 +1,20 @@ +Configuration HostOSAdapterVlan +{ + Import-DscResource -ModuleName cHyper-V -Name cVMNetworkAdapterVlan + Import-DscResource -ModuleName PSDesiredStateConfiguration + + cVMNetworkAdapterVlan VMMgmtAdapterVlan { + Id = 'VMManagement-NIC' + Name = 'VMManagement-NIC' + VMName = 'SQLVM01' + AdapterMode = 'Access' + VlanId = 10 + } + + cVMNetworkAdapterVlan VMiSCSIAdapterVlan { + Id = 'VMiSCSI-NIC' + Name = 'VMiSCSI-NIC' + VMName = 'SQLVM01' + AdapterMode = 'Untagged' + } +} diff --git a/deployment/dsc/azshcihost/cHyper-V/3.0.0.0/Examples/Resources/cVMSwitch/HostTeamSwitch.Example.ps1 b/deployment/dsc/azshcihost/cHyper-V/3.0.0.0/Examples/Resources/cVMSwitch/HostTeamSwitch.Example.ps1 new file mode 100644 index 0000000..deff9fe --- /dev/null +++ b/deployment/dsc/azshcihost/cHyper-V/3.0.0.0/Examples/Resources/cVMSwitch/HostTeamSwitch.Example.ps1 @@ -0,0 +1,14 @@ +Configuration SimpleHostTeamvSwitch +{ + Import-DscResource -ModuleName cHyper-V -Name cVMSwitch + Import-DscResource -ModuleName PSDesiredStateConfiguration + + cVMSwitch HostSwitch { + Name = 'HostSwitch' + Type = 'External' + AllowManagementOS = $true + MinimumBandwidthMode = 'Weight' + NetAdapterName = 'HostTeam' + Ensure = 'Present' + } +} diff --git a/deployment/dsc/azshcihost/cHyper-V/3.0.0.0/Examples/Resources/cVMSwitch/InternalSwitch.Example.ps1 b/deployment/dsc/azshcihost/cHyper-V/3.0.0.0/Examples/Resources/cVMSwitch/InternalSwitch.Example.ps1 new file mode 100644 index 0000000..869774d --- /dev/null +++ b/deployment/dsc/azshcihost/cHyper-V/3.0.0.0/Examples/Resources/cVMSwitch/InternalSwitch.Example.ps1 @@ -0,0 +1,11 @@ +Configuration InternalSwitch +{ + Import-DscResource -ModuleName cHyper-V -Name cVMSwitch + Import-DscResource -ModuleName PSDesiredStateConfiguration + + cVMSwitch HostSwitch { + Name = 'HostSwitch' + Type = 'Internal' + Ensure = 'Present' + } +} diff --git a/deployment/dsc/azshcihost/cHyper-V/3.0.0.0/Examples/Resources/cVMSwitch/NetAdapterSwitch.Example.ps1 b/deployment/dsc/azshcihost/cHyper-V/3.0.0.0/Examples/Resources/cVMSwitch/NetAdapterSwitch.Example.ps1 new file mode 100644 index 0000000..a2c3de9 --- /dev/null +++ b/deployment/dsc/azshcihost/cHyper-V/3.0.0.0/Examples/Resources/cVMSwitch/NetAdapterSwitch.Example.ps1 @@ -0,0 +1,13 @@ +Configuration SimpleNetAdaptervSwitch +{ + Import-DscResource -ModuleName cHyper-V -Name cVMSwitch + Import-DscResource -ModuleName PSDesiredStateConfiguration + + cVMSwitch HostSwitch { + Name = 'HostSwitch' + Type = 'External' + AllowManagementOS = $true + NetAdapterName = 'NIC1' + Ensure = 'Present' + } +} diff --git a/deployment/dsc/azshcihost/cHyper-V/3.0.0.0/Examples/Resources/cVMSwitch/PrivateSwitch.Example.ps1 b/deployment/dsc/azshcihost/cHyper-V/3.0.0.0/Examples/Resources/cVMSwitch/PrivateSwitch.Example.ps1 new file mode 100644 index 0000000..ca415f2 --- /dev/null +++ b/deployment/dsc/azshcihost/cHyper-V/3.0.0.0/Examples/Resources/cVMSwitch/PrivateSwitch.Example.ps1 @@ -0,0 +1,11 @@ +Configuration PrivateSwitch +{ + Import-DscResource -ModuleName cHyper-V -Name cVMSwitch + Import-DscResource -ModuleName PSDesiredStateConfiguration + + cVMSwitch HostSwitch { + Name = 'HostSwitch' + Type = 'Private' + Ensure = 'Present' + } +} diff --git a/deployment/dsc/azshcihost/cHyper-V/3.0.0.0/Examples/Resources/cVMSwitch/SETTeamSwitch.Example.ps1 b/deployment/dsc/azshcihost/cHyper-V/3.0.0.0/Examples/Resources/cVMSwitch/SETTeamSwitch.Example.ps1 new file mode 100644 index 0000000..aa8d382 --- /dev/null +++ b/deployment/dsc/azshcihost/cHyper-V/3.0.0.0/Examples/Resources/cVMSwitch/SETTeamSwitch.Example.ps1 @@ -0,0 +1,16 @@ +Configuration SETTeamSwitch +{ + Import-DscResource -ModuleName cHyper-V -Name cVMSwitch + Import-DscResource -ModuleName PSDesiredStateConfiguration + + cVMSwitch HostSwitch { + Name = 'HostSwitch' + Type = 'External' + AllowManagementOS = $true + MinimumBandwidthMode = 'Weight' + TeamingMode = 'SwitchIndependent' + LoadBalancingAlgorithm = 'HyperVPort' + NetAdapterName = 'NIC1','NIC2','NIC3','NIC4' + Ensure = 'Present' + } +} diff --git a/deployment/dsc/azshcihost/cHyper-V/3.0.0.0/Examples/Resources/cWaitForVMIntegrationComponent/WaitForIntegrationComponentCustom.Example.ps1 b/deployment/dsc/azshcihost/cHyper-V/3.0.0.0/Examples/Resources/cWaitForVMIntegrationComponent/WaitForIntegrationComponentCustom.Example.ps1 new file mode 100644 index 0000000..fb99fca --- /dev/null +++ b/deployment/dsc/azshcihost/cHyper-V/3.0.0.0/Examples/Resources/cWaitForVMIntegrationComponent/WaitForIntegrationComponentCustom.Example.ps1 @@ -0,0 +1,12 @@ +Configuration WaitForIC +{ + Import-DscResource -Name cWaitForVMGuestIntegration -ModuleName cHyper-V + + cWaitForVMGuestIntegration VM01 + { + Id = 'VM01-IC01' + VMName = 'VM01' + RetryIntervalSec = 20 + RetryCount = 10 + } +} diff --git a/deployment/dsc/azshcihost/cHyper-V/3.0.0.0/Examples/Resources/cWaitForVMIntegrationComponent/WaitForIntegrationComponentDefault.Example.ps1 b/deployment/dsc/azshcihost/cHyper-V/3.0.0.0/Examples/Resources/cWaitForVMIntegrationComponent/WaitForIntegrationComponentDefault.Example.ps1 new file mode 100644 index 0000000..1cf8f6c --- /dev/null +++ b/deployment/dsc/azshcihost/cHyper-V/3.0.0.0/Examples/Resources/cWaitForVMIntegrationComponent/WaitForIntegrationComponentDefault.Example.ps1 @@ -0,0 +1,10 @@ +Configuration WaitForIC +{ + Import-DscResource -Name cWaitForVMGuestIntegration -ModuleName cHyper-V + + cWaitForVMGuestIntegration VM01 + { + Id = 'VM01-IC01' + VMName = 'VM01' + } +} diff --git a/deployment/dsc/azshcihost/cHyper-V/3.0.0.0/Examples/SwitchEmbeddedTeamingWithHostNetworkAdapter/SETTeamWithHostNetworkAdapters.Example.ps1 b/deployment/dsc/azshcihost/cHyper-V/3.0.0.0/Examples/SwitchEmbeddedTeamingWithHostNetworkAdapter/SETTeamWithHostNetworkAdapters.Example.ps1 new file mode 100644 index 0000000..927be9c --- /dev/null +++ b/deployment/dsc/azshcihost/cHyper-V/3.0.0.0/Examples/SwitchEmbeddedTeamingWithHostNetworkAdapter/SETTeamWithHostNetworkAdapters.Example.ps1 @@ -0,0 +1,105 @@ +Configuration SETTeam +{ + param ( + [String] $SwitchName, + [String[]] $NetAdapterName, + [Int] $ManagementVlan, + [Int] $ManagementBandwidthWeight, + [String] $ClusterAdapterName, + [Int] $ClusterVlan, + [Int] $ClusterBandwidthWeight, + [String] $LiveMigrationAdapterName, + [Int] $LiveMigrationVlan, + [Int] $LiveMigrationBandwidthWeight + ) + Import-DscResource -ModuleName cHyper-V + + cVMSwitch $SwitchName + { + Name = $SwitchName + NetAdapterName = $NetAdapterName + Type = 'External' + MinimumBandwidthMode = 'Weight' + TeamingMode = 'SwitchIndependent' + LoadBalancingAlgorithm = 'Dynamic' + Ensure = 'Present' + } + + cVMNetworkAdapterVlan $SwitchName + { + Id = 'Mgmt-NIC' + Name = $SwitchName + AdapterMode = 'Access' + VlanId = $ManagementVlan + VMName = 'ManagementOS' + DependsOn = "[cVMSwitch]$SwitchName" + } + + cVMNetworkAdapterSettings $SwitchName + { + Id = 'Mgmt-NIC' + Name = $SwitchName + VMName = 'ManagementOS' + SwitchName = $SwitchName + MinimumBandwidthWeight = $ManagementBandwidthWeight + DependsOn = "[cVMSwitch]$SwitchName" + } + + cVMNetworkAdapter $ClusterAdapterName + { + Id = 'Cluster-NIC' + Name = $ClusterAdapterName + VMName = 'ManagementOS' + SwitchName = $SwitchName + DependsOn = "[cVMSwitch]$SwitchName" + } + + cVMNetworkAdapterVlan $ClusterAdapterName + { + Id = 'Cluster-NIC' + Name = $ClusterAdapterName + AdapterMode = 'Access' + VlanId = $ClusterVlan + VMName = 'ManagementOS' + DependsOn = "[cVMNetworkAdapter]$ClusterAdapterName" + } + + cVMNetworkAdapterSettings $ClusterAdapterName + { + Id = 'Cluster-NIC' + Name = $ClusterAdapterName + VMName = 'ManagementOS' + SwitchName = $SwitchName + MinimumBandwidthWeight = $ClusterBandwidthWeight + DependsOn = "[cVMNetworkAdapter]$ClusterAdapterName" + } + + cVMNetworkAdapter $LiveMigrationAdapterName + { + Id = 'LM-NIC' + Name = $LiveMigrationAdapterName + VMName = 'ManagementOS' + SwitchName = $SwitchName + DependsOn = "[cVMSwitch]$SwitchName" + } + + cVMNetworkAdapterVlan $LiveMigrationAdapterName + { + Id = 'LM-NIC' + Name = $LiveMigrationAdapterName + AdapterMode = 'Access' + VlanId = $LiveMigrationVlan + VMName = 'ManagementOS' + DependsOn = "[cVMNetworkAdapter]$LiveMigrationAdapterName" + } + + cVMNetworkAdapterSettings $LiveMigrationAdapterName + { + Id = 'LM-NIC' + Name = $LiveMigrationAdapterName + VMName = 'ManagementOS' + SwitchName = $SwitchName + MinimumBandwidthWeight = $LiveMigrationBandwidthWeight + DependsOn = "[cVMNetworkAdapter]$LiveMigrationAdapterName" + } +} diff --git a/deployment/dsc/azshcihost/cHyper-V/3.0.0.0/README.md b/deployment/dsc/azshcihost/cHyper-V/3.0.0.0/README.md new file mode 100644 index 0000000..7c21445 --- /dev/null +++ b/deployment/dsc/azshcihost/cHyper-V/3.0.0.0/README.md @@ -0,0 +1,18 @@ +# DSCResources # +### Custom DSC resource module for Microsoft Hyper-V Networking by [PowerShell Magazine](http://www.powershellmagazine.com "PowerShell Magazine"). ### +---------- + +### This release (3.0.0.0) removed the cSwitchEmbededTeaming and cNATSwitch from this resource module. The functionality for creating SET team is now a part of cVMSwitch and cNatSwitch will go to xNetworking soon! ### + +Microsoft Hyper-V DSC resource module contains a set of resources for managing Hyper-V management OS and guest networking. + +- [cVMSwitch](https://github.com/rchaganti/DSCResources/tree/master/cHyper-V/DSCResources/cVMSwitch) is used to create virtual machine switches. +- [cVMNetworkAdapter](https://github.com/rchaganti/DSCResources/tree/master/cHyper-V/DSCResources/cVMNetworkAdapter) is used to create VM network adapters to attach to either management OS or the virtual machines. +- [cVMNetworkAdapterSettings](https://github.com/rchaganti/DSCResources/tree/master/cHyper-V/DSCResources/cVMNetworkAdapterSettings) is used to configure VM network adapter settings such as bandwidth weights, port mirroring, DHCP guard, MAC address spoofing, etc. +- [cVMNetworkAdapterVlan](https://github.com/rchaganti/DSCResources/tree/master/cHyper-V/DSCResources/cVMNetworkAdapterVlan) is used to configure VLANs on virtual network adapters either in the management OS or virtual machines. +- [cVMIPAddress](https://github.com/rchaganti/DSCResources/tree/master/cHyper-V/DSCResources/cVMIPAddress) is used to inject IP Address into a virtual machine running on Hyper-V host. +- [cWaitForVMGuestIntegration](https://github.com/rchaganti/DSCResources/tree/master/cHyper-V/DSCResources/cWaitForVMGuestIntegration) is used to ensure that the VM integration components are running. This will be useful when you want to wait until a VM completes reboot and then perform an action. + +Note that before using any of the custom resources, you must either import the individual resources or the entire module containing these resources. You can do this using the Import-DscResource cmdlet. + +Note: For documentation of each of these resources, visit the resource page. \ No newline at end of file diff --git a/deployment/dsc/azshcihost/cHyper-V/3.0.0.0/cHyper-V.psd1 b/deployment/dsc/azshcihost/cHyper-V/3.0.0.0/cHyper-V.psd1 new file mode 100644 index 0000000..ce2b0e6 Binary files /dev/null and b/deployment/dsc/azshcihost/cHyper-V/3.0.0.0/cHyper-V.psd1 differ diff --git a/deployment/dsc/azshcihost/dscmetadata.json b/deployment/dsc/azshcihost/dscmetadata.json new file mode 100644 index 0000000..214fe5b --- /dev/null +++ b/deployment/dsc/azshcihost/dscmetadata.json @@ -0,0 +1,20 @@ +{ + "Modules": [ + "StorageDSC", + "NetworkingDSC", + "ComputerManagementDsc", + "xPSDesiredStateConfiguration", + "xHyper-v", + "cHyper-v", + "xDhcpServer", + "DnsServerDsc", + "cChoco", + "DSCR_Shortcut", + "xCredSSP", + "ActiveDirectoryDsc", + "WindowsDeploymentHelper", + "WindowsImageTools", + "MSCatalog", + "Hyper-ConvertImage" + ] +} \ No newline at end of file diff --git a/deployment/dsc/azshcihost/xCredSSP/1.3.0.0/DSCResources/MSFT_xCredSSP/MSFT_xCredSSP.psm1 b/deployment/dsc/azshcihost/xCredSSP/1.3.0.0/DSCResources/MSFT_xCredSSP/MSFT_xCredSSP.psm1 new file mode 100644 index 0000000..11a9c7a --- /dev/null +++ b/deployment/dsc/azshcihost/xCredSSP/1.3.0.0/DSCResources/MSFT_xCredSSP/MSFT_xCredSSP.psm1 @@ -0,0 +1,324 @@ +function Get-TargetResource +{ + [CmdletBinding()] + [OutputType([System.Collections.Hashtable])] + param + ( + [parameter(Mandatory = $true)] + [ValidateSet("Server","Client")] + [System.String] + $Role + ) + + #Check if GPO policy has been set + switch($Role) + { + "Server" + { + $RegKey = "HKLM:\SOFTWARE\Policies\Microsoft\Windows\WinRM\Service" + } + "Client" + { + $RegKey = "HKLM:\SOFTWARE\Policies\Microsoft\Windows\WinRM\Client" + } + } + $RegValueName = "AllowCredSSP" + + if (Test-RegistryValue -Path $RegKey -Name $RegValueName) + { + Write-Verbose -Message "CredSSP is configured via Group Policies" + } + else + { + # Check regular values + switch($Role) + { + "Server" + { + $RegKey = "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\WSMAN\Service" + } + "Client" + { + $RegKey = "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\WSMAN\Client" + } + } + $RegValueName = "auth_credssp" + } + + if(Test-RegistryValue -Path $RegKey -Name $RegValueName) + { + $Setting = (Get-ItemProperty -Path $RegKey -Name $RegValueName).$RegValueName + } + else + { + $Setting = 0 + } + + switch($Role) + { + "Server" + { + switch($Setting) + { + 1 + { + $returnValue = @{ + Ensure = "Present"; + Role = "Server" + } + } + 0 + { + $returnValue = @{ + Ensure = "Absent"; + Role = "Server" + } + } + } + } + "Client" + { + switch($Setting) + { + 1 + { + $key = "HKLM:\SOFTWARE\Policies\Microsoft\Windows\CredentialsDelegation\AllowFreshCredentials" + + $DelegateComputers = @() + + + Get-Item -Path $key -ErrorAction SilentlyContinue | + Select-Object -ExpandProperty Property | + ForEach-Object { + $DelegateComputer = ((Get-ItemProperty -Path $key -Name $_).$_).Split("/")[1] + $DelegateComputers += $DelegateComputer + } + $DelegateComputers = $DelegateComputers | Sort-Object -Unique + + $returnValue = @{ + Ensure = "Present"; + Role = "Client"; + DelegateComputers = @($DelegateComputers) + } + } + 0 + { + $returnValue = @{ + Ensure = "Absent"; + Role = "Client" + } + } + } + } + } + + return $returnValue +} + + +function Set-TargetResource +{ + [CmdletBinding()] + param + ( + [ValidateSet("Present","Absent")] + [System.String] + $Ensure = "Present", + + [parameter(Mandatory = $true)] + [ValidateSet("Server","Client")] + [System.String] + $Role, + + [System.String[]] + $DelegateComputers, + + [System.Boolean] + $SuppressReboot = $false + ) + + if ($Role -eq "Server" -and ($DelegateComputers)) + { + throw ("Cannot use the Role=Server parameter together with " + ` + "the DelegateComputers parameter") + } + + #Check if policy has been set + switch($Role) + { + "Server" + { + $RegKey = "HKLM:\SOFTWARE\Policies\Microsoft\Windows\WinRM\Service" + } + "Client" + { + $RegKey = "HKLM:\SOFTWARE\Policies\Microsoft\Windows\WinRM\Client" + } + } + $RegValueName = "AllowCredSSP" + + if (Test-RegistryValue -Path $RegKey -Name $RegValueName) + { + Throw "Cannot configure CredSSP. CredSSP is configured via Group Policies" + } + + switch($Role) + { + "Server" + { + switch($Ensure) + { + "Present" + { + Enable-WSManCredSSP -Role Server -Force | Out-Null + if ($SuppressReboot -eq $false) + { + $global:DSCMachineStatus = 1 + } + } + "Absent" + { + Disable-WSManCredSSP -Role Server | Out-Null + } + } + } + "Client" + { + switch($Ensure) + { + "Present" + { + if($DelegateComputers) + { + $key = "HKLM:\SOFTWARE\Policies\Microsoft\Windows\CredentialsDelegation\AllowFreshCredentials" + + if (!(test-path $key)) + { + New-Item $key -Force | out-null + } + + $CurrentDelegateComputers = @() + + Get-Item -Path $key | + Select-Object -ExpandProperty Property | + ForEach-Object { + $CurrentDelegateComputer = ((Get-ItemProperty -Path $key -Name $_).$_).Split("/")[1] + $CurrentDelegateComputers += $CurrentDelegateComputer + } + $CurrentDelegateComputers = $CurrentDelegateComputers | Sort-Object -Unique + + foreach($DelegateComputer in $DelegateComputers) + { + if(($CurrentDelegateComputers -eq $NULL) -or (!$CurrentDelegateComputers.Contains($DelegateComputer))) + { + Enable-WSManCredSSP -Role Client -DelegateComputer $DelegateComputer -Force | Out-Null + if ($SuppressReboot -eq $false) + { + $global:DSCMachineStatus = 1 + } + } + } + } + else + { + Throw "DelegateComputers is required!" + } + } + "Absent" + { + Disable-WSManCredSSP -Role Client | Out-Null + } + } + } + } +} + + +function Test-TargetResource +{ + [CmdletBinding()] + [OutputType([System.Boolean])] + param + ( + [ValidateSet("Present","Absent")] + [System.String] + $Ensure = "Present", + + [parameter(Mandatory = $true)] + [ValidateSet("Server","Client")] + [System.String] + $Role, + + [System.String[]] + $DelegateComputers, + + [System.Boolean] + $SuppressReboot = $false + ) + + if ($Role -eq "Server" -and $PSBoundParameters.ContainsKey("DelegateComputers")) + { + Write-Verbose -Message ("Cannot use the Role=Server parameter together with " + ` + "the DelegateComputers parameter") + } + + $CredSSP = Get-TargetResource -Role $Role + + switch($Role) + { + "Server" + { + return ($CredSSP.Ensure -eq $Ensure) + } + "Client" + { + switch($Ensure) + { + "Present" + { + $CorrectDelegateComputers = $true + if($DelegateComputers) + { + foreach($DelegateComputer in $DelegateComputers) + { + if(!($CredSSP.DelegateComputers | Where-Object {$_ -eq $DelegateComputer})) + { + $CorrectDelegateComputers = $false + } + } + } + $result = (($CredSSP.Ensure -eq $Ensure) -and $CorrectDelegateComputers) + } + "Absent" + { + $result = ($CredSSP.Ensure -eq $Ensure) + } + } + } + } + + return $result +} + + +Export-ModuleMember -Function *-TargetResource + + +function Test-RegistryValue +{ + param ( + [Parameter(Mandatory = $true)] + [String]$Path + , + [Parameter(Mandatory = $true)] + [String]$Name + ) + + if ($null -eq $Path) + { + return $false + } + + $itemProperties = Get-ItemProperty -Path $Path -ErrorAction SilentlyContinue + return ($null -ne $itemProperties -and $null -ne $itemProperties.$Name) +} diff --git a/deployment/dsc/azshcihost/xCredSSP/1.3.0.0/DSCResources/MSFT_xCredSSP/MSFT_xCredSSP.schema.mof b/deployment/dsc/azshcihost/xCredSSP/1.3.0.0/DSCResources/MSFT_xCredSSP/MSFT_xCredSSP.schema.mof new file mode 100644 index 0000000..80b7354 --- /dev/null +++ b/deployment/dsc/azshcihost/xCredSSP/1.3.0.0/DSCResources/MSFT_xCredSSP/MSFT_xCredSSP.schema.mof @@ -0,0 +1,9 @@ +[ClassVersion("1.0.0.0"), FriendlyName("xCredSSP")] +class MSFT_xCredSSP : OMI_BaseResource +{ + [Key, Description("Specifies the CredSSP role.\nServer \nClient \n"), ValueMap{"Server","Client"}, Values{"Server","Client"}] String Role; + [Write, Description("An enumerated value that describes if the role is expected to be enabled on the machine.\nPresent {default} \nAbsent \n"), ValueMap{"Present","Absent"}, Values{"Present","Absent"}] String Ensure; + [Write, Description("Specifies the array of computers that CredSSP client can delegate to.")] String DelegateComputers[]; + [Write, Description("Specifies if a reboot should be supressed. Default is False")] Boolean SuppressReboot; +}; + diff --git a/deployment/dsc/azshcihost/xCredSSP/1.3.0.0/LICENSE b/deployment/dsc/azshcihost/xCredSSP/1.3.0.0/LICENSE new file mode 100644 index 0000000..567fd6a --- /dev/null +++ b/deployment/dsc/azshcihost/xCredSSP/1.3.0.0/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2015 Microsoft Corporation. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. \ No newline at end of file diff --git a/deployment/dsc/azshcihost/xCredSSP/1.3.0.0/PSGetModuleInfo.xml b/deployment/dsc/azshcihost/xCredSSP/1.3.0.0/PSGetModuleInfo.xml new file mode 100644 index 0000000..7061cea Binary files /dev/null and b/deployment/dsc/azshcihost/xCredSSP/1.3.0.0/PSGetModuleInfo.xml differ diff --git a/deployment/dsc/azshcihost/xCredSSP/1.3.0.0/README.md b/deployment/dsc/azshcihost/xCredSSP/1.3.0.0/README.md new file mode 100644 index 0000000..04de267 --- /dev/null +++ b/deployment/dsc/azshcihost/xCredSSP/1.3.0.0/README.md @@ -0,0 +1,102 @@ +[![Build status](https://ci.appveyor.com/api/projects/status/29y5yx2vxwjq60ic/branch/master?svg=true)](https://ci.appveyor.com/project/PowerShell/xcredssp/branch/master) + +# xCredSSP + + +The **xCredSSP** module is a part of the Windows PowerShell Desired State Configuration (DSC) Resource Kit, which is a collection of DSC Resources produced by the PowerShell Team. +This module contains the **xCredSSP** resource, which enables or disables Credential Security Support Provider (CredSSP) authentication on a client or on a server computer, and which server or servers the client credentials can be delegated to. + + +**All of the resources in the DSC Resource Kit are provided AS IS, and are not supported through any Microsoft standard support program or service. +The "x" in xCredSSP stands for experimental**, which means that these resources will be **fix forward** and monitored by the module owner(s). + +This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). +For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments. + +## Installation + +To install **xCredSSP** module + +* Unzip the content under $env:ProgramFiles\WindowsPowerShell\Modules folder + +To confirm installation: + +* Run **Get-DSCResource** to see that **xCredSSP** is among the DSC Resources listed. + + +## Requirements + +This module requires the latest version of PowerShell (v4.0, which ships in Windows 8.1 or Windows Server 2012R2). +To easily use PowerShell 4.0 on older operating systems, [<span style="color:#0000ff">install WMF 4.0</span>](http://www.microsoft.com/en-us/download/details.aspx?id=40855). +Please read the installation instructions that are present on both the download page and the release notes for WMF 4.0. + + +## Description + +The **xCredSSP** module contains the **xCredSSP** resource, which enables or disables Credential Security Support Provider (CredSSP) authentication on a client or on a server computer, and which server or servers the client credentials can be delegated to. + + +## Details + +**xCredSSP** resource has following properties: + +* **Ensure:** Specifies whether the domain trust is present or absent +* **Role**: REQUIRED parameter representing the CredSSP role, and is either "Server" or "Client" +* **DelegateComputers**: Array of servers to be delegated to, REQUIRED when Role is set to "Client". +* **SuppressReboot**: Specifies whether a necessary reboot has to be supressed or not. + +## Versions + +### Unreleased + +### 1.3.0.0 +* Added a fix to enable credSSP with a fresh server installation + +### 1.2.0.0 +* Converted appveyor.yml to install Pester from PSGallery instead of from Chocolatey. +* Implemented a GPO check to prevent an endless reboot loop when CredSSP is configured via a GPO +* Fixed issue with Test always returning false with other regional settings then english +* Added check to test if Role=Server and DelegateComputers parameter is specified +* Added parameter to supress a reboot, default value is false (reboot server when required) + +### 1.1.0.0 + +* Made sure DSC reboots if credSS is enabled + +### 1.0.1.0 + +* Updated with minor bug fixes. + + +### 1.0.0.0 + +* Initial release with the following resources + * <span style="font-family:Calibri; font-size:medium">xADDomain</span> + +## Examples + +Enable CredSSP for both server and client roles, and delegate to Server1 and Server2. + +```powershell +Configuration EnableCredSSP +{ + Import-DscResource -Module xCredSSP + Node localhost + { + xCredSSP Server + { + Ensure = "Present" + Role = "Server" + } + xCredSSP Client + { + Ensure = "Present" + Role = "Client" + DelegateComputers = "Server1","Server2" + } + } +} +``` + +## Contributing +Please check out common DSC Resources [contributing guidelines](https://github.com/PowerShell/DscResource.Kit/blob/master/CONTRIBUTING.md). diff --git a/deployment/dsc/azshcihost/xCredSSP/1.3.0.0/Tests/Unit/MSFT_xCredSSP.tests.ps1 b/deployment/dsc/azshcihost/xCredSSP/1.3.0.0/Tests/Unit/MSFT_xCredSSP.tests.ps1 new file mode 100644 index 0000000..6e8bddf --- /dev/null +++ b/deployment/dsc/azshcihost/xCredSSP/1.3.0.0/Tests/Unit/MSFT_xCredSSP.tests.ps1 @@ -0,0 +1,275 @@ +<# +.Synopsis + Unit tests for MSFT_xCredSSP +.DESCRIPTION + Unit tests for MSFT_xCredSSP + +.NOTES + Code in HEADER and FOOTER regions are standard and may be moved into DSCResource.Tools in + Future and therefore should not be altered if possible. +#> + + +$Global:DSCModuleName = 'xCredSSP' # Example xNetworking +$Global:DSCResourceName = 'MSFT_xCredSSP' # Example MSFT_xFirewall + +#region HEADER +[String] $moduleRoot = Split-Path -Parent (Split-Path -Parent (Split-Path -Parent $Script:MyInvocation.MyCommand.Path)) +if ( (-not (Test-Path -Path (Join-Path -Path $moduleRoot -ChildPath 'DSCResource.Tests'))) -or ` + (-not (Test-Path -Path (Join-Path -Path $moduleRoot -ChildPath 'DSCResource.Tests\TestHelper.psm1'))) ) +{ + & git @('clone','https://github.com/PowerShell/DscResource.Tests.git',(Join-Path -Path $moduleRoot -ChildPath '\DSCResource.Tests\')) +} +else +{ + & git @('-C',(Join-Path -Path $moduleRoot -ChildPath '\DSCResource.Tests\'),'pull') +} +Import-Module (Join-Path -Path $moduleRoot -ChildPath 'DSCResource.Tests\TestHelper.psm1') -Force +$TestEnvironment = Initialize-TestEnvironment ` + -DSCModuleName $Global:DSCModuleName ` + -DSCResourceName $Global:DSCResourceName ` + -TestType Unit +#endregion + +# TODO: Other Optional Init Code Goes Here... + +# Begin Testing +try +{ + #region Pester Tests + + # The InModuleScope command allows you to perform white-box unit testing on the internal + # (non-exported) code of a Script Module. + InModuleScope $Global:DSCResourceName { + + #region Pester Test Initialization + # TODO: Optopnal Load Mock for use in Pester tests here... + #endregion + + + #region Function Get-TargetResource + Describe "$($Global:DSCResourceName)\Get-TargetResource" { + # TODO: Complete Tests... + } + #endregion + + + #region Function Test-TargetResource + Describe "$($Global:DSCResourceName)\Test-TargetResource" { + # TODO: Complete Tests... + } + #endregion + + + #region Function Set-TargetResource + Describe "$($Global:DSCResourceName)\Set-TargetResource" { + + Context "Enable Server Role with invalid delegate Computer parameter" { + BeforeAll { + $global:DSCMachineStatus = $null + } + AfterAll { + $global:DSCMachineStatus = $null + } + + mock Enable-WSManCredSSP -MockWith {} -Verifiable + mock Disable-WSManCredSSP -MockWith {} + + it 'should throw' { + { Set-TargetResource -Ensure 'Present' -Role Server -DelegateComputer 'foo' } | should throw + } + it 'should have not called enable' { + Assert-MockCalled -CommandName Enable-WSManCredSSP -Times 0 -Scope 'Context' + } + it 'should have not called disable' { + Assert-MockCalled -CommandName Disable-WSManCredSSP -Times 0 -Scope 'Context' + } + it 'Should not have triggered a reboot' { + $global:DSCMachineStatus | should be $null + } + } + + Context "Enable Server Role when it has been configured using GPO" { + BeforeAll { + $global:DSCMachineStatus = $null + } + AfterAll { + $global:DSCMachineStatus = $null + } + + mock Enable-WSManCredSSP -MockWith {} -Verifiable + mock Disable-WSManCredSSP -MockWith {} + mock Get-ItemProperty -MockWith { + return @{ AllowCredSSP = 1 } + } -ParameterFilter { $Path -eq "HKLM:\SOFTWARE\Policies\Microsoft\Windows\WinRM\Service" } + + it 'should throw' { + { Set-TargetResource -Ensure 'Present' -Role Server }| should throw + } + it 'should have not called enable' { + Assert-MockCalled -CommandName Enable-WSManCredSSP -Times 0 -Scope 'Context' + } + it 'should have not called disable' { + Assert-MockCalled -CommandName Disable-WSManCredSSP -Times 0 -Scope 'Context' + } + it 'Should not have triggered a reboot' { + $global:DSCMachineStatus | should be $null + } + } + + Context "Enable Client Role when it has been configured using GPO" { + BeforeAll { + $global:DSCMachineStatus = $null + } + AfterAll { + $global:DSCMachineStatus = $null + } + + mock Enable-WSManCredSSP -MockWith {} -Verifiable + mock Disable-WSManCredSSP -MockWith {} + mock Get-ItemProperty -MockWith { + return @{ AllowCredSSP = 1 } + } -ParameterFilter { $Path -eq "HKLM:\SOFTWARE\Policies\Microsoft\Windows\WinRM\Client" } + + it 'should throw' { + {Set-TargetResource -Ensure 'Present' -Role Client -DelegateComputers 'foo'}| should throw + } + it 'should have not called enable' { + Assert-MockCalled -CommandName Enable-WSManCredSSP -Times 0 -Scope 'Context' + } + it 'should have not called disable' { + Assert-MockCalled -CommandName Disable-WSManCredSSP -Times 0 -Scope 'Context' + } + it 'Should not have triggered a reboot' { + $global:DSCMachineStatus | should be $null + } + } + + Context "Enable Server Role" { + BeforeAll { + $global:DSCMachineStatus = $null + } + AfterAll { + $global:DSCMachineStatus = $null + } + + mock Enable-WSManCredSSP -MockWith {} -Verifiable + mock Disable-WSManCredSSP -MockWith {} + mock Get-ItemProperty -MockWith { + return @{ auth_credssp = 1 } + } + it 'should not return anything' { + Set-TargetResource -Ensure 'Present' -Role Server | should be $null + } + it 'should have called enable'{ + Assert-MockCalled -CommandName Enable-WSManCredSSP -Times 1 -ParameterFilter {$Role -eq 'Server' -and $Force -eq $true} + } + it 'should have not called disable' { + Assert-MockCalled -CommandName Disable-WSManCredSSP -Times 0 + } + it 'Should have triggered a reboot'{ + $global:DSCMachineStatus | should be 1 + } + } + + Context "Enable Client Role" { + BeforeAll { + $global:DSCMachineStatus = $null + } + AfterAll { + $global:DSCMachineStatus = $null + } + + Mock Get-WSManCredSSP -MockWith {@([string]::Empty,[string]::Empty)} + mock Enable-WSManCredSSP -MockWith {} -Verifiable + mock Disable-WSManCredSSP -MockWith {} + mock Get-ItemProperty -MockWith { + return @{ + 1 = "wsman/testserver.domain.com" + 2 = "wsman/testserver2.domain.com" + } + } + mock Get-Item -MockWith { + $client1 = New-Object -typename PSObject| + Add-Member NoteProperty "Name" 1 -PassThru | + Add-Member NoteProperty "Property" 1 -PassThru + + $client2 = New-Object -typename PSObject| + Add-Member NoteProperty "Name" 2 -PassThru | + Add-Member NoteProperty "Property" 2 -PassThru + + return @($client1, $client2) + } + + it 'should not return anything' { + Set-TargetResource -Ensure 'Present' -Role Client -DelegateComputer 'foo' | should be $null + } + it 'should have called enable'{ + Assert-MockCalled -CommandName Enable-WSManCredSSP -Times 1 -ParameterFilter {$Role -eq 'Client' -and $Force -eq $true -and $DelegateComputer -eq 'foo'} + } + it 'should have not called disable' { + Assert-MockCalled -CommandName Disable-WSManCredSSP -Times 0 + } + it 'Should have triggered a reboot'{ + $global:DSCMachineStatus | should be 1 + } + } + Context "Enable Client Role with invalid delegate Computer parameter" { + BeforeAll { + $global:DSCMachineStatus = $null + } + AfterAll { + $global:DSCMachineStatus = $null + } + + Mock Get-WSManCredSSP -MockWith {@([string]::Empty,[string]::Empty)} + mock Enable-WSManCredSSP -MockWith {} -Verifiable + mock Disable-WSManCredSSP -MockWith {} + mock Get-ItemProperty -MockWith { + return @{ auth_credssp = 1 } + } + mock Get-Item -MockWith { + return @( + @{ + Name = 1 + Property = "wsman/foo" + }, + @{ + Name = 1 + Property = "wsman/testserver.domain.com" + } + ) + } + + it 'should throw' { + { Set-TargetResource -Ensure 'Present' -Role Client } | should throw 'DelegateComputers is required!' + } + it 'should have not called get' { + Assert-MockCalled -CommandName Get-WSManCredSSP -Times 0 -Scope 'Context' + } + it 'should have called enable' { + Assert-MockCalled -CommandName Enable-WSManCredSSP -Times 0 -Scope 'Context' + } + it 'should have not called disable' { + Assert-MockCalled -CommandName Disable-WSManCredSSP -Times 0 -Scope 'Context' + } + it 'Should have triggered a reboot'{ + $global:DSCMachineStatus | should be $null + } + } + } + #endregion + + # TODO: Pester Tests for any Helper Cmdlets + + } + #endregion +} +finally +{ + #region FOOTER + Restore-TestEnvironment -TestEnvironment $TestEnvironment + #endregion + + # TODO: Other Optional Cleanup Code Goes Here... +} diff --git a/deployment/dsc/azshcihost/xCredSSP/1.3.0.0/xCredSSP.psd1 b/deployment/dsc/azshcihost/xCredSSP/1.3.0.0/xCredSSP.psd1 new file mode 100644 index 0000000..5ceaf44 --- /dev/null +++ b/deployment/dsc/azshcihost/xCredSSP/1.3.0.0/xCredSSP.psd1 @@ -0,0 +1,60 @@ +@{ +# Version number of this module. +ModuleVersion = '1.3.0.0' + +# ID used to uniquely identify this module +GUID = '38e1ad0f-9b30-490a-a2b6-cc77765af4ec' + +# Author of this module +Author = 'Microsoft Corporation' + +# Company or vendor of this module +CompanyName = 'Microsoft Corporation' + +# Copyright statement for this module +Copyright = '(c) 2014 Microsoft Corporation. All rights reserved.' + +# Description of the functionality provided by this module +Description = 'Module with DSC Resources for WSMan CredSSP.' + +# Minimum version of the Windows PowerShell engine required by this module +PowerShellVersion = '4.0' + +# Minimum version of the common language runtime (CLR) required by this module +CLRVersion = '4.0' + +# Functions to export from this module +FunctionsToExport = '*' + +# Cmdlets to export from this module +CmdletsToExport = '*' + +# Private data to pass to the module specified in RootModule/ModuleToProcess. This may also contain a PSData hashtable with additional module metadata used by PowerShell. +PrivateData = @{ + + PSData = @{ + + # Tags applied to this module. These help with module discovery in online galleries. + Tags = @('DesiredStateConfiguration', 'DSC', 'DSCResourceKit', 'DSCResource') + + # A URL to the license for this module. + LicenseUri = 'https://github.com/PowerShell/xCredSSP/blob/master/LICENSE' + + # A URL to the main website for this project. + ProjectUri = 'https://github.com/PowerShell/xCredSSP' + + # A URL to an icon representing this module. + # IconUri = '' + + # ReleaseNotes of this module + ReleaseNotes = '* Added a fix to enable credSSP with a fresh server installation + +' + + } # End of PSData hashtable + +} # End of PrivateData hashtable +} + + + diff --git a/deployment/dsc/azshcihost/xDhcpServer/2.0.0.0/DSCResources/Helper.psm1 b/deployment/dsc/azshcihost/xDhcpServer/2.0.0.0/DSCResources/Helper.psm1 new file mode 100644 index 0000000..9a2f675 --- /dev/null +++ b/deployment/dsc/azshcihost/xDhcpServer/2.0.0.0/DSCResources/Helper.psm1 @@ -0,0 +1,267 @@ +# Localized messages +data LocalizedData +{ + # culture="en-US" + ConvertFrom-StringData @' +RoleNotFound = Please ensure that the PowerShell module for role {0} is installed +InvalidIPAddressFormat = Value of {0} property is not in a valid IP address format. Specify a valid IP address format and try again. +InvalidIPAddressFamily = The IP address {0} is not a valid {1} address. Specify a valid IP address in {1} format and try again. +InvalidTimeSpanFormat = Value of {0} property is not in a valid timespan format. Specify the timespan in days.hrs:mins:secs format and try again. +InvalidScopeIdSubnetMask = Value of byte {0} in {1} ({2}) is not valid. Binary AND with byte {0} in SubnetMask ({3}) should be equal to byte {0} in ScopeId ({4}). +InvalidStartAndEndRangeMessage = Value of IPStartRange ({0}) and IPEndRange ({1}) are not valid. Start should be lower than end. +'@ +} + +# Internal function to throw terminating error with specified ErrorCategory, ErrorId and ErrorMessage +function New-TerminatingError +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [String] + $ErrorId, + + [Parameter(Mandatory = $true)] + [String] + $ErrorMessage, + + [Parameter(Mandatory = $true)] + [System.Management.Automation.ErrorCategory] + $ErrorCategory + ) + + $exception = New-Object -TypeName System.InvalidOperationException -ArgumentList $ErrorMessage + $errorRecord = New-Object -TypeName System.Management.Automation.ErrorRecord -ArgumentList $exception, $ErrorId, $ErrorCategory, $null + throw $errorRecord +} + +# Internal function to translate a string to valid IPAddress format +function Get-ValidIPAddress +{ + [CmdletBinding()] + [OutputType([System.Net.IPAddress])] + param + ( + [Parameter(Mandatory = $true)] + [string] + $IpString, + + [Parameter(Mandatory = $true)] + [ValidateSet('IPv4')] + [String] + $AddressFamily, + + [Parameter(Mandatory = $true)] + [string] + $ParameterName + ) + + $ipAddressFamily = '' + if($AddressFamily -eq 'IPv4') + { + $ipAddressFamily = 'InterNetwork' + } + else + { + $ipAddressFamily = 'InterNetworkV6' + } + + [System.Net.IPAddress]$ipAddress = $null + $result = [System.Net.IPAddress]::TryParse($IpString, [ref]$ipAddress) + if(-not $result) + { + $errorMsg = $($LocalizedData.InvalidIPAddressFormat) -f $ParameterName + New-TerminatingError -ErrorId 'NotValidIPAddress' -ErrorMessage $errorMsg -ErrorCategory InvalidType + } + + if($ipAddress.AddressFamily -ne $ipAddressFamily) + { + $errorMsg = $($LocalizedData.InvalidIPAddressFamily) -f $ipAddress,$AddressFamily + New-TerminatingError -ErrorId 'InvalidIPAddressFamily' -ErrorMessage $errorMsg -ErrorCategory SyntaxError + } + + $ipAddress +} + +# Internal function to assert if the role specific module is installed or not +function Assert-Module +{ + [CmdletBinding()] + param + ( + [Parameter()] + [String] + $ModuleName = 'DHCPServer' + ) + + if(! (Get-Module -Name $ModuleName -ListAvailable)) + { + $errorMsg = $($LocalizedData.RoleNotFound) -f $ModuleName + New-TerminatingError -ErrorId 'ModuleNotFound' -ErrorMessage $errorMsg -ErrorCategory ObjectNotFound + } +} + +<# + .SYNOPSIS + Internal function to assert if values of ScopeId/SubnetMask/IPStartRange/IPEndRange make sense. + + .DESCRIPTION + Internal function used to assert if value of following parameters are correct: + - ScopeID + - SubnetMask + - IPStartRange + - IPEndRange + + It validates them against simple rules: + - Has to be correct (IPv4) address + - Anything but SubnetMask has to follow the rule that: + (TokenFromParameter) -band (TokenFromSubnetMask) = (TokenFromScopeId) + - IPStartRange has to be before IPEndRange + Implementation for IPv4. + + .PARAMETER ScopeId + String version of ScopeId. + + .PARAMETER SubnetMask + String version of SubnetMask. + + .PARAMETER IPStartRange + String version of StartRange. + + .PARAMETER IPEndRange + String version of EndRange. + + .PARAMETER AddressFamily + AddressFamily that IPs should validate against. + + .EXAMPLE + Assert-ScopeParameter -ScopeId 192.168.1.0 -SubnetMask 255.255.255.0 -IPStartRange 192.168.1.1 -IPEndRange 192.168.1.254 -AddressFamily IPv4 + Validates all parameters against rules and returns nothing (all parameters are correct). + + .EXAMPLE + Assert-ScopeParameter -ScopeId 192.168.1.0 -SubnetMask 255.255.240.0 -IPStartRange 192.168.1.1 -IPEndRange 192.168.1.254 -AddressFamily IPv4 + Returns error informing that using specified SubnetMask with specified ScopeId is incorrect: + Value of byte 3 in ScopeId (1) is not valid. Binary AND with byte 3 in SubnetMask (240) should be equal to byte 3 in ScopeId (1). +#> +function Assert-ScopeParameter +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [String] + $ScopeId, + + [Parameter(Mandatory = $true)] + [String] + $SubnetMask, + + [Parameter(Mandatory = $true)] + [String] + $IPStartRange, + + [Parameter(Mandatory = $true)] + [String] + $IPEndRange, + + [Parameter(Mandatory = $true)] + [String] + $AddressFamily + ) + + # Convert the Subnet Mask to be a valid IPAddress + $netMask = Get-ValidIpAddress -IpString $SubnetMask -AddressFamily $AddressFamily -ParameterName SubnetMask + + # Convert the ScopeID to be a valid IPAddress + $scope = Get-ValidIPAddress -IpString $ScopeId -AddressFamily $AddressFamily -ParameterName ScopeId + + # Convert the Start Range to be a valid IPAddress + $startRange = Get-ValidIpAddress -IpString $IPStartRange -AddressFamily $AddressFamily -ParameterName IPStartRange + + # Convert the End Range to be a valid IPAddress + $endRange = Get-ValidIpAddress -IpString $IPEndRange -AddressFamily $AddressFamily -ParameterName IPEndRange + + # Check to ensure startRange is smaller than endRange + if($endRange.Address -lt $startRange.Address) + { + $errorMsg = $LocalizedData.InvalidStartAndEndRangeMessage -f $IPStartRange, $IPEndRange + New-TerminatingError -ErrorId RangeNotCorrect -ErrorMessage $errorMsg -ErrorCategory InvalidArgument + } + + $addressBytes = @{ + ScopeId = $scope.GetAddressBytes() + SubnetMask = $netMask.GetAddressBytes() + IPStartRange = $startRange.GetAddressBytes() + IPEndRange = $endRange.GetAddressBytes() + } + + foreach ($parameter in $addressBytes.Keys.Where{ $_ -ne 'SubnetMask' }) + { + foreach ($ipTokenIndex in 0..3) + { + $parameterByte = $addressBytes[$parameter][$ipTokenIndex] + $subnetMaskByte = $addressBytes['SubnetMask'][$ipTokenIndex] + $scopeIdByte = $addressBytes['ScopeId'][$ipTokenIndex] + if(($parameterByte -band $subnetMaskByte) -ne $scopeIdByte) + { + $errorMsg = $($LocalizedData.InvalidScopeIdSubnetMask) -f ($ipTokenIndex + 1), $parameter, $parameterByte, $subnetMaskByte, $scopeIdByte + New-TerminatingError -ErrorId ScopeIdOrMaskIncorrect -ErrorMessage $errorMsg -ErrorCategory InvalidArgument + } + } + } +} + +# Internal function to write verbose messages for collection of properties +function Write-PropertyMessage +{ + param + ( + [Parameter(Mandatory = $true)] + [Hashtable] + $Parameters, + + [Parameter(Mandatory = $true)] + [String[]] + $KeysToSkip, + + [Parameter(Mandatory = $true)] + [String] + $MessageTemplate + ) + + foreach($key in $parameters.keys) + { + if($keysToSkip -notcontains $key) + { + $msg = $MessageTemplate -f $key,$parameters[$key] + Write-Verbose -Message $msg + } + } +} + +# Internal function to translate a string to valid IPAddress format +function Get-ValidTimeSpan +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [string] + $TsString, + + [Parameter(Mandatory = $true)] + [string] + $ParameterName + ) + + [System.TimeSpan]$timeSpan = New-TimeSpan + $result = [System.TimeSpan]::TryParse($TsString, [ref]$timeSpan) + if(-not $result) + { + $errorMsg = $($LocalizedData.InvalidTimeSpanFormat) -f $ParameterName + New-TerminatingError -ErrorId 'NotValidTimeSpan' -ErrorMessage $errorMsg -ErrorCategory InvalidType + } + + $timeSpan +} diff --git a/deployment/dsc/azshcihost/xDhcpServer/2.0.0.0/DSCResources/MSFT_DhcpPolicyOptionValue/MSFT_DhcpPolicyOptionValue.psm1 b/deployment/dsc/azshcihost/xDhcpServer/2.0.0.0/DSCResources/MSFT_DhcpPolicyOptionValue/MSFT_DhcpPolicyOptionValue.psm1 new file mode 100644 index 0000000..0bfa09f --- /dev/null +++ b/deployment/dsc/azshcihost/xDhcpServer/2.0.0.0/DSCResources/MSFT_DhcpPolicyOptionValue/MSFT_DhcpPolicyOptionValue.psm1 @@ -0,0 +1,207 @@ +$currentPath = Split-Path -Path $MyInvocation.MyCommand.Path -Parent + +$modulePathhelper = (Join-Path -Path (Split-Path -Path $currentPath -Parent) -ChildPath 'Helper.psm1') +$modulePathOptionValueHelper = (Join-Path -Path (Join-Path -Path (Join-Path -Path (Split-Path -Path (Split-Path -Path $currentPath -Parent) -Parent) ` + -ChildPath 'modules') -ChildPath 'DhcpServerDsc.OptionValueHelper') -ChildPath 'OptionValueHelper.psm1') + +Import-Module -Name $modulePathhelper +Import-Module -Name $modulePathOptionValueHelper + +<# + .SYNOPSIS + This function gets a DHCP policy option value. + + .PARAMETER PolicyName + The Policy name. + + .PARAMETER OptionId + The ID of the option. + + .PARAMETER ScopeId + The scope ID to get the value. If not used server level values are retrieved. + + .PARAMETER VendorClass + The vendor class of the option. Use an empty string for standard class. + + .PARAMETER AddressFamily + The option definition address family (IPv4 or IPv6). Currently only the IPv4 is supported. +#> +function Get-TargetResource +{ + [CmdletBinding()] + [OutputType([System.Collections.Hashtable])] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [String] + $PolicyName, + + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [UInt32] + $OptionId, + + [Parameter()] + [AllowNull()] + [String] + $ScopeId, + + [Parameter(Mandatory = $true)] + [AllowEmptyString()] + [String] + $VendorClass, + + [Parameter(Mandatory = $true)] + [ValidateSet('IPv4')] + [String] + $AddressFamily + ) + + $hashTable = Get-TargetResourceHelper -ApplyTo 'Policy' -UserClass '' @PSBoundParameters + + # Removing properties that are not in the schema.mof before returning the hash table + $hashTable.Remove('ApplyTo') + $hashTable.Remove('ReservedIP') + $hashTable.Remove('UserClass') + + $hashTable +} + +<# + .SYNOPSIS + This function sets a DHCP policy option value. + + .PARAMETER PolicyName + The policy name. + + .PARAMETER OptionId + The ID of the option. + + .PARAMETER Value + The data value option. + + .PARAMETER ScopeId + The scope ID to set the value. If not used server level values are used. + + .PARAMETER VendorClass + The vendor class of the option. Use an empty string for standard class. + + .PARAMETER AddressFamily + The option definition address family (IPv4 or IPv6). Currently only the IPv4 is supported. + + .PARAMETER Ensure + When set to 'Present', the option will be created. + When set to 'Absent', the option will be removed. +#> +function Set-TargetResource +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [String] + $PolicyName, + + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [UInt32] + $OptionId, + + [Parameter()] + [String[]] + $Value, + + [Parameter()] + [String] + $ScopeId, + + [Parameter(Mandatory = $true)] + [AllowEmptyString()] + [String] + $VendorClass, + + [Parameter(Mandatory = $true)] + [ValidateSet('IPv4')] + [String] + $AddressFamily, + + [Parameter()] + [ValidateSet('Present','Absent')] + [String] + $Ensure = 'Present' + ) + + Set-TargetResourceHelper -ApplyTo 'Policy' -UserClass '' @PSBoundParameters +} + +<# + .SYNOPSIS + This function tests a DHCP policy option value. + + .PARAMETER PolicyName + The policy name. + + .PARAMETER OptionId + The ID of the option. + + .PARAMETER Value + The data value option. + + .PARAMETER ScopeId + The scope ID to test the value. If not used server level values are tested. + + .PARAMETER VendorClass + The vendor class of the option. Use an empty string for standard class. + + .PARAMETER AddressFamily + The option definition address family (IPv4 or IPv6). Currently only the IPv4 is supported. + + .PARAMETER Ensure + When set to 'Present', the option will be created. + When set to 'Absent', the option will be removed. +#> +function Test-TargetResource +{ + [CmdletBinding()] + [OutputType([System.Boolean])] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [String] + $PolicyName, + + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [UInt32] + $OptionId, + + [Parameter()] + [String[]] + $Value, + + [Parameter()] + [String] + $ScopeId, + + [Parameter(Mandatory = $true)] + [AllowEmptyString()] + [String] + $VendorClass, + + [Parameter(Mandatory = $true)] + [ValidateSet('IPv4')] + [String] + $AddressFamily, + + [Parameter()] + [ValidateSet('Present','Absent')] + [String] + $Ensure = 'Present' + ) + + $result = Test-TargetResourceHelper -ApplyTo 'Policy' -UserClass '' @PSBoundParameters + $result +} diff --git a/deployment/dsc/azshcihost/xDhcpServer/2.0.0.0/DSCResources/MSFT_DhcpPolicyOptionValue/MSFT_DhcpPolicyOptionValue.schema.mof b/deployment/dsc/azshcihost/xDhcpServer/2.0.0.0/DSCResources/MSFT_DhcpPolicyOptionValue/MSFT_DhcpPolicyOptionValue.schema.mof new file mode 100644 index 0000000..1885693 --- /dev/null +++ b/deployment/dsc/azshcihost/xDhcpServer/2.0.0.0/DSCResources/MSFT_DhcpPolicyOptionValue/MSFT_DhcpPolicyOptionValue.schema.mof @@ -0,0 +1,11 @@ +[ClassVersion("1.0.0.0"), FriendlyName("DhcpPolicyOptionValue")] +class MSFT_DhcpPolicyOptionValue : OMI_BaseResource +{ + [Key, Description("Option ID, specify an integer between 1 and 255.")] UInt32 OptionId; + [Key, Description("Policy Name.")] String PolicyName; + [Write, Description("Option data value. Could be an array of string for a multivalued option.")] String Value[]; + [Write, Description("Scope ID to get policy values from. Do not use it to get an option from server level.")] String ScopeId; + [Key, Description("Vendor class. Use an empty string for default vendor class.")] String VendorClass; + [Key, Description("Address family. Currently needs to be IPv4."), ValueMap{"IPv4"}, Values{"IPv4"}] String AddressFamily; + [Write, Description("Whether the DHCP option should exist."), ValueMap{"Present","Absent"}, Values{"Present","Absent"}] String Ensure; +}; diff --git a/deployment/dsc/azshcihost/xDhcpServer/2.0.0.0/DSCResources/MSFT_DhcpReservedIPOptionValue/MSFT_DhcpReservedIPOptionValue.psm1 b/deployment/dsc/azshcihost/xDhcpServer/2.0.0.0/DSCResources/MSFT_DhcpReservedIPOptionValue/MSFT_DhcpReservedIPOptionValue.psm1 new file mode 100644 index 0000000..94c6378 --- /dev/null +++ b/deployment/dsc/azshcihost/xDhcpServer/2.0.0.0/DSCResources/MSFT_DhcpReservedIPOptionValue/MSFT_DhcpReservedIPOptionValue.psm1 @@ -0,0 +1,209 @@ +$currentPath = Split-Path -Path $MyInvocation.MyCommand.Path -Parent + +$modulePathhelper = (Join-Path -Path (Split-Path -Path $currentPath -Parent) -ChildPath 'Helper.psm1') +$modulePathOptionValueHelper = (Join-Path -Path (Join-Path -Path (Join-Path -Path (Split-Path -Path (Split-Path -Path $currentPath -Parent) -Parent) ` + -ChildPath 'modules') -ChildPath 'DhcpServerDsc.OptionValueHelper') -ChildPath 'OptionValueHelper.psm1') + +Import-Module -Name $modulePathhelper +Import-Module -Name $modulePathOptionValueHelper + +<# + .SYNOPSIS + This function gets a DHCP reserved IP option value. + + .PARAMETER ReservedIP + The Reserved IP to get the option value from. + + .PARAMETER OptionId + The ID of the option. + + .PARAMETER VendorClass + The vendor class of the option. Use an empty string for standard class. + + .PARAMETER UserClass + The user class of the option. + + .PARAMETER AddressFamily + The option definition address family (IPv4 or IPv6). Currently only the IPv4 is supported. +#> +function Get-TargetResource +{ + [CmdletBinding()] + [OutputType([System.Collections.Hashtable])] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [String] + $ReservedIP, + + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [UInt32] + $OptionId, + + [Parameter(Mandatory = $true)] + [AllowEmptyString()] + [String] + $VendorClass, + + [Parameter(Mandatory = $true)] + [AllowEmptyString()] + [String] + $UserClass, + + [Parameter(Mandatory = $true)] + [ValidateSet('IPv4')] + [String] + $AddressFamily + ) + + $hashTable = Get-TargetResourceHelper -ApplyTo 'ReservedIP' @PSBoundParameters + + # Removing properties that are not in the schema.mof before returning the hash table + $hashTable.Remove('ApplyTo') + $hashTable.Remove('ScopeId') + $hashTable.Remove('PolicyName') + + $hashTable +} + +<# + .SYNOPSIS + This function sets a DHCP reserved IP option value. + + .PARAMETER ReservedIP + The reserved IP. + + .PARAMETER OptionId + The Option ID. + + .PARAMETER Value + The option data value. + + .PARAMETER VendorClass + The vendor class of the option. Use an empty string for standard class. + + .PARAMETER UserClass + The user class of the option. + + .PARAMETER AddressFamily + The option definition address family (IPv4 or IPv6). Currently only the IPv4 is supported. + + .PARAMETER Ensure + When set to 'Present', the option will be created. + When set to 'Absent', the option will be removed. +#> +function Set-TargetResource +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [String] + $ReservedIP, + + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [UInt32] + $OptionId, + + [Parameter()] + [String[]] + $Value, + + [Parameter(Mandatory = $true)] + [AllowEmptyString()] + [String] + $VendorClass, + + [Parameter(Mandatory = $true)] + [AllowEmptyString()] + [String] + $UserClass, + + [Parameter(Mandatory = $true)] + [ValidateSet('IPv4')] + [String] + $AddressFamily, + + [Parameter()] + [ValidateSet('Present','Absent')] + [String] + $Ensure = 'Present' + ) + + Set-TargetResourceHelper -ApplyTo 'ReservedIP' @PSBoundParameters +} + +<# + .SYNOPSIS + This function tests a DHCP reserved IP option value. + + .PARAMETER ReservedIP + The reserved IP. + + .PARAMETER OptionId + The Option ID. + + .PARAMETER Value + The option data value. + + .PARAMETER VendorClass + The vendor class of the option. Use an empty string for standard class. + + .PARAMETER UserClass + The user class of the option. + + .PARAMETER AddressFamily + The option definition address family (IPv4 or IPv6). Currently only the IPv4 is supported. + + .PARAMETER Ensure + When set to 'Present', the option will be created. + When set to 'Absent', the option will be removed. +#> +function Test-TargetResource +{ + [CmdletBinding()] + [OutputType([System.Boolean])] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [String] + $ReservedIP, + + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [UInt32] + $OptionId, + + [Parameter()] + [String[]] + $Value, + + [Parameter(Mandatory = $true)] + [AllowEmptyString()] + [String] + $VendorClass, + + [Parameter(Mandatory = $true)] + [AllowEmptyString()] + [String] + $UserClass, + + [Parameter(Mandatory = $true)] + [ValidateSet('IPv4')] + [String] + $AddressFamily, + + [Parameter()] + [ValidateSet('Present','Absent')] + [String] + $Ensure = 'Present' + ) + + $result = Test-TargetResourceHelper -ApplyTo 'ReservedIP' @PSBoundParameters + $result +} diff --git a/deployment/dsc/azshcihost/xDhcpServer/2.0.0.0/DSCResources/MSFT_DhcpReservedIPOptionValue/MSFT_DhcpReservedIPOptionValue.schema.mof b/deployment/dsc/azshcihost/xDhcpServer/2.0.0.0/DSCResources/MSFT_DhcpReservedIPOptionValue/MSFT_DhcpReservedIPOptionValue.schema.mof new file mode 100644 index 0000000..8f1ec5b --- /dev/null +++ b/deployment/dsc/azshcihost/xDhcpServer/2.0.0.0/DSCResources/MSFT_DhcpReservedIPOptionValue/MSFT_DhcpReservedIPOptionValue.schema.mof @@ -0,0 +1,11 @@ +[ClassVersion("1.0.0.0"), FriendlyName("DhcpReservedIPOptionValue")] +class MSFT_DhcpReservedIPOptionValue : OMI_BaseResource +{ + [Key, Description("Reserved IP to set the option value.")] String ReservedIP; + [Key, Description("Option ID, specify an integer between 1 and 255.")] UInt32 OptionId; + [Write, Description("Option data value. Could be an array of string for a multivalued option.")] String Value[]; + [Key, Description("Vendor class. Use an empty string for default vendor class.")] String VendorClass; + [Key, Description("User class. Use an empty string for default user class.")] String UserClass; + [Key, Description("Address family. Currently needs to be IPv4."), ValueMap{"IPv4"}, Values{"IPv4"}] String AddressFamily; + [Write, Description("Whether the DHCP option should exist."), ValueMap{"Present","Absent"}, Values{"Present","Absent"}] String Ensure; +}; diff --git a/deployment/dsc/azshcihost/xDhcpServer/2.0.0.0/DSCResources/MSFT_DhcpScopeOptionValue/MSFT_DhcpScopeOptionValue.psm1 b/deployment/dsc/azshcihost/xDhcpServer/2.0.0.0/DSCResources/MSFT_DhcpScopeOptionValue/MSFT_DhcpScopeOptionValue.psm1 new file mode 100644 index 0000000..37ade8d --- /dev/null +++ b/deployment/dsc/azshcihost/xDhcpServer/2.0.0.0/DSCResources/MSFT_DhcpScopeOptionValue/MSFT_DhcpScopeOptionValue.psm1 @@ -0,0 +1,209 @@ +$currentPath = Split-Path -Path $MyInvocation.MyCommand.Path -Parent + +$modulePathhelper = (Join-Path -Path (Split-Path -Path $currentPath -Parent) -ChildPath 'Helper.psm1') +$modulePathOptionValueHelper = (Join-Path -Path (Join-Path -Path (Join-Path -Path (Split-Path -Path (Split-Path -Path $currentPath -Parent) -Parent) ` + -ChildPath 'modules') -ChildPath 'DhcpServerDsc.OptionValueHelper') -ChildPath 'OptionValueHelper.psm1') + +Import-Module -Name $modulePathhelper +Import-Module -Name $modulePathOptionValueHelper + +<# + .SYNOPSIS + This function gets a DHCP scope option value. + + .PARAMETER ScopeId + The ID of the scope. + + .PARAMETER OptionId + The ID of the option. + + .PARAMETER VendorClass + The vendor class of the option. Use an empty string for standard class. + + .PARAMETER UserClass + The user class of the option. + + .PARAMETER AddressFamily + The option definition address family (IPv4 or IPv6). Currently only the IPv4 is supported. +#> +function Get-TargetResource +{ + [CmdletBinding()] + [OutputType([System.Collections.Hashtable])] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [String] + $ScopeId, + + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [UInt32] + $OptionId, + + [Parameter(Mandatory = $true)] + [AllowEmptyString()] + [String] + $VendorClass, + + [Parameter(Mandatory = $true)] + [AllowEmptyString()] + [String] + $UserClass, + + [Parameter(Mandatory = $true)] + [ValidateSet('IPv4')] + [String] + $AddressFamily + ) + + $hashTable = Get-TargetResourceHelper -ApplyTo 'Scope' @PSBoundParameters + + # Removing properties that are not in the schema.mof before returning the hash table + $hashTable.Remove('ApplyTo') + $hashTable.Remove('PolicyName') + $hashTable.Remove('ReservedIP') + + $hashTable +} + +<# + .SYNOPSIS + This function sets a DHCP scope option value. + + .PARAMETER ScopeId + The ID of the scope. + + .PARAMETER OptionId + The ID of the option. + + .PARAMETER Value + The data value option. + + .PARAMETER VendorClass + The vendor class of the option. Use an empty string for standard class. + + .PARAMETER UserClass + The user class of the option. + + .PARAMETER AddressFamily + The option definition address family (IPv4 or IPv6). Currently only the IPv4 is supported. + + .PARAMETER Ensure + When set to 'Present', the option will be created. + When set to 'Absent', the option will be removed. +#> +function Set-TargetResource +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [String] + $ScopeId, + + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [UInt32] + $OptionId, + + [Parameter()] + [String[]] + $Value, + + [Parameter(Mandatory = $true)] + [AllowEmptyString()] + [String] + $VendorClass, + + [Parameter(Mandatory = $true)] + [AllowEmptyString()] + [String] + $UserClass, + + [Parameter(Mandatory = $true)] + [ValidateSet('IPv4')] + [String] + $AddressFamily, + + [Parameter()] + [ValidateSet('Present','Absent')] + [String] + $Ensure = 'Present' + ) + + Set-TargetResourceHelper -ApplyTo 'Scope' @PSBoundParameters +} + +<# + .SYNOPSIS + This function tests a DHCP scope option value. + + .PARAMETER ScopeId + The ID of the scope. + + .PARAMETER OptionId + The ID of the option. + + .PARAMETER Value + The data value option. + + .PARAMETER VendorClass + The vendor class of the option. Use an empty string for standard class. + + .PARAMETER UserClass + The user class of the option. + + .PARAMETER AddressFamily + The option definition address family (IPv4 or IPv6). Currently only the IPv4 is supported. + + .PARAMETER Ensure + When set to 'Present', the option will be created. + When set to 'Absent', the option will be removed. +#> +function Test-TargetResource +{ + [CmdletBinding()] + [OutputType([System.Boolean])] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [String] + $ScopeId, + + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [UInt32] + $OptionId, + + [Parameter()] + [String[]] + $Value, + + [Parameter(Mandatory = $true)] + [AllowEmptyString()] + [String] + $VendorClass, + + [Parameter(Mandatory = $true)] + [AllowEmptyString()] + [String] + $UserClass, + + [Parameter(Mandatory = $true)] + [ValidateSet('IPv4')] + [String] + $AddressFamily, + + [Parameter()] + [ValidateSet('Present','Absent')] + [String] + $Ensure = 'Present' + ) + + $result = Test-TargetResourceHelper -ApplyTo 'Scope' @PSBoundParameters + $result +} diff --git a/deployment/dsc/azshcihost/xDhcpServer/2.0.0.0/DSCResources/MSFT_DhcpScopeOptionValue/MSFT_DhcpScopeOptionValue.schema.mof b/deployment/dsc/azshcihost/xDhcpServer/2.0.0.0/DSCResources/MSFT_DhcpScopeOptionValue/MSFT_DhcpScopeOptionValue.schema.mof new file mode 100644 index 0000000..e5a40f2 --- /dev/null +++ b/deployment/dsc/azshcihost/xDhcpServer/2.0.0.0/DSCResources/MSFT_DhcpScopeOptionValue/MSFT_DhcpScopeOptionValue.schema.mof @@ -0,0 +1,11 @@ +[ClassVersion("1.0.0.0"), FriendlyName("DhcpScopeOptionValue")] +class MSFT_DhcpScopeOptionValue : OMI_BaseResource +{ + [Key, Description("Scope ID to set the option.")] String ScopeId; + [Key, Description("Option ID, specify an integer between 1 and 255.")] UInt32 OptionId; + [Write, Description("Option data value.")] String Value[]; + [Key, Description("Vendor class. Use an empty string for default vendor class.")] String VendorClass; + [Key, Description("User class. Use an empty string for default user class.")] String UserClass; + [Key, Description("Address family. Currently needs to be IPv4."), ValueMap{"IPv4"}, Values{"IPv4"}] String AddressFamily; + [Write, Description("Whether the DHCP option should exist."), ValueMap{"Present","Absent"}, Values{"Present","Absent"}] String Ensure; +}; diff --git a/deployment/dsc/azshcihost/xDhcpServer/2.0.0.0/DSCResources/MSFT_DhcpServerOptionValue/MSFT_DhcpServerOptionValue.psm1 b/deployment/dsc/azshcihost/xDhcpServer/2.0.0.0/DSCResources/MSFT_DhcpServerOptionValue/MSFT_DhcpServerOptionValue.psm1 new file mode 100644 index 0000000..de7b23c --- /dev/null +++ b/deployment/dsc/azshcihost/xDhcpServer/2.0.0.0/DSCResources/MSFT_DhcpServerOptionValue/MSFT_DhcpServerOptionValue.psm1 @@ -0,0 +1,187 @@ +$currentPath = Split-Path -Path $MyInvocation.MyCommand.Path -Parent + +$modulePathhelper = (Join-Path -Path (Split-Path -Path $currentPath -Parent) -ChildPath 'Helper.psm1') +$modulePathOptionValueHelper = (Join-Path -Path (Join-Path -Path (Join-Path -Path (Split-Path -Path (Split-Path -Path $currentPath -Parent) -Parent) ` + -ChildPath 'modules') -ChildPath 'DhcpServerDsc.OptionValueHelper') -ChildPath 'OptionValueHelper.psm1') + +Import-Module -Name $modulePathhelper +Import-Module -Name $modulePathOptionValueHelper + +<# + .SYNOPSIS + This function gets a DHCP server option value. + + .PARAMETER OptionId + The ID of the option. + + .PARAMETER VendorClass + The vendor class of the option. Use an empty string for standard class. + + .PARAMETER UserClass + The user class of the option. + + .PARAMETER AddressFamily + The option definition address family (IPv4 or IPv6). Currently only the IPv4 is supported. +#> +function Get-TargetResource +{ + [CmdletBinding()] + [OutputType([System.Collections.Hashtable])] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [UInt32] + $OptionId, + + [Parameter(Mandatory = $true)] + [AllowEmptyString()] + [String] + $VendorClass, + + [Parameter(Mandatory = $true)] + [AllowEmptyString()] + [String] + $UserClass, + + [Parameter(Mandatory = $true)] + [ValidateSet('IPv4')] + [String] + $AddressFamily + ) + + $hashTable = Get-TargetResourceHelper -ApplyTo 'Server' @PSBoundParameters + + # Removing properties that are not in the schema.mof before returning the hash table + $hashTable.Remove('ApplyTo') + $hashTable.Remove('PolicyName') + $hashTable.Remove('ReservedIP') + $hashTable.Remove('ScopeId') + + $hashTable +} + +<# + .SYNOPSIS + This function sets a DHCP server option value. + + .PARAMETER OptionId + The ID of the option. + + .PARAMETER Value + The data value option. + + .PARAMETER VendorClass + The vendor class of the option. Use an empty string for standard class. + + .PARAMETER UserClass + The user class of the option. + + .PARAMETER AddressFamily + The option definition address family (IPv4 or IPv6). Currently only the IPv4 is supported. + + .PARAMETER Ensure + When set to 'Present', the option will be created. + When set to 'Absent', the option will be removed. +#> +function Set-TargetResource +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [UInt32] + $OptionId, + + [Parameter()] + [String[]] + $Value, + + [Parameter(Mandatory = $true)] + [AllowEmptyString()] + [String] + $VendorClass, + + [Parameter(Mandatory = $true)] + [AllowEmptyString()] + [String] + $UserClass, + + [Parameter(Mandatory = $true)] + [ValidateSet('IPv4')] + [String] + $AddressFamily, + + [Parameter()] + [ValidateSet('Present','Absent')] + [String] + $Ensure = 'Present' + ) + + Set-TargetResourceHelper -ApplyTo 'Server' @PSBoundParameters +} + +<# + .SYNOPSIS + This function tests a DHCP server option value. + + .PARAMETER OptionId + The ID of the option. + + .PARAMETER Value + The data value option. + + .PARAMETER VendorClass + The vendor class of the option. Use an empty string for standard class. + + .PARAMETER UserClass + The user class of the option. + + .PARAMETER AddressFamily + The option definition address family (IPv4 or IPv6). Currently only the IPv4 is supported. + + .PARAMETER Ensure + When set to 'Present', the option will be created. + When set to 'Absent', the option will be removed. +#> +function Test-TargetResource +{ + [CmdletBinding()] + [OutputType([System.Boolean])] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [UInt32] + $OptionId, + + [Parameter()] + [String[]] + $Value, + + [Parameter(Mandatory = $true)] + [AllowEmptyString()] + [String] + $VendorClass, + + [Parameter(Mandatory = $true)] + [AllowEmptyString()] + [String] + $UserClass, + + [Parameter(Mandatory = $true)] + [ValidateSet('IPv4')] + [String] + $AddressFamily, + + [Parameter()] + [ValidateSet('Present','Absent')] + [String] + $Ensure = 'Present' + ) + + + $result = Test-TargetResourceHelper -ApplyTo 'Server' @PSBoundParameters + $result +} diff --git a/deployment/dsc/azshcihost/xDhcpServer/2.0.0.0/DSCResources/MSFT_DhcpServerOptionValue/MSFT_DhcpServerOptionValue.schema.mof b/deployment/dsc/azshcihost/xDhcpServer/2.0.0.0/DSCResources/MSFT_DhcpServerOptionValue/MSFT_DhcpServerOptionValue.schema.mof new file mode 100644 index 0000000..c5d1d5f --- /dev/null +++ b/deployment/dsc/azshcihost/xDhcpServer/2.0.0.0/DSCResources/MSFT_DhcpServerOptionValue/MSFT_DhcpServerOptionValue.schema.mof @@ -0,0 +1,10 @@ +[ClassVersion("1.0.0.0"), FriendlyName("DhcpServerOptionValue")] +class MSFT_DhcpServerOptionValue : OMI_BaseResource +{ + [Key, Description("Option ID, specify an integer between 1 and 255.")] UInt32 OptionId; + [Write, Description("Option data value. Could be an array of string for a multivalued option.")] String Value[]; + [Key, Description("Vendor class. Use an empty string for default vendor class.")] String VendorClass; + [Key, Description("User class. Use an empty string for default user class.")] String UserClass; + [Key, Description("Address family. Currently needs to be IPv4."), ValueMap{"IPv4"}, Values{"IPv4"}] String AddressFamily; + [Write, Description("Whether the DHCP option should exist."), ValueMap{"Present","Absent"}, Values{"Present","Absent"}] String Ensure; +}; diff --git a/deployment/dsc/azshcihost/xDhcpServer/2.0.0.0/DSCResources/MSFT_xDhcpServerAuthorization/MSFT_xDhcpServerAuthorization.psm1 b/deployment/dsc/azshcihost/xDhcpServer/2.0.0.0/DSCResources/MSFT_xDhcpServerAuthorization/MSFT_xDhcpServerAuthorization.psm1 new file mode 100644 index 0000000..71be251 --- /dev/null +++ b/deployment/dsc/azshcihost/xDhcpServer/2.0.0.0/DSCResources/MSFT_xDhcpServerAuthorization/MSFT_xDhcpServerAuthorization.psm1 @@ -0,0 +1,171 @@ +Import-Module $PSScriptRoot\..\Helper.psm1 -Verbose:$false + +# Localized messages +data LocalizedData +{ + # culture="en-US" + ConvertFrom-StringData @' +ResolvingIPv4Address = Resolving first local IPv4 IP address ... +ResolvingHostname = Resolving local hostname ... +AuthorizingServer = Authorizing DHCP Server '{0}' with IP address '{1}' +UnauthorizingServer = Unauthorizing DHCP Server '{0}' with IP address '{1}' +ServerIsAuthorized = DHCP Server '{0}' with IP address '{1}' IS authorized +ServerNotAuthorized = DHCP Server '{0}' with IP address '{1}' is NOT authorized +IncorrectPropertyValue = Property '{0}' is incorrect. Expected '{1}', actual '{2}' +ResourceInDesiredState = DHCP Server '{0}' is in the desired state +ResourceNotInDesiredState = DHCP Server '{0}' is NOT in the desired state +'@ +} + +function Get-TargetResource +{ + [CmdletBinding()] + [OutputType([System.Collections.Hashtable])] + param + ( + [Parameter(Mandatory)] + [ValidateSet('Present','Absent')] + [System.String] $Ensure, + + [ValidateNotNullOrEmpty()] + [System.String] $DnsName = ( Get-Hostname ), + + [ValidateNotNullOrEmpty()] + [System.String] $IPAddress = ( Get-IPv4Address | Select-Object -First 1 ) + ) + Assert-Module -ModuleName 'DHCPServer'; + $IPAddress = Get-ValidIPAddress -IPString $IPAddress -AddressFamily 'IPv4' -ParameterName 'IPAddress' + $dhcpServer = Get-DhcpServerInDC | Where-Object { ($_.DnsName -eq $DnsName) -and ($_.IPAddress -eq $IPAddress) } + $targetResource = @{ + DnsName = $dhcpServer.DnsName + IPAddress = $dhcpServer.IPAddress + } + if ($dhcpServer) + { + Write-Verbose ($LocalizedData.ServerIsAuthorized -f $DnsName, $IPAddress) + $targetResource['Ensure'] = 'Present' + } + else + { + Write-Verbose ($LocalizedData.ServerNotAuthorized -f $DnsName, $IPAddress) + $targetResource['Ensure'] = 'Absent' + } + return $targetResource +} + +function Set-TargetResource +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory)] + [ValidateSet('Present','Absent')] + [System.String] $Ensure, + + [ValidateNotNullOrEmpty()] + [System.String] $DnsName = ( Get-Hostname ), + + [ValidateNotNullOrEmpty()] + [System.String] $IPAddress = ( Get-IPv4Address | Select-Object -First 1 ) + ) + Assert-Module -ModuleName 'DHCPServer' + $IPAddress = Get-ValidIPAddress -IPString $IPAddress -AddressFamily 'IPv4' -ParameterName 'IPAddress' + if ($Ensure -eq 'Present') + { + Write-Verbose ($LocalizedData.AuthorizingServer -f $DnsName, $IPAddress) + Add-DhcpServerInDc -DnsName $DnsName -IPAddress $IPAddress + } + elseif ($Ensure -eq 'Absent') + { + Write-Verbose ($LocalizedData.UnauthorizingServer -f $DnsName, $IPAddress) + Get-DhcpServerInDC | Where-Object { ($_.DnsName -eq $DnsName) -and ($_.IPAddress -eq $IPAddress) } | Remove-DhcpServerInDc + } +} + +function Test-TargetResource +{ + [CmdletBinding()] + [OutputType([System.Boolean])] + param + ( + [Parameter(Mandatory)] + [ValidateSet('Present','Absent')] + [System.String] $Ensure, + + [ValidateNotNullOrEmpty()] + [System.String] $DnsName = ( Get-Hostname ), + + [ValidateNotNullOrEmpty()] + [System.String] $IPAddress = ( Get-IPv4Address | Select-Object -First 1 ) + ) + $targetResource = Get-TargetResource @PSBoundParameters + $isCompliant = $true + + if ($targetResource.Ensure -ne $Ensure) + { + Write-Verbose ($LocalizedData.IncorrectPropertyValue -f 'Ensure', $Ensure, $targetResource.Ensure) + $isCompliant = $false + + } + elseif ($Ensure -eq 'Present') + { + if ($targetResource.DnsName -ne $DnsName) + { + Write-Verbose ($LocalizedData.IncorrectPropertyValue -f 'DnsName', $DnsName, $targetResource.DnsName) + $isCompliant = $false + } + if ($targetResource.IPAddress -ne $IPAddress) + { + Write-Verbose ($LocalizedData.IncorrectPropertyValue -f 'IPAddress', $IPAddress, $targetResource.IPAddress) + $isCompliant = $false + } + } + + if ($isCompliant) + { + Write-Verbose ($LocalizedData.ResourceInDesiredState -f $DnsName) + } + else { + Write-Verbose ($LocalizedData.ResourceNotInDesiredState -f $DnsName) + } + return $isCompliant +} + +## Internal function used to return all IPv4 addresses +function Get-IPv4Address +{ + [CmdletBinding()] + [OutputType([System.String])] + param ( ) + process + { + Write-Verbose $LocalizedData.ResolvingIPv4Address + Get-WmiObject Win32_NetworkAdapterConfiguration -Namespace 'root\CIMV2' | + Where-Object IPEnabled -eq 'True' | + ForEach-Object { + Write-Output ($_.IPAddress -notmatch ':') + } + } #end process +} #end function Get-IPv4Address + +## Internal function used to resolve the local hostname +function Get-Hostname { + [CmdletBinding()] + [OutputType([System.String])] + param ( ) + process + { + Write-Verbose $LocalizedData.ResolvingHostname; + $globalIpProperties = [System.Net.NetworkInformation.IPGlobalProperties]::GetIPGlobalProperties(); + if ($globalIpProperties.DomainName) + { + return '{0}.{1}' -f $globalIpProperties.HostName, $globalIpProperties.DomainName + } + else + { + return $globalIpProperties.HostName + } + } #end process +} #end function Get-Hostname + +Export-ModuleMember -Function *-TargetResource; diff --git a/deployment/dsc/azshcihost/xDhcpServer/2.0.0.0/DSCResources/MSFT_xDhcpServerAuthorization/MSFT_xDhcpServerAuthorization.schema.mof b/deployment/dsc/azshcihost/xDhcpServer/2.0.0.0/DSCResources/MSFT_xDhcpServerAuthorization/MSFT_xDhcpServerAuthorization.schema.mof new file mode 100644 index 0000000..61b7b64 --- /dev/null +++ b/deployment/dsc/azshcihost/xDhcpServer/2.0.0.0/DSCResources/MSFT_xDhcpServerAuthorization/MSFT_xDhcpServerAuthorization.schema.mof @@ -0,0 +1,7 @@ +[ClassVersion("1.0.0.0"), FriendlyName("xDhcpServerAuthorization")] +class MSFT_xDhcpServerAuthorization : OMI_BaseResource +{ + [Write, Description("DHCP Server FQDN")] String DnsName; + [Write, Description("DHCP Server IP Address")] String IPAddress; + [Key, Description("Whether the DHCP server should be authorised within Active Directory"), ValueMap{"Present","Absent"}, Values{"Present","Absent"}] String Ensure; +}; diff --git a/deployment/dsc/azshcihost/xDhcpServer/2.0.0.0/DSCResources/MSFT_xDhcpServerClass/MSFT_xDhcpServerClass.psm1 b/deployment/dsc/azshcihost/xDhcpServer/2.0.0.0/DSCResources/MSFT_xDhcpServerClass/MSFT_xDhcpServerClass.psm1 new file mode 100644 index 0000000..2f7badf --- /dev/null +++ b/deployment/dsc/azshcihost/xDhcpServer/2.0.0.0/DSCResources/MSFT_xDhcpServerClass/MSFT_xDhcpServerClass.psm1 @@ -0,0 +1,192 @@ +Import-Module $PSScriptRoot\..\Helper.psm1 -Verbose:$false + +# Localized messages +data LocalizedData +{ + # culture="en-US" + ConvertFrom-StringData @' + SettingClassIDMessage = Setting DHCP Server Class {0} + AddingClassIDMessage = Adding DHCP Server Class {0} + RemovingClassIDMessage = Removing DHCP Server Class {0} +'@ +} + +function Get-TargetResource +{ + [CmdletBinding()] + [OutputType([System.Collections.Hashtable])] + param + ( + [Parameter(Mandatory)][ValidateSet('Present','Absent')] + [System.String] $Ensure, + + [parameter(Mandatory)] [ValidateNotNullOrEmpty()] + [String]$Name, + + [parameter(Mandatory)][ValidateSet('Vendor','User')] + [String]$Type, + + [parameter(Mandatory)][ValidateNotNullOrEmpty()] + [string] $AsciiData, + + [AllowEmptyString()] + [string]$Description = '', + + [parameter(Mandatory)][ValidateSet('IPv4')] + [String]$AddressFamily + ) + +#region Input Validation + + # Check for DhcpServer module/role + Assert-Module -moduleName DHCPServer + +#endregion Input Validation + + $DhcpServerClass = Get-DhcpServerv4Class -Name $Name -ErrorAction SilentlyContinue + + if ($DhcpServerClass) + { + $HashTable = @{ + 'Name'=$DhcpServerClass.Name + 'Type'=$DhcpServerClass.Type + 'AsciiData' = $DhcpServerClass.AsciiData + 'Description' = $DhcpServerClass.Description + 'AddressFamily' = 'IPv4' + } + } + else + { + $HashTable = @{ + 'Name' = '' + 'Type' = '' + 'AsciiData' = '' + 'Description' = '' + 'AddressFamily' = '' + } + } + $HashTable +} + +function Set-TargetResource +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory)][ValidateSet('Present','Absent')] + [System.String] $Ensure, + + [parameter(Mandatory)] [ValidateNotNullOrEmpty()] + [String]$Name, + + [parameter(Mandatory)][ValidateSet('Vendor','User')] + [String]$Type, + + [parameter(Mandatory)][ValidateNotNullOrEmpty()] + [string] $AsciiData, + + [AllowEmptyString()] + [string]$Description = '', + + [parameter(Mandatory)][ValidateSet('IPv4')] + [String]$AddressFamily + ) + + $DhcpServerClass = Get-DhcpServerv4Class $Name -ErrorAction SilentlyContinue + + #testing for ensure = present + if ($Ensure -eq 'Present') + { + #testing if class exists + if ($DhcpServerClass) + { + #if it exists we use the set verb + $scopeIDMessage = $($LocalizedData.SettingClassIDMessage) -f $Name + Write-Verbose -Message $scopeIDMessage + set-DhcpServerv4Class -Name $Name -Type $Type -Data $AsciiData -Description $Description + } + + #class not exists + else + { + $scopeIDMessage = $($LocalizedData.AddingClassIDMessage) -f $Name + Write-Verbose -Message $scopeIDMessage + Add-DhcpServerv4Class -Name $Name -Type $Type -Data $AsciiData -Description $Description + } + } + + #ensure = absent + else + { + $scopeIDMessage = $($LocalizedData.RemovingClassIDMessage) -f $Name + Write-Verbose -Message $scopeIDMessage + Remove-DhcpServerv4Class -Name $Name -Type $Type + } +} +function Test-TargetResource +{ + [CmdletBinding()] + [OutputType([System.Boolean])] + param + ( + [Parameter(Mandatory)][ValidateSet('Present','Absent')] + [System.String] $Ensure, + + [parameter(Mandatory)] [ValidateNotNullOrEmpty()] + [String]$Name, + + [parameter(Mandatory)][ValidateSet('Vendor','User')] + [String]$Type, + + [parameter(Mandatory)][ValidateNotNullOrEmpty()] + [string] $AsciiData, + + [AllowEmptyString()] + [string]$Description = '', + + [parameter(Mandatory)][ValidateSet('IPv4')] + [String]$AddressFamily + ) + + $DhcpServerClass = Get-DhcpServerv4Class -Name $Name -ErrorAction SilentlyContinue + + #testing for ensure = present + if ($Ensure -eq 'Present') + { + #testing if $DhcpServerClass is not null + if ($DhcpServerClass) + { + #since $DhcpServerClass is not null compare the values + if (($DhcpServerClass.Type -eq $Type) -and ($DhcpServerClass.asciiData -eq $AsciiData) -and ($DhcpServerClass.Description -eq $Description)) + { + $result = $true + } + + else + { + $result = $false + } + } + #if $DhcpServerClass return false + else + { + $result = $false + } + } + + #ensure = absent + else + { + #testing if $DhcpServerClass is not null, if it exists return false + if ($DhcpServerClass) + { + $result = $false + } + #if it not exists return true + else + { + $result = $true + } + } + $result +} diff --git a/deployment/dsc/azshcihost/xDhcpServer/2.0.0.0/DSCResources/MSFT_xDhcpServerClass/MSFT_xDhcpServerClass.schema.mof b/deployment/dsc/azshcihost/xDhcpServer/2.0.0.0/DSCResources/MSFT_xDhcpServerClass/MSFT_xDhcpServerClass.schema.mof new file mode 100644 index 0000000..f36df4c --- /dev/null +++ b/deployment/dsc/azshcihost/xDhcpServer/2.0.0.0/DSCResources/MSFT_xDhcpServerClass/MSFT_xDhcpServerClass.schema.mof @@ -0,0 +1,10 @@ +[ClassVersion("1.0.0.0"), FriendlyName("xDhcpServerClass")] +class MSFT_xDhcpServerClass : OMI_BaseResource +{ + [key, Description("Class Name")] String Name; + [key, Description("Class Type, Vendor or User"), ValueMap{"Vendor","User"}, Values{"Vendor","User"}] String Type; + [key, Description("Class Data, in ASCII format")] String AsciiData; + [Write, Description("Class Description")] String Description; + [key, Description("Class address family. Currently needs to be IPv4"), ValueMap{"IPv4"}, Values{"IPv4"}] String AddressFamily; + [Key, Description("Whether the DHCP server Class should exist"), ValueMap{"Present","Absent"}, Values{"Present","Absent"}] String Ensure; +}; diff --git a/deployment/dsc/azshcihost/xDhcpServer/2.0.0.0/DSCResources/MSFT_xDhcpServerOption/MSFT_xDhcpServerOption.psm1 b/deployment/dsc/azshcihost/xDhcpServer/2.0.0.0/DSCResources/MSFT_xDhcpServerOption/MSFT_xDhcpServerOption.psm1 new file mode 100644 index 0000000..34e033e --- /dev/null +++ b/deployment/dsc/azshcihost/xDhcpServer/2.0.0.0/DSCResources/MSFT_xDhcpServerOption/MSFT_xDhcpServerOption.psm1 @@ -0,0 +1,428 @@ +Import-Module $PSScriptRoot\..\Helper.psm1 -Verbose:$false + +# Localized messages +data LocalizedData +{ + # culture="en-US" + ConvertFrom-StringData @' +InvalidScopeIDMessage = DHCP server scopeID {0} is not valid. Supply a valid scopeID and try again +CheckScopeIDMessage = Checking DHCP server options for scopeID {0} ... +AddingScopeIDMessage = Adding DHCP server options for scopeID {0} ... +SetScopeIDMessage = DHCP server options is set for scopeID {0}. +FoundScopeIDMessage = Found DHCP server options for scopeID {0} and they should be {1} +NotFoundScopeIDMessage = Can not find DHCP server options for scopeID {0} and they should be {1} +RemovingScopeOptions = Removing DHCP Server options for scopeID {0}... +ScopeOptionsRemoved = DHCP Server options are removed. + +CheckPropertyMessage = Checking {0} option ... +NotDesiredPropertyMessage = {0} is not correct. Expected {1}, actual {2} +DesiredPropertyMessage = {0} option is correct. + +SettingPropertyMessage = Setting {0} option ... +SetPropertyMessage = {0} option is set to {1}. +'@ +} + +function Get-TargetResource +{ + [CmdletBinding()] + [OutputType([System.Collections.Hashtable])] + param + ( + [parameter(Mandatory = $true)] + [String]$ScopeID, + + [Parameter()] [ValidateNotNullOrEmpty()] + [String[]]$DnsServerIPAddress, + + [Parameter()] [ValidateSet('IPv4')] + [String]$AddressFamily = 'IPv4' + ) + +#region Input Validation + + # Check for DhcpServer module/role + Assert-Module -moduleName DHCPServer + + # Convert the ScopeID to be a valid IPAddress + $ScopeID = (Get-ValidIpAddress -ipString $ScopeID -AddressFamily $AddressFamily -parameterName 'ScopeID').ToString() + + # Test if the ScopeID is valid + $null = Get-DhcpServerv4Scope -ScopeId $ScopeID -ErrorAction SilentlyContinue -ErrorVariable err + if($err) + { + $errorMsg = $($LocalizedData.InvalidScopeIdMessage) -f $ScopeID + New-TerminatingError -errorId ScopeIdNotFound -errorMessage $errorMsg -errorCategory InvalidOperation + } + +#endregion Input Validation + + $ensure = 'Absent' + try + { + $dhcpOption = Get-DhcpServerv4OptionValue -ScopeID $ScopeID + if($dhcpOption) + { + $dnsDomain = (($dhcpOption | Where-Object Name -like 'DNS Domain Name').value)[0] + $ensure = 'Present' + $dnsServerIP = ($dhcpOption | Where-Object Name -like 'DNS Servers').value + $Router = ($dhcpOption | Where-Object OptionId -Like 3).value + } + } + catch + { + } + + @{ + ScopeID = $ScopeID + DnsDomain = $dnsDomain + AddressFamily = 'IPv4' + Ensure = $ensure + DnsServerIPAddress = $dnsServerIP + Router = $Router + } +} + +function Set-TargetResource +{ + [CmdletBinding()] + param + ( + [parameter(Mandatory = $true)] + [String]$ScopeID, + + [Parameter()] [ValidateNotNullOrEmpty()] + [String[]]$DnsServerIPAddress, + + [Parameter()] [ValidateNotNullOrEmpty()] + [String[]]$Router, + + [Parameter()] [ValidateNotNullOrEmpty()] + [String]$DnsDomain, + + [ValidateSet('IPv4')] + [String]$AddressFamily = 'IPv4', + + [ValidateSet('Present','Absent')] + [String]$Ensure = 'Present' + ) + +#region Input Validation + + # Array of valid IP Address + [String[]]$validDnSServer = @() + + # Convert the ScopeID to be a valid IPAddress + $ScopeID = (Get-ValidIpAddress -ipString $ScopeID -AddressFamily $AddressFamily -parameterName 'ScopeID').ToString() + + # Convert the input to be valid IPAddress + foreach ($dnsServerIp in $DnsServerIPAddress) + { + $validDnSServer += (Get-ValidIpAddress -ipString $dnsServerIp -AddressFamily $AddressFamily -parameterName 'DnsServerIPAddress').ToString() + } + $DnsServerIPAddress = $validDnSServer + + # Array of valid IP Address + [String[]]$validRouter = @() + + # Convert the input to be valid IPAddress + foreach ($routerIp in $Router) + { + $validRouter += (Get-ValidIpAddress -ipString $routerIp -AddressFamily $AddressFamily -parameterName 'Router').ToString() + } + $Router = $validRouter + +#endregion Input Validation + + # Remove $AddressFamily and $debug from PSBoundParameters and pass it to validate-properties helper function + If($PSBoundParameters['Debug']) {$null = $PSBoundParameters.Remove('Debug')} + If($PSBoundParameters['AddressFamily']){$null = $PSBoundParameters.Remove('AddressFamily')} + + ValidateResourceProperties @PSBoundParameters -Apply +} + +function Test-TargetResource +{ + [CmdletBinding()] + [OutputType([System.Boolean])] + param + ( + [parameter(Mandatory = $true)] + [String]$ScopeID, + + [Parameter()] [ValidateNotNullOrEmpty()] + [String[]]$DnsServerIPAddress, + + [Parameter()] [ValidateNotNullOrEmpty()] + [String[]]$Router, + + [Parameter()] [ValidateNotNullOrEmpty()] + [String]$DnsDomain, + + [ValidateSet('IPv4')] + [String]$AddressFamily = 'IPv4', + + [ValidateSet('Present','Absent')] + [String]$Ensure = 'Present' + ) + +#region Input Validation + + # Array of valid IP Address + [String[]]$validDnSServer = @() + + # Check for DhcpServer module/role + Assert-Module -moduleName DHCPServer + + # Convert the ScopeID to be a valid IPAddress + $ScopeID = (Get-ValidIpAddress -ipString $ScopeID -AddressFamily $AddressFamily -parameterName 'ScopeID').ToString() + + # Array of valid IP Address + [String[]]$validDnSServer = @() + + # Convert the input to be valid IPAddress + foreach ($dnsServerIp in $DnsServerIPAddress) + { + $validDnSServer += (Get-ValidIpAddress -ipString $dnsServerIp -AddressFamily $AddressFamily -parameterName 'DnsServerIPAddress').ToString() + } + $DnsServerIPAddress = $validDnSServer + + # Array of valid IP Address + [String[]]$validRouter = @() + + # Convert the input to be valid IPAddress + foreach ($routerIp in $Router) + { + $validRouter += (Get-ValidIpAddress -ipString $routerIp -AddressFamily $AddressFamily -parameterName 'Router').ToString() + } + $Router = $validRouter + + # Test if the ScopeID is valid + $null = Get-DhcpServerv4Scope -ScopeId $ScopeID -ErrorAction SilentlyContinue -ErrorVariable err + if($err) + { + $errorMsg = $($LocalizedData.InvalidScopeIdMessage) -f $ScopeID + New-TerminatingError -errorId ScopeIdNotFound -errorMessage $errorMsg -errorCategory InvalidOperation + } + +#endregion Input Validation + + # Remove $AddressFamily and $debug from PSBoundParameters and pass it to validateProperties helper function + If($PSBoundParameters['Debug']) {$null = $PSBoundParameters.Remove('Debug')} + If($PSBoundParameters['AddressFamily']){$null = $PSBoundParameters.Remove('AddressFamily')} + + ValidateResourceProperties @PSBoundParameters +} + +#region Helper function + +# Internal function to validate dhcpOptions properties +function ValidateResourceProperties +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory)] + [string]$ScopeID, + + [Parameter()] [ValidateNotNullOrEmpty()] + [String[]]$DnsServerIPAddress, + + [Parameter()] [ValidateNotNullOrEmpty()] + [String]$DnsDomain, + + [Parameter()] [ValidateNotNullOrEmpty()] + [String[]]$Router, + + [ValidateSet('Present','Absent')] + [String]$Ensure = 'Present', + + [switch]$Apply + ) + + $scopeIDMessage = $($LocalizedData.CheckScopeIDMessage) -f $ScopeID + Write-Verbose -Message $scopeIDMessage + + $dhcpOption = Get-DhcpServerv4OptionValue -ScopeID $ScopeID + + # Found DHCPOption + if($dhcpOption) + { + $foundScopeIdMessage = $($LocalizedData.FoundScopeIDMessage) -f $ScopeID, $Ensure + Write-Verbose -Message $foundScopeIdMessage + + # If Options should be present, check other properties + if($Ensure -eq 'Present') + { + + if($PSBoundParameters.ContainsKey('DnsServerIPAddress')) + { + # Test the DNS Server IPs + $checkPropertyMessage = $($LocalizedData.CheckPropertyMessage) -f 'Dns server ip' + Write-Verbose -Message $checkPropertyMessage + + $dnsServerIP = ($DhcpOption | Where-Object OptionId -eq 6).Value + # If comparison return something, they are not equal + if((-not $dnsServerIP) -or (Compare-Object $dnsServerIP $DnsServerIPAddress)) + { + $notDesiredPropertyMessage = $($LocalizedData.NotDesiredPropertyMessage) -f 'DNS server ip', ($DnsServerIPAddress -join ', '), ($dnsServerIP -join ', ') + Write-Verbose -Message $notDesiredPropertyMessage + if($Apply) + { + $settingPropertyMessage = $($LocalizedData.SettingPropertyMessage) -f 'DNS server ip' + Write-Verbose -Message $settingPropertyMessage + + Set-DhcpServerv4OptionValue -ScopeId $ScopeID -DnsServer $DnsServerIPAddress -Force + + $setPropertyMessage = $($LocalizedData.SetPropertyMessage) -f 'DNS server ip', ($DnsServerIPAddress -join ', ') + Write-Verbose -Message $setPropertyMessage + + } # end $Apply + else + { + return $false + } + } # end Compare-object + else + { + $desiredPropertyMessage = $($LocalizedData.DesiredPropertyMessage) -f 'DNS server ip' + Write-Verbose -Message $desiredPropertyMessage + } + } + + # If DNS Domain is specified, test that + if($PSBoundParameters.ContainsKey('DnsDomain')) + { + $checkPropertyMessage = $($LocalizedData.CheckPropertyMessage) -f 'Dns domain name' + Write-Verbose -Message $checkPropertyMessage + + $dnsDomainName = ($DhcpOption | Where-Object OptionId -eq 15).Value + if($dnsDomainName -ne $DnsDomain) + { + $notDesiredPropertyMessage = $($LocalizedData.NotDesiredPropertyMessage) -f 'DNS domain name', $DnsDomain, ($dnsDomainName -join ', ') + Write-Verbose -Message $notDesiredPropertyMessage + + if($Apply) + { + $settingPropertyMessage = $($LocalizedData.SettingPropertyMessage) -f 'DNS domain name' + Write-Verbose -Message $settingPropertyMessage + + Set-DhcpServerv4OptionValue -ScopeId $ScopeID -DnsDomain $DnsDomain + + $setPropertyMessage = $($LocalizedData.SetPropertyMessage) -f 'DNS domain name', ($DnsDomain -join ', ') + Write-Verbose -Message $setPropertyMessage + } # end $Apply + else + { + return $false + } + } # end $dnsDomainName -ne $DnsDomain + else + { + $desiredPropertyMessage = $($LocalizedData.DesiredPropertyMessage) -f 'DNS domain name' + Write-Verbose -Message $desiredPropertyMessage + } + } # end $PSBoundParameters.ContainsKey('DnsDomain') + + # If Router is specified, test that + if($PSBoundParameters.ContainsKey('Router')) + { + $propertyName = 'Router ip addresses' + $checkPropertyMessage = $($LocalizedData.CheckPropertyMessage) -f 'Router ip addresses' + Write-Verbose -Message $checkPropertyMessage + + $routerIP = ($DhcpOption | Where-Object OptionId -eq 3).Value + + if((-not $routerIP) -or (Compare-Object $routerIP $Router)) + { + $notDesiredPropertyMessage = $($LocalizedData.NotDesiredPropertyMessage) -f $propertyName, ($Router -join ', '), ($routerIP -join ', ') + Write-Verbose -Message $notDesiredPropertyMessage + + if($Apply) + { + $settingPropertyMessage = $($LocalizedData.SettingPropertyMessage) -f $propertyName + Write-Verbose -Message $settingPropertyMessage + + Set-DhcpServerv4OptionValue -ScopeId $ScopeID -Router $Router + + $setPropertyMessage = $($LocalizedData.SetPropertyMessage) -f $propertyName, ($Router -join ', ') + Write-Verbose -Message $setPropertyMessage + } # end $Apply + else + { + return $false + } + } # end $routerIP -ne $Router + else + { + $desiredPropertyMessage = $($LocalizedData.DesiredPropertyMessage) -f $propertyName + Write-Verbose -Message $desiredPropertyMessage + } + } # end $PSBoundParameters.ContainsKey('Router') + + if(-not $Apply) + { + return $true + } + } # end $Ensure -eq 'Present' + + # If Options should be absent, return False or remove it + else + { + if($Apply) + { + Write-Verbose -Message ($LocalizedData.RemovingScopeOptions -f $ScopeID) + foreach($option in $dhcpOption.OptionID) + { + Remove-DhcpServerv4OptionValue -ScopeId $ScopeID -OptionId $option + } + Write-Verbose -Message ($LocalizedData.ScopeOptionsRemoved) + } # end if $Apply + else {return $false} + } + } + else + { + $notFoundScopeIdMessage = $($LocalizedData.NotFoundScopeIDMessage) -f $ScopeID, $Ensure + Write-Verbose -Message $notFoundScopeIdMessage + + if($Apply) + { + # If Options should be present, create those + if($Ensure -eq 'Present') + { + $addingScopeIdMessage = $($LocalizedData.AddingScopeIDMessage) -f $ScopeID + Write-Verbose -Message $addingScopeIdMessage + + $parameters = @{ScopeID = $ScopeID;} + + ## If DnsServer(s) specified, pass it + if ($PSBoundParameters.ContainsKey('DnsServerIPAddress')) + { + $parameters['DnsServer'] = $DnsServerIPAddress + } + + # If Dns domain is specified pass it + if($PSBoundParameters.ContainsKey('DnsDomain')) + { + $parameters['DnsDomain'] = $DnsDomain + } + + Set-DhcpServerv4OptionValue @parameters -Force + + $setScopeIdMessage = $($LocalizedData.SetScopeIDMessage) -f $ScopeID + Write-Verbose -Message $setScopeIdMessage + } # end Ensure -eq 'Present + } # end if $Apply + else + { + # If Options should be present, return false else true + return ($Ensure -eq 'Absent') + } + } +} +#endregion Helper function +if($global:DhpcOptionTest -ne $true) +{ + Export-ModuleMember -Function *-TargetResource +} diff --git a/deployment/dsc/azshcihost/xDhcpServer/2.0.0.0/DSCResources/MSFT_xDhcpServerOption/MSFT_xDhcpServerOption.schema.mof b/deployment/dsc/azshcihost/xDhcpServer/2.0.0.0/DSCResources/MSFT_xDhcpServerOption/MSFT_xDhcpServerOption.schema.mof new file mode 100644 index 0000000..90f3627 --- /dev/null +++ b/deployment/dsc/azshcihost/xDhcpServer/2.0.0.0/DSCResources/MSFT_xDhcpServerOption/MSFT_xDhcpServerOption.schema.mof @@ -0,0 +1,11 @@ +[ClassVersion("1.0.0.0"), FriendlyName("xDhcpServerOption")] +class MSFT_xDhcpServerOption : OMI_BaseResource +{ + [Key, Description("ScopeId for which options are set")] String ScopeID; + [Write, Description("IP address of DNS Servers")] String DnsServerIPAddress[]; + [Write, Description("Domain name of DNS Server")] String DnsDomain; + [Write, Description("IP address of the router/default gateway.")] String Router[]; + [Write, Description("Address family type"), ValueMap{"IPv4"}, Values{"IPv4"}] String AddressFamily; + [Write, Description("Whether option should be set or removed"), ValueMap{"Present","Absent"}, Values{"Present","Absent"}] String Ensure; +}; + diff --git a/deployment/dsc/azshcihost/xDhcpServer/2.0.0.0/DSCResources/MSFT_xDhcpServerOptionDefinition/MSFT_xDhcpServerOptionDefinition.psm1 b/deployment/dsc/azshcihost/xDhcpServer/2.0.0.0/DSCResources/MSFT_xDhcpServerOptionDefinition/MSFT_xDhcpServerOptionDefinition.psm1 new file mode 100644 index 0000000..4d1d6bf --- /dev/null +++ b/deployment/dsc/azshcihost/xDhcpServer/2.0.0.0/DSCResources/MSFT_xDhcpServerOptionDefinition/MSFT_xDhcpServerOptionDefinition.psm1 @@ -0,0 +1,396 @@ +Import-Module $PSScriptRoot\..\Helper.psm1 -Verbose:$false + +# Localized messages +data LocalizedData +{ + # Culture="en-US" + ConvertFrom-StringData @' + GettingOptionDefinitionIDMessage = Getting DHCP server option definition "{0}" with vendor class "{1}". + TestingOptionDefinitionIDMessage = Begin testing DHCP server option definition "{0}" with vendor class "{1}". + RemovingOptionDefinitionIDMessage = Removing DHCP server option definition "{0}" with vendor class "{1}". + RecreatingOptionDefinitionIDMessage = Recreating DHCP server option definition "{0}" with vendor class "{1}". + AddingOptionDefinitionIDMessage = Adding DHCP server option definition "{0}" with vendor class "{1}". + SettingOptionDefinitionIDMessage = Setting DHCP server option definition "{0}" with vendor class "{1}". + FoundOptionDefinitionIDMessage = Found DHCP server option Definition "{0}" with vendor class "{1}". + NotFoundOptionDefinitionIDMessage = Cannot find DHCP server option Definition "{0}" with vendor class "{1}". + ComparingOptionDefinitionIDMessage = Comparing option definition "{0}", vendor class "{1}" with existing definition. + ExactMatchOptionDefinitionIDMessage = Matched option definition "{0}" with vendor class "{1}" with existing definition. + NotMatchOptionDefinitionIDMessage = Not matched all parameters in option definition "{0}" with vendor class "{1}", should adjust. +'@ +} + + <# + + .SYNOPSIS + This function gets a DHCP option definition. + + .PARAMETER Ensure + When set to 'Present', the option definition will be created. + When set to 'Absent', the option definition will be removed. + + .PARAMETER OptionId + The ID of the option definition. + + .PARAMETER Name + The name of the option definition. + + .PARAMETER VendorClass + The vendor class of the option definition. Use an empty string for standard class. + + .PARAMETER Type + The data type of the option definition. + + .PARAMETER AddressFamily + The option definition address family (IPv4 or IPv6). Currently only the IPv4 is supported. + +#> + +function Get-TargetResource +{ + [CmdletBinding()] + [OutputType([System.Collections.Hashtable])] + param + ( + [Parameter()] + [ValidateSet('Present','Absent')] + [ValidateNotNullOrEmpty()] + [String] + $Ensure = 'Present', + + [Parameter(Mandatory = $true)] + [Validaterange(1,255)] + [UInt32] + $OptionId, + + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [String] + $Name, + + [Parameter(Mandatory = $true)] + [ValidateSet('Byte','Word','Dword','DwordDword','IPv4Address','String','BinaryData','EncapsulatedData')] + [String] + $Type, + + [Parameter(Mandatory = $true)] + [AllowEmptyString()] + [String] + $VendorClass, + + [Parameter(Mandatory = $true)] + [ValidateSet('IPv4')] + [String] + $AddressFamily + ) + + # Region Input Validation + + # Check for DhcpServer module/role + Assert-Module -ModuleName DHCPServer + + # Endregion Input Validation + + $gettingIDMessage = $localizedData.GettingOptionDefinitionIDMessage -f $OptionId, $VendorClass + Write-Verbose -Message $gettingIDMessage + $dhcpServerOptionDefinition = Get-DhcpServerv4OptionDefinition -OptionId $OptionId -VendorClass $VendorClass -ErrorAction SilentlyContinue + + if ($dhcpServerOptionDefinition) + { + $hashTable = @{ + OptionId = $dhcpServerOptionDefinition.OptionId + Name = $dhcpServerOptionDefinition.Name + AddressFamily = $AddressFamily + Description = $dhcpServerOptionDefinition.Description + Type = $dhcpServerOptionDefinition.Type + VendorClass = $dhcpServerOptionDefinition.VendorClass + MultiValued = $dhcpServerOptionDefinition.MultiValued + Ensure = 'Present' + } + } + else + { + $hashTable = @{ + OptionId = $null + Name = $null + AddressFamily = $null + Description = $null + Type = $null + VendorClass = $null + MultiValued = $null + Ensure = 'Absent' + } + } + + $hashTable +} + +<# + + .SYNOPSIS + This function sets the state of a DHCP option definition. + + .PARAMETER Ensure + When set to 'Present', the option definition will be created. + When set to 'Absent', the option definition will be removed. + + .PARAMETER OptionId + The ID of the option definition. + + .PARAMETER Name + The name of the option definition. + + .PARAMETER Description + Description of the option definition. + + .PARAMETER VendorClass + The vendor class of the option definition. Use an empty string for standard class. + + .PARAMETER Type + The data type of the option definition. + + .PARAMETER Multivalued + Whether the option definition is multivalued or not. + + .PARAMETER AddressFamily + The option definition address family (IPv4 or IPv6). Currently only the IPv4 is supported. + +#> + +function Set-TargetResource +{ + [CmdletBinding()] + param + ( + [Parameter()] + [ValidateSet('Present','Absent')] + [ValidateNotNullOrEmpty()] + [String] + $Ensure = 'Present', + + [Parameter(Mandatory = $true)] + [Validaterange(1,255)] + [UInt32] + $OptionId, + + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [String] + $Name, + + [Parameter()] + [AllowEmptyString()] + [String] + $Description, + + [Parameter(Mandatory = $true)] + [AllowEmptyString()] + [String] + $VendorClass, + + [Parameter(Mandatory = $true)] + [ValidateSet('Byte','Word','Dword','DwordDword','IPv4Address','String','BinaryData','EncapsulatedData')] + [String] + $Type, + + [Parameter()] + [Boolean] + $MultiValued, + + [Parameter(Mandatory = $true)] + [ValidateSet('IPv4')] + [String] + $AddressFamily + ) + + # Reading the DHCP option + $dhcpServerOptionDefinition = Get-TargetResource -OptionId $OptionId -Name $Name -VendorClass $VendorClass -Type $Type -AddressFamily $AddressFamily -ErrorAction SilentlyContinue + + # Testing for present + if ($Ensure -eq 'Present') + { + # Testing if option exists + if ($dhcpServerOptionDefinition.Ensure -eq 'Present') + { + # If it exists and any of multivalued, type or vendorclass is being changed remove then re-add the whole option definition + if (($dhcpServerOptionDefinition.Type -ne $Type) -or ($dhcpServerOptionDefinition.MultiValued -ne $MultiValued) -or ($dhcpServerOptionDefinition.VendorClass -ne $VendorClass)) + { + $scopeIDMessage = $localizedData.RecreatingOptionDefinitionIDMessage -f $OptionId, $VendorClass + Write-Verbose -Message $scopeIDMessage + Remove-DhcpServerv4OptionDefinition -OptionId $OptionId -VendorClass $VendorClass + Add-DhcpServerv4OptionDefinition -OptionId $OptionId -name $Name -Type $Type -Description $Description -MultiValued:$MultiValued -VendorClass $VendorClass + } + # If option exists we need only to adjust the parameters + else + { + $settingIDMessage = $localizedData.SettingOptionDefinitionIDMessage -f $OptionId, $VendorClass + Write-Verbose -Message $settingIDMessage + Set-DhcpServerv4OptionDefinition -OptionId $OptionId -VendorClass $VendorClass -name $Name -Description $Description + } + } + # If option does not exist we need to add it + else + { + $scopeIDMessage = $localizedData.AddingOptionDefinitionIDMessage -f $OptionId, $VendorClass + Write-Verbose -Message $scopeIDMessage + Add-DhcpServerv4OptionDefinition -OptionId $OptionId -name $Name -Type $Type -Description $Description -MultiValued:$MultiValued -VendorClass $VendorClass + } + } + # Testing for 'absent' + else + { + if ($dhcpServerOptionDefinition) + { + $scopeIDMessage = $localizedData.RemovingOptionDefinitionIDMessage -f $OptionId,$VendorClass + Write-Verbose -Message $scopeIDMessage + Remove-DhcpServerv4OptionDefinition -OptionId $OptionId -VendorClass $VendorClass + } + } +} + +<# + + .SYNOPSIS + This function tests if the DHCP option definition is created. + + .PARAMETER Ensure + When set to 'Present', the option definition will be created. + When set to 'Absent', the option definition will be removed. + + .PARAMETER OptionId + The ID of the option definition. + + .PARAMETER Name + The name of the option definition. + + .PARAMETER Description + Description of the option definition. + + .PARAMETER VendorClass + The vendor class of the option definition. Use an empty string for standard class. + + .PARAMETER Type + The data type of the option definition. + + .PARAMETER Multivalued + Whether the option definition is multivalued or not. + + .PARAMETER AddressFamily + The option definition address family (IPv4 or IPv6). Currently only the IPv4 is supported. + +#> + +function Test-TargetResource +{ + [CmdletBinding()] + [OutputType([System.Boolean])] + param + ( + [Parameter()] + [ValidateSet('Present','Absent')] + [ValidateNotNullOrEmpty()] + [String] + $Ensure = 'Present', + + [Parameter(Mandatory = $true)] + [Validaterange(1,255)] + [UInt32] $OptionId, + + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [String] + $Name, + + [Parameter()] + [AllowEmptyString()] + [String] + $Description, + + [Parameter(Mandatory = $true)] + [AllowEmptyString()] + [String] + $VendorClass, + + [Parameter(Mandatory = $true)] + [ValidateSet('Byte','Word','Dword','DwordDword','IPv4Address','String','BinaryData','EncapsulatedData')] + [String] + $Type, + + [Parameter()] + [Boolean] + $MultiValued, + + [Parameter(Mandatory = $true)] + [ValidateSet('IPv4')] + [String] + $AddressFamily + ) + + # Region Input Validation + + # Check for DhcpServer module/role + Assert-Module -ModuleName DHCPServer + # Endregion Input Validation + + $testingIDMessage = $localizedData.TestingOptionDefinitionIDMessage -f $OptionId, $VendorClass + # Geting the dhcp option definition + Write-Verbose -Message $testingIDMessage + + $currentConfiguration = Get-TargetResource -OptionId $OptionId -Name $Name -VendorClass $VendorClass -Type $Type -AddressFamily $AddressFamily -ErrorAction SilentlyContinue + + if ($currentConfiguration.Ensure -eq 'Present') + { + $foundIDMessage = $localizedData.FoundOptionDefinitionIDMessage -f $OptionId, $VendorClass + Write-Verbose $foundIDMessage + } + else + { + $notFoundIDMessage = $localizedData.NotFoundOptionDefinitionIDMessage -f $OptionId, $VendorClass + Write-Verbose $notFoundIDMessage + } + + + # Testing for Ensure = Present + if ($Ensure -eq 'Present') + { + # Testing if $OptionId and VendorClass already exist + if ($currentConfiguration.Ensure -eq 'Present') + { + $comparingIDMessage = $localizedData.ComparingOptionDefinitionIDMessage -f $OptionId, $VendorClass + Write-Verbose $comparingIDMessage + + # Since $OptionId and $VendorClass exist compare all the Values + if (($currentConfiguration.OptionId -eq $OptionId) -and ($currentConfiguration.Name -eq $Name) -and ($currentConfiguration.Description -eq $Description) -and ($currentConfiguration.VendorClass -eq $VendorClass) -and ($currentConfiguration.Type -eq $Type) -and ($currentConfiguration.MultiValued -eq $MultiValued)) + { + $exactMatchIDMessage = $localizedData.ExactMatchOptionDefinitionIDMessage -f $OptionId, $VendorClass + Write-Verbose $exactMatchIDMessage + $result = $true + } + else + { + $notMatchIDMessage = $localizedData.NotMatchOptionDefinitionIDMessage -f $OptionId, $VendorClass + Write-Verbose $notMatchIDMessage + $result = $false + } + } + else + { + # Since $OptionId and $VendorClass do not exist return $false + $result = $false + } + } + # If Ensure = Absent + else + { + if ($currentConfiguration.Ensure -eq 'Present') + { + # Since desired state is 'Absent' and $OptionId and $VendorClass exist return $false + $result = $false + } + else + { + # Since desired state is 'Absent' and $OptionId and $VendorClass do not exist return $true + $result = $true + } + } +$result +} diff --git a/deployment/dsc/azshcihost/xDhcpServer/2.0.0.0/DSCResources/MSFT_xDhcpServerOptionDefinition/MSFT_xDhcpServerOptionDefinition.schema.mof b/deployment/dsc/azshcihost/xDhcpServer/2.0.0.0/DSCResources/MSFT_xDhcpServerOptionDefinition/MSFT_xDhcpServerOptionDefinition.schema.mof new file mode 100644 index 0000000..32309d7 --- /dev/null +++ b/deployment/dsc/azshcihost/xDhcpServer/2.0.0.0/DSCResources/MSFT_xDhcpServerOptionDefinition/MSFT_xDhcpServerOptionDefinition.schema.mof @@ -0,0 +1,12 @@ +[ClassVersion("1.0.0.0"), FriendlyName("xDhcpServerOptionDefinition")] +class MSFT_xDhcpServerOptionDefinition : OMI_BaseResource +{ + [Key, Description("Option ID, specify a number between 1 and 255.")] uint32 OptionId; + [Key, Description("Vendor class. Use an empty string for standard option class.")] String VendorClass; + [Required, Description("Option name.")] String Name; + [Required, Description("Option data type."),ValueMap{"Byte","Word","Dword","DwordDword","IPv4Address","String","BinaryData","EncapsulatedData"},Values{"Byte","Word","Dword","DwordDword","IPv4Address","String","BinaryData","EncapsulatedData"}] string Type; + [Write, Description("Whether option is multivalued or not.")] Boolean Multivalued; + [Write, Description("Option description.")] String Description; + [Key, Description("Class address family. Currently needs to be IPv4."), ValueMap{"IPv4"}, Values{"IPv4"}] String AddressFamily; + [Write, Description("Whether the DHCP server class should exist."), ValueMap{"Present","Absent"}, Values{"Present","Absent"}] String Ensure; +}; diff --git a/deployment/dsc/azshcihost/xDhcpServer/2.0.0.0/DSCResources/MSFT_xDhcpServerReservation/MSFT_xDhcpServerReservation.psm1 b/deployment/dsc/azshcihost/xDhcpServer/2.0.0.0/DSCResources/MSFT_xDhcpServerReservation/MSFT_xDhcpServerReservation.psm1 new file mode 100644 index 0000000..640c7b5 --- /dev/null +++ b/deployment/dsc/azshcihost/xDhcpServer/2.0.0.0/DSCResources/MSFT_xDhcpServerReservation/MSFT_xDhcpServerReservation.psm1 @@ -0,0 +1,354 @@ +Import-Module $PSScriptRoot\..\Helper.psm1 -Verbose:$false + +# Localized messages +data LocalizedData +{ + # culture="en-US" + ConvertFrom-StringData @' +InvalidScopeIDMessage = DHCP server scopeID {0} is not valid. Supply a valid scopeID and try again +CheckingReservationMessage = Checking DHCP server reservation in scope id {0} for IP address {1} ... +TestReservationMessage = DHCP server reservation in the given scope id for the IP address is {0} and it should be {1} +RemovingReservationMessage = Removing DHCP server reservation from scope id {0} for MAC address {1} ... +DeleteReservationMessage = DHCP server reservation for the given MAC address is now absent +AddingReservationMessage = Adding DHCP server reservation with the given IP address ... +SetReservationMessage = DHCP server reservation in the given scope id for the IP address {0} is now present + +CheckPropertyMessage = Checking DHCP server reservation {0} for the given ipaddress ... +NotDesiredPropertyMessage = DHCP server reservation for the given ipaddress doesn't have correct {0}. Expected {1}, actual {2} +DesiredPropertyMessage = DHCP server reservation {0} for the given ipaddress is correct. +SetPropertyMessage = DHCP server reservation {0} for the given ipaddress is set. +'@ +} + +function Get-TargetResource +{ + [CmdletBinding()] + [OutputType([System.Collections.Hashtable])] + param + ( + [parameter(Mandatory)] + [String]$ScopeID, + + [parameter(Mandatory)] + [String]$ClientMACAddress, + + [parameter(Mandatory)] + [String]$IPAddress, + + [ValidateSet("IPv4")] + [String]$AddressFamily = 'IPv4' + ) + +#region input validation + # Check for DhcpServer module/role + Assert-Module -moduleName DHCPServer + + # Convert the ScopeID to be a valid IPAddress + $ScopeID = (Get-ValidIpAddress -ipString $ScopeID -AddressFamily $AddressFamily -parameterName 'ScopeID').ToString() + + # Test if the ScopeID is valid + $null = Get-DhcpServerv4Scope -ScopeId $ScopeID -ErrorAction SilentlyContinue -ErrorVariable err + if($err) + { + $errorMsg = $($LocalizedData.InvalidScopeIdMessage) -f $ScopeID + New-TerminatingError -errorId ScopeIdNotFound -errorMessage $errorMsg -errorCategory InvalidOperation + } + + # Convert the Start Range to be a valid IPAddress + $IPAddress = (Get-ValidIpAddress -ipString $IPAddress -AddressFamily $AddressFamily -parameterName 'IPAddress').ToString() + +#endregion input validation + + $reservation = Get-DhcpServerv4Reservation -ScopeID $ScopeID | Where-Object IPAddress -eq $IPAddress + + if($reservation) + { + $ensure = 'Present' + } + else + { + $ensure = 'Absent' + } + + @{ + ScopeID = $ScopeID + IPAddress = $IPAddress + ClientMACAddress = $reservation.ClientId + Name = $reservation.Name + AddressFamily = $AddressFamily + Ensure = $Ensure + } + +} + +function Set-TargetResource +{ + [CmdletBinding()] + param + ( + [parameter(Mandatory)] + [String]$ScopeID, + + [parameter(Mandatory)] + [String]$ClientMACAddress, + + [parameter(Mandatory)] + [String]$IPAddress, + + [String]$Name, + + [ValidateSet("IPv4")] + [String]$AddressFamily = 'IPv4', + + [ValidateSet("Present","Absent")] + [String]$Ensure = 'Present' + ) + + if($PSBoundParameters.ContainsKey('Debug')){ $null = $PSBoundParameters.Remove('Debug')} + if($PSBoundParameters.ContainsKey('AddressFamily')) {$null = $PSBoundParameters.Remove('AddressFamily')} + + Validate-ResourceProperties @PSBoundParameters -Apply +} + +function Test-TargetResource +{ + [CmdletBinding()] + [OutputType([System.Boolean])] + param + ( + [parameter(Mandatory)] + [String]$ScopeID, + + [parameter(Mandatory)] + [String]$ClientMACAddress, + + [parameter(Mandatory)] + [String]$IPAddress, + + [String]$Name, + + [ValidateSet("IPv4")] + [String]$AddressFamily = 'IPv4', + + [ValidateSet("Present","Absent")] + [String]$Ensure = 'Present' + ) + +#region input validation + # Check for DhcpServer module/role + Assert-Module -moduleName DHCPServer + + # Convert the ScopeID to be a valid IPAddress + $ScopeID = (Get-ValidIpAddress -ipString $ScopeID -AddressFamily $AddressFamily -parameterName 'ScopeID').ToString() + + # Test if the ScopeID is valid + $null = Get-DhcpServerv4Scope -ScopeId $ScopeID -ErrorAction SilentlyContinue -ErrorVariable err + if($err) + { + $errorMsg = $($LocalizedData.InvalidScopeIdMessage) -f $ScopeID + New-TerminatingError -errorId ScopeIdNotFound -errorMessage $errorMsg -errorCategory InvalidOperation + } + + # Convert the Start Range to be a valid IPAddress + $IPAddress = (Get-ValidIpAddress -ipString $IPAddress -AddressFamily $AddressFamily -parameterName 'IPAddress').ToString() + + #Convert the MAC Address into normalized form for comparison + $ClientMACAddress = $ClientMACAddress.Replace('-','') + +#endregion input validation + + if($PSBoundParameters.ContainsKey('Debug')){ $null = $PSBoundParameters.Remove('Debug')} + if($PSBoundParameters.ContainsKey('AddressFamily')) {$null = $PSBoundParameters.Remove('AddressFamily')} + + Validate-ResourceProperties @PSBoundParameters +} + +#region Helper function + +# Internal function to validate dhcpOptions properties +function Validate-ResourceProperties +{ + [CmdletBinding()] + param + ( + [parameter(Mandatory)] + [String]$ScopeID, + + [parameter(Mandatory)] + [String]$ClientMACAddress, + + [parameter(Mandatory)] + [String]$IPAddress, + + [String]$Name, + + [ValidateSet("IPv4")] + [String]$AddressFamily = 'IPv4', + + [ValidateSet("Present","Absent")] + [String]$Ensure = 'Present', + + [Switch]$Apply + ) + + $reservationMessage = $($LocalizedData.CheckingReservationMessage) -f $ScopeID, $IPAddress + Write-Verbose -Message $reservationMessage + + $reservation = Get-DhcpServerv4Reservation -ScopeID $ScopeID | Where-Object IPAddress -eq $IPAddress + + # Initialize the parameter collection + if($Apply) + { + $parameters = @{IPAddress = $IPAddress} + } + # Found DHCP reservation + if($reservation) + { + $TestReservationMessage = $($LocalizedData.TestReservationMessage) -f 'present', $Ensure + Write-Verbose -Message $TestReservationMessage + + # if it should be present, test individual properties to match parameter values + if($Ensure -eq 'Present') + { + #Convert the MAC Address into normalized form for comparison + $normalizedClientID = $reservation.ClientId.Replace('-','') + + #region Test MAC address + $checkPropertyMsg = $($LocalizedData.CheckPropertyMessage) -f 'client MAC address' + Write-Verbose -Message $checkPropertyMsg + + if($normalizedClientID -ne $ClientMACAddress) + { + $notDesiredPropertyMsg = $($LocalizedData.NotDesiredPropertyMessage) -f 'client MAC address',$ClientMACAddress,$normalizedClientID + Write-Verbose -Message $notDesiredPropertyMsg + + if($Apply) + { + $parameters['ClientID'] = $ClientMACAddress + } + else + { + return $false + } + } # end ClientID ne ClientMACAddress + else + { + $desiredPropertyMsg = $($LocalizedData.DesiredPropertyMessage) -f 'client MAC address' + Write-Verbose -Message $desiredPropertyMsg + } + #endregion Test MAC address + + #region Test reservation name + $checkPropertyMsg = $($LocalizedData.CheckPropertyMessage) -f 'name' + Write-Verbose -Message $checkPropertyMsg + + if($reservation.Name -ne $Name) + { + $notDesiredPropertyMsg = $($LocalizedData.NotDesiredPropertyMessage) -f 'name',$Name,$($reservation.Name) + Write-Verbose -Message $notDesiredPropertyMsg + + if($Apply) + { + $parameters['Name'] = $Name + } + else + { + return $false + } + } # end reservation.Name -ne Name + else + { + $desiredPropertyMsg = $($LocalizedData.DesiredPropertyMessage) -f 'name' + Write-Verbose -Message $desiredPropertyMsg + } + #endregion Test reservation name + + if($Apply) + { + # If parameters contains more than 1 key, set the DhcpServer reservation + if($parameters.Count -gt 1) + { + Set-DhcpServerv4Reservation @parameters + + Write-PropertyMessage -Parameters $parameters -keysToSkip IPAddress ` + -Message $($LocalizedData.SetPropertyMessage) -Verbose + } + } # end Apply + else + { + return $true + } + } # end ensure -eq present + + # If dhcpreservation should be absent + else + { + if($Apply) + { + $removingReservationMsg = $($LocalizedData.RemovingReservationMessage) -f $ScopeID,$ClientMACAddress + Write-Verbose -Message $removingReservationMsg + + # Remove the reservation + Remove-DhcpServerv4Reservation -ScopeId $ScopeID -ClientId $ClientMACAddress + + $deleteReservationMsg = $LocalizedData.deleteReservationMessage + Write-Verbose -Message $deleteReservationMsg + } + else + { + return $false + } + } # end ensure -eq absent + } # end found resevation + + else + { + $TestReservationMessage = $($LocalizedData.TestReservationMessage) -f 'absent', $Ensure + Write-Verbose -Message $TestReservationMessage + + if($Ensure -eq 'Present') + { + if($Apply) + { + # Add other mandatory parameters + $parameters['ScopeId'] = $ScopeID + $parameters['ClientId'] = $ClientMACAddress + + # Check if reservation name is specified, add to parameter collection + if($PSBoundParameters.ContainsKey('Name')) + { + $parameters['Name'] = $Name + } + + $addingReservationeMessage = $LocalizedData.AddingReservationMessage + Write-Verbose -Message $addingReservationeMessage + + try + { + # Create a new scope with specified properties + Add-DhcpServerv4Reservation @parameters + + $setReservationMessage = $($LocalizedData.SetReservationMessage) -f $Name + Write-Verbose -Message $setReservationMessage + } + catch + { + New-TerminatingError -errorId DhcpServerReservationFailure -errorMessage $_.Exception.Message -errorCategory InvalidOperation + } + }# end Apply + else + { + return $false + } + } # end Ensure -eq Present + else + { + return $true + } + } # end ! reservation +} + +#endregion + +Export-ModuleMember -Function *-TargetResource + diff --git a/deployment/dsc/azshcihost/xDhcpServer/2.0.0.0/DSCResources/MSFT_xDhcpServerReservation/MSFT_xDhcpServerReservation.schema.mof b/deployment/dsc/azshcihost/xDhcpServer/2.0.0.0/DSCResources/MSFT_xDhcpServerReservation/MSFT_xDhcpServerReservation.schema.mof new file mode 100644 index 0000000..c1d6c0e --- /dev/null +++ b/deployment/dsc/azshcihost/xDhcpServer/2.0.0.0/DSCResources/MSFT_xDhcpServerReservation/MSFT_xDhcpServerReservation.schema.mof @@ -0,0 +1,11 @@ +[ClassVersion("1.0.0.0"), FriendlyName("xDhcpServerReservation")] +class MSFT_xDhcpServerReservation : OMI_BaseResource +{ + [Key, Description("ScopeId for which reservations are set")] String ScopeID; + [Key, Description("IP address of the reservation for which the properties are modified")] String IPAddress; + [Required, Description("Client MAC Address to set on the reservation")] String ClientMACAddress; + [Write, Description("Reservation name")] String Name; + [Write, Description("Address family type"), ValueMap{"IPv4"}, Values{"IPv4"}] String AddressFamily; + [Write, Description("Whether option should be set or removed"), ValueMap{"Present","Absent"}, Values{"Present","Absent"}] String Ensure; +}; + diff --git a/deployment/dsc/azshcihost/xDhcpServer/2.0.0.0/DSCResources/MSFT_xDhcpServerScope/MSFT_xDhcpServerScope.psm1 b/deployment/dsc/azshcihost/xDhcpServer/2.0.0.0/DSCResources/MSFT_xDhcpServerScope/MSFT_xDhcpServerScope.psm1 new file mode 100644 index 0000000..fb3ec17 --- /dev/null +++ b/deployment/dsc/azshcihost/xDhcpServer/2.0.0.0/DSCResources/MSFT_xDhcpServerScope/MSFT_xDhcpServerScope.psm1 @@ -0,0 +1,700 @@ +Import-Module $PSScriptRoot\..\Helper.psm1 -Verbose:$false + +# Localized messages +data LocalizedData +{ + # culture="en-US" + ConvertFrom-StringData @' +AddingScopeMessage = Adding DHCP server scope with the given ScopeId ({0})... +CheckScopeMessage = Checking DHCP server scope with the given ScopeId ({0})... +SetScopeMessage = DHCP server scope with name '{0}' is now present. +RemovingScopeMessage = Removing DHCP server scope with the given ScopeId ({0})... +DeleteScopeMessage = DHCP server scope with the given ScopeId ({0}) is now absent. +TestScopeMessage = DHCP server scope with the given ScopeId ({0}) is '{1}' and it should be '{2}'. + +CheckPropertyMessage = Checking DHCP server scope '{0}' ... +NotDesiredPropertyMessage = DHCP server scope '{0}' is not correct; expected '{1}', actual '{2}'. +DesiredPropertyMessage = DHCP server scope '{0}' is correct. +SetPropertyMessage = DHCP server scope '{0}' is set to '{1}'. +'@ +} + +<# + .SYNOPSIS + Gets current status of the scope with specified ScopeId. + + .DESCRIPTION + Used by DSC Resource to perform Get method. + For existing scopes retrieves all information that might be defined in the resource. + Fore missing scopes returns only ScopeId, AddressFamily and the fact that it is absent. + + .EXAMPLE + Get-TargetResource -ScopeId 192.168.1.0 -Name MyScope -IPStartRange 192.168.1.1 -IPEndRange 192.168.1.250 -SubnetMask 255.255.255.0 + Gets information about scope 192.168.1.0 (if exists) or retunrs information about missing scope. + + .PARAMETER ScopeId + ScopeId of the DHCP scope + + .PARAMETER Name + Name of the DHCP scope + + .PARAMETER IPStartRange + StartRange of the DHCP scope + + .PARAMETER IPEndRange + EndRange of the DHCP scope + + .PARAMETER SubnetMask + SubnetMask of the DHCP scope + + .PARAMETER AddressFamily + AddressFamily of the DHCP scope +#> +function Get-TargetResource +{ + [CmdletBinding()] + [OutputType([System.Collections.Hashtable])] + param + ( + [Parameter(Mandatory = $true)] + [String] + $ScopeId, + + [Parameter(Mandatory = $true)] + [String] + $Name, + + [Parameter(Mandatory = $true)] + [String] + $IPStartRange, + + [Parameter(Mandatory = $true)] + [String] + $IPEndRange, + + [Parameter(Mandatory = $true)] + [String] + $SubnetMask, + + [Parameter()] + [ValidateSet('IPv4')] + [String] + $AddressFamily = 'IPv4' + + ) + #region Input Validation + + # Check for DhcpServer module/role + Assert-Module -moduleName DHCPServer + + # Check values of IP Addresses used to define the scope + $ipAddressesAssertionParameters = @{ + ScopeId = $ScopeId + IPStartRange = $IPStartRange + IPEndRange = $IPEndRange + SubnetMask = $SubnetMask + AddressFamily = $AddressFamily + } + Assert-ScopeParameter @ipAddressesAssertionParameters + + #endregion Input Validation + + $dhcpScope = Get-DhcpServerv4Scope -ScopeId $ScopeId -ErrorAction SilentlyContinue + if($dhcpScope) + { + $ensure = 'Present' + $leaseDuration = $dhcpScope.LeaseDuration.ToString() + } + else + { + $ensure = 'Absent' + $leaseDuration = '' + } + + return @{ + ScopeID = $ScopeId + Name = $dhcpScope.Name + IPStartRange = $dhcpScope.StartRange + IPEndRange = $dhcpScope.EndRange + SubnetMask = $dhcpScope.SubnetMask + Description = $dhcpScope.Description + LeaseDuration = $leaseDuration + State = $dhcpScope.State + AddressFamily = $AddressFamily + Ensure = $ensure + } +} + +<# + .SYNOPSIS + Sets the scope with specified ScopeId. + + .DESCRIPTION + Used by DSC Resource to perform Set method. + It will add/remove/modify Scope based on input parameters + + .EXAMPLE + Set-TargetResource -ScopeId 192.168.1.0 -Name MyScope -IPStartRange 192.168.1.1 -IPEndRange 192.168.1.250 -SubnetMask 255.255.255.0 + Sets or creates scope with ScopeId 192.168.1.0 with parameters specified. + + .PARAMETER ScopeId + ScopeId of the DHCP scope + + .PARAMETER Name + Expected name of the DHCP scope + + .PARAMETER IPStartRange + Expected startRange of the DHCP scope + + .PARAMETER IPEndRange + Expected endRange of the DHCP scope + + .PARAMETER SubnetMask + Expected subnetMask of the DHCP scope + + .PARAMETER Description + Expected description of the DHCP scope + + .PARAMETER LeaseDuration + Expected duration of the lease of the DHCP scope + + .PARAMETER AddressFamily + Expected address family of the DHCP scope + + .PARAMETER State + Expected state of the DHCP scope + + .PARAMETER Ensure + Expected presence of the DHCP scope +#> +function Set-TargetResource +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [String] + $ScopeId, + + [Parameter(Mandatory = $true)] + [String] + $Name, + + [Parameter(Mandatory = $true)] + [String] + $IPStartRange, + + [Parameter(Mandatory = $true)] + [String] + $IPEndRange, + + [Parameter(Mandatory = $true)] + [String] + $SubnetMask, + + [Parameter()] + [String] + $Description, + + [Parameter()] + [ValidateNotNullOrEmpty()] + [String] + $LeaseDuration, + + [Parameter()] + [ValidateSet('IPv4')] + [String] + $AddressFamily = 'IPv4', + + [Parameter()] + [ValidateSet('Active','Inactive')] + [String] + $State = 'Active', + + [Parameter()] + [ValidateSet('Present','Absent')] + [String] + $Ensure = 'Present' + ) + + #region Input Validation + + # Check for DhcpServer module/role + Assert-Module -moduleName DHCPServer + + # Check values of IP Addresses used to define the scope + $ipAddressesAssertionParameters = @{ + ScopeId = $ScopeId + IPStartRange = $IPStartRange + IPEndRange = $IPEndRange + SubnetMask = $SubnetMask + AddressFamily = $AddressFamily + } + Assert-ScopeParameter @ipAddressesAssertionParameters + + #endregion Input Validation + + + if($PSBoundParameters.ContainsKey('Debug')){ $null = $PSBoundParameters.Remove('Debug')} + if($PSBoundParameters.ContainsKey('AddressFamily')) {$null = $PSBoundParameters.Remove('AddressFamily')} + + Validate-ResourceProperties @PSBoundParameters -Apply + +} + +<# + .SYNOPSIS + Tests the scope with specified ScopeId. + + .DESCRIPTION + Used by DSC Resource to perform Test method. + It will verify that Scope is configured as described in the parameters. + + .EXAMPLE + Test-TargetResource -ScopeId 192.168.1.0 -Name MyScope -IPStartRange 192.168.1.1 -IPEndRange 192.168.1.250 -SubnetMask 255.255.255.0 + Returns $true if scope is configured as described and $false if it's not + + .PARAMETER ScopeId + ScopeId of the DHCP scope + + .PARAMETER Name + Expected name of the DHCP scope + + .PARAMETER IPStartRange + Expected startRange of the DHCP scope + + .PARAMETER IPEndRange + Expected endRange of the DHCP scope + + .PARAMETER SubnetMask + Expected subnetMask of the DHCP scope + + .PARAMETER Description + Expected description of the DHCP scope + + .PARAMETER LeaseDuration + Expected duration of the lease of the DHCP scope + + .PARAMETER AddressFamily + Expected address family of the DHCP scope + + .PARAMETER State + Expected state of the DHCP scope + + .PARAMETER Ensure + Expected presence of the DHCP scope +#> +function Test-TargetResource +{ + [CmdletBinding()] + [OutputType([System.Boolean])] + param + ( + [Parameter(Mandatory = $true)] + [String] + $ScopeId, + + [Parameter(Mandatory = $true)] + [String] + $Name, + + [Parameter(Mandatory = $true)] + [String] + $IPStartRange, + + [Parameter(Mandatory = $true)] + [String] + $IPEndRange, + + [Parameter(Mandatory = $true)] + [String] + $SubnetMask, + + [Parameter()] + [String] + $Description, + + [Parameter()] + [ValidateNotNullOrEmpty()] + [String] + $LeaseDuration, + + [Parameter()] + [ValidateSet('IPv4')] + [String] + $AddressFamily = 'IPv4', + + [Parameter()] + [ValidateSet('Active','Inactive')] + [String] + $State = 'Active', + + [Parameter()] + [ValidateSet('Present','Absent')] + [String] + $Ensure = 'Present' + ) + + #region Input Validation + + # Check for DhcpServer module/role + Assert-Module -moduleName DHCPServer + + # Check values of IP Addresses used to define the scope + $ipAddressesAssertionParameters = @{ + ScopeId = $ScopeId + IPStartRange = $IPStartRange + IPEndRange = $IPEndRange + SubnetMask = $SubnetMask + AddressFamily = $AddressFamily + } + Assert-ScopeParameter @ipAddressesAssertionParameters + + #endregion Input Validation + + if($PSBoundParameters.ContainsKey('Debug')){ $null = $PSBoundParameters.Remove('Debug')} + if($PSBoundParameters.ContainsKey('AddressFamily')) {$null = $PSBoundParameters.Remove('AddressFamily')} + + Validate-ResourceProperties @PSBoundParameters + +} + +function Validate-ResourceProperties +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [String] + $ScopeId, + + [Parameter(Mandatory = $true)] + [String] + $Name, + + [Parameter(Mandatory = $true)] + [String] + $IPStartRange, + + [Parameter(Mandatory = $true)] + [String] + $IPEndRange, + + [Parameter(Mandatory = $true)] + [String] + $SubnetMask, + + [Parameter()] + [String] + $Description, + + [Parameter()] + [ValidateNotNullOrEmpty()] + [String] + $LeaseDuration, + + [Parameter()] + [ValidateSet('Active','Inactive')] + [String] + $State = 'Active', + + [Parameter()] + [ValidateSet('Present','Absent')] + [String] + $Ensure = 'Present', + + [Parameter()] + [Switch] + $Apply + ) + + # Convert the Lease duration to be a valid timespan + if($LeaseDuration) + { + $LeaseDuration = (Get-ValidTimeSpan -tsString $LeaseDuration -parameterName 'Leaseduration').ToString() + } + + $checkScopeMessage = $LocalizedData.CheckScopeMessage -f $ScopeId + Write-Verbose -Message $checkScopeMessage + + $dhcpScope = Get-DhcpServerv4Scope -ScopeId $ScopeId -ErrorAction SilentlyContinue + # Initialize the parameter collection + if($Apply) + { + $parameters = @{} + } + + # dhcpScope is set + if($dhcpScope) + { + $TestScopeMessage = $($LocalizedData.TestScopeMessage) -f $ScopeId, 'present', $Ensure + Write-Verbose -Message $TestScopeMessage + + # if it should be present, test individual properties to match parameter values + if($Ensure -eq 'Present') + { + #region Test the Scope Name + $checkPropertyMsg = $($LocalizedData.CheckPropertyMessage) -f 'name' + Write-Verbose -Message $checkPropertyMsg + + if($dhcpScope.Name -ne $Name) + { + $notDesiredPropertyMsg = $($LocalizedData.NotDesiredPropertyMessage) -f 'name',$Name,$($dhcpScope.Name) + Write-Verbose -Message $notDesiredPropertyMsg + + if($Apply) + { + $parameters['Name'] = $Name + } + else + { + return $false + } + } + else + { + $desiredPropertyMsg = $($LocalizedData.DesiredPropertyMessage) -f 'name' + Write-Verbose -Message $desiredPropertyMsg + } + #endregion scope name + + #region Test the IPStartRange and IPEndRange + if($dhcpScope.StartRange -ne $IPStartRange -or $dhcpScope.EndRange -ne $IPEndRange) + { + $notDesiredPropertyMsg = $($LocalizedData.NotDesiredPropertyMessage) -f 'Start/EndRange',"$IPStartRange/$IPEndRange","$($dhcpScope.StartRange)/$($dhcpScope.EndRange)" + Write-Verbose -Message $notDesiredPropertyMsg + + if($Apply) + { + $parameters['StartRange'] = $IPStartRange + $parameters['EndRange'] = $IPEndRange + } + else + { + return $false + } + } + #endregion IPStartRange and IPEndRange + + #region Test the Scope Description + if($PSBoundParameters.ContainsKey('Description')) + { + $checkPropertyMsg = $($LocalizedData.CheckPropertyMessage) -f 'description' + Write-Verbose -Message $checkPropertyMsg + + if($dhcpScope.Description -ne $Description) + { + $notDesiredPropertyMsg = $($LocalizedData.NotDesiredPropertyMessage) -f 'description',$Description,$($dhcpScope.Description) + Write-Verbose -Message $notDesiredPropertyMsg + + if($Apply) + { + $parameters['Description'] = $Description + } + else + { + return $false + } + } + else + { + $desiredPropertyMsg = $($LocalizedData.DesiredPropertyMessage) -f 'description' + Write-Verbose -Message $desiredPropertyMsg + } + } + #endregion scope description + + #region Test the Lease duration + if($PSBoundParameters.ContainsKey('LeaseDuration')) + { + $checkPropertyMsg = $($LocalizedData.CheckPropertyMessage) -f 'lease duration' + Write-Verbose -Message $checkPropertyMsg + + if($dhcpScope.LeaseDuration -ne $LeaseDuration) + { + $notDesiredPropertyMsg = $($LocalizedData.NotDesiredPropertyMessage) -f 'lease duration',$LeaseDuration,$($dhcpScope.LeaseDuration) + Write-Verbose -Message $notDesiredPropertyMsg + + if($Apply) + { + $parameters['LeaseDuration'] = $LeaseDuration + } + else + { + return $false + } + } + else + { + $desiredPropertyMsg = $($LocalizedData.DesiredPropertyMessage) -f 'lease duration' + Write-Verbose -Message $desiredPropertyMsg + } + } + #endregion lease duration + + #region Test the Scope State + if($PSBoundParameters.ContainsKey('State')) + { + $checkPropertyMsg = $($LocalizedData.CheckPropertyMessage) -f 'state' + Write-Verbose -Message $checkPropertyMsg + + if($dhcpScope.State -ne $State) + { + $notDesiredPropertyMsg = $($LocalizedData.NotDesiredPropertyMessage) -f 'state',$State,$($dhcpScope.State) + Write-Verbose -Message $notDesiredPropertyMsg + + if($Apply) + { + $parameters['State'] = $State + } + else + { + return $false + } + } + else + { + $desiredPropertyMsg = $($LocalizedData.DesiredPropertyMessage) -f 'state' + Write-Verbose -Message $desiredPropertyMsg + } + } + #endregion scope state + + #region Test the Subnet Mask + $checkPropertyMsg = $($LocalizedData.CheckPropertyMessage) -f 'subnet mask' + Write-Verbose -Message $checkPropertyMsg + + if($dhcpScope.SubnetMask -ne $SubnetMask) + { + $notDesiredPropertyMsg = $($LocalizedData.NotDesiredPropertyMessage) -f 'subnet mask',$SubnetMask,$($dhcpScope.SubnetMask) + Write-Verbose -Message $notDesiredPropertyMsg + + if($Apply) + { + try + { + # To set the subnet mask scope, the only ways is to remove the old scope and add a new scope + Remove-DhcpServerv4Scope -ScopeId $ScopeId + ## We can't splat two hashtables and $parameters may be empty, so just clone the existing one + $addDhcpServerv4ScopeParams = $parameters.Clone(); + $addDhcpServerv4ScopeParams['Type'] = 'Dhcp'; + $addDhcpServerv4ScopeParams['StartRange'] = $IPStartRange; + $addDhcpServerv4ScopeParams['EndRange'] = $IPEndRange; + $addDhcpServerv4ScopeParams['Name'] = $Name; + $addDhcpServerv4ScopeParams['SubnetMask'] = $SubnetMask; + Add-DhcpServerv4Scope @addDhcpServerv4ScopeParams; + } + catch + { + New-TerminatingError -errorId DhcpServerScopeFailure -errorMessage $_.Exception.Message -errorCategory InvalidOperation + } + + $setPropertyMsg = $($LocalizedData.SetPropertyMessage) -f 'subnet mask',$SubnetMask + Write-Verbose -Message $setPropertyMsg + } + else + { + return $false + } + } + else + { + $desiredPropertyMsg = $($LocalizedData.DesiredPropertyMessage) -f 'subnet mask' + Write-Verbose -Message $desiredPropertyMsg + } + #endregion subnet mask + + if($Apply) + { + # If parameters contains more than 0 key, set the DhcpServer scope + if($parameters.Count -gt 0) + { + Set-DhcpServerv4Scope @parameters -ScopeId $dhcpScope.ScopeId + Write-PropertyMessage -Parameters $parameters -keysToSkip ScopeId ` + -Message $($LocalizedData.SetPropertyMessage) -Verbose + } + } # end Apply + else + { + return $true + } + } # end ensure eq present + + # If dhcpscope should be absent + else + { + if($Apply) + { + $removingScopeMsg = $LocalizedData.RemovingScopeMessage -f $ScopeId + Write-Verbose -Message $removingScopeMsg + + # Remove the scope + Remove-DhcpServerv4Scope -ScopeId $ScopeId + + $deleteScopeMsg = $LocalizedData.deleteScopeMessage -f $ScopeId + Write-Verbose -Message $deleteScopeMsg + } + else + { + return $false + } + }# end ensure -eq 'Absent' + } # if $dhcpScope + + #If dhcpScope is not set, create it if needed + else + { + $TestScopeMessage = $($LocalizedData.TestScopeMessage) -f $ScopeId, 'absent', $Ensure + Write-Verbose -Message $TestScopeMessage + + if($Ensure -eq 'Present') + { + if($Apply) + { + # Add mandatory parameters + $parameters['Name'] = $Name + $parameters['StartRange'] = $IPStartRange + $parameters['EndRange'] = $IPEndRange + $parameters['SubnetMask'] = $SubnetMask + + # Check if Lease duration is specified, add to parameter collection + if($PSBoundParameters.ContainsKey('LeaseDuration')) + { + $parameters['LeaseDuration'] = $LeaseDuration + } + + # Check if State is specified, add to parameter collection + if($PSBoundParameters.ContainsKey('State')) + { + $parameters['State'] = $State + } + + $addingScopeMessage = $LocalizedData.AddingScopeMessage -f $ScopeId + Write-Verbose -Message $addingScopeMessage + + try + { + # Create a new scope with specified properties + Add-DhcpServerv4Scope @parameters -Type dhcp + + $setScopeMessage = $($LocalizedData.SetScopeMessage) -f $Name + Write-Verbose -Message $setScopeMessage + } + catch + { + New-TerminatingError -errorId DhcpServerScopeFailure -errorMessage $_.Exception.Message -errorCategory InvalidOperation + } + }# end Apply + else + { + return $false + } + } # end Ensure -eq Present + else + { + return $true + } + } # else !dhcpscope +} + +Export-ModuleMember -Function *-TargetResource diff --git a/deployment/dsc/azshcihost/xDhcpServer/2.0.0.0/DSCResources/MSFT_xDhcpServerScope/MSFT_xDhcpServerScope.schema.mof b/deployment/dsc/azshcihost/xDhcpServer/2.0.0.0/DSCResources/MSFT_xDhcpServerScope/MSFT_xDhcpServerScope.schema.mof new file mode 100644 index 0000000..da1b50c --- /dev/null +++ b/deployment/dsc/azshcihost/xDhcpServer/2.0.0.0/DSCResources/MSFT_xDhcpServerScope/MSFT_xDhcpServerScope.schema.mof @@ -0,0 +1,15 @@ +[ClassVersion("1.0.0.0"), FriendlyName("xDhcpServerScope")] +class MSFT_xDhcpServerScope : OMI_BaseResource +{ + [Key, Description("ScopeId for the given scope")] String ScopeId; + [Required, Description("Name of DHCP Scope")] String Name; + [Required, Description("Subnet mask for the scope specified in IP address format")] String SubnetMask; + [Required, Description("Starting address to set for this scope")] String IPStartRange; + [Required, Description("Ending address to set for this scope")] String IPEndRange; + [Write, Description("Description of DHCP Scope")] String Description; + [Write, Description("Time interval for which an IP address should be leased")] String LeaseDuration; + [Write, Description("Whether scope should be active or inactive"), ValueMap{"Active","Inactive"}, Values{"Active","Inactive"}] String State; + [Write, Description("Address family type"), ValueMap{"IPv4"}, Values{"IPv4"}] String AddressFamily; + [Write, Description("Whether scope should be set or removed"), ValueMap{"Present","Absent"}, Values{"Present","Absent"}] String Ensure; +}; + diff --git a/deployment/dsc/azshcihost/xDhcpServer/2.0.0.0/LICENSE b/deployment/dsc/azshcihost/xDhcpServer/2.0.0.0/LICENSE new file mode 100644 index 0000000..6bf8d3a --- /dev/null +++ b/deployment/dsc/azshcihost/xDhcpServer/2.0.0.0/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2018 Microsoft Corporation. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/deployment/dsc/azshcihost/xDhcpServer/2.0.0.0/Misc/New-DhcpServerOptionResource.ps1 b/deployment/dsc/azshcihost/xDhcpServer/2.0.0.0/Misc/New-DhcpServerOptionResource.ps1 new file mode 100644 index 0000000..678623c --- /dev/null +++ b/deployment/dsc/azshcihost/xDhcpServer/2.0.0.0/Misc/New-DhcpServerOptionResource.ps1 @@ -0,0 +1,17 @@ +$Properties = @{ + DnsServer = New-xDscResourceProperty -Name DnsServerIPAddress -Type String[] -Attribute Required ` + -Description 'IP address of DNS Servers' + Router = New-xDscResourceProperty -Name Router -Type String[] -Attribute Required ` + -Description 'IP address of the router/default gateway.' + DnsDomain = New-xDscResourceProperty -Name DnsDomain -Type String -Attribute Write ` + -Description 'Domain name of DNS Server' + AddressFamily = New-xDscResourceProperty -Name AddressFamily -Type String -Attribute Write ` + -ValidateSet 'IPv4' -Description 'Address family type' + ScopeID = New-xDscResourceProperty -Name ScopeID -Type String -Attribute Key ` + -Description 'ScopeId for which options are set' + Ensure = New-xDscResourceProperty -Name Ensure -Type String -Attribute Write ` + -ValidateSet 'Present','Absent' ` + -Description 'Whether option should be set or removed' +} + +New-xDscResource -Name MSFT_xDhcpServerOption -Property $Properties.Values -ModuleName xDhcpServer -FriendlyName xDhcpServerOption diff --git a/deployment/dsc/azshcihost/xDhcpServer/2.0.0.0/Misc/New-DhcpServerReservationResource.ps1 b/deployment/dsc/azshcihost/xDhcpServer/2.0.0.0/Misc/New-DhcpServerReservationResource.ps1 new file mode 100644 index 0000000..dd0850c --- /dev/null +++ b/deployment/dsc/azshcihost/xDhcpServer/2.0.0.0/Misc/New-DhcpServerReservationResource.ps1 @@ -0,0 +1,17 @@ +$Properties = @{ + ScopeID = New-xDscResourceProperty -Name ScopeID -Type String -Attribute Key ` + -Description 'ScopeId for which reservations are set' + IPAddress = New-xDscResourceProperty -Name IPAddress -Type String -Attribute Required ` + -Description 'IP address of the reservation for which the properties are modified' + ClientMACAddress = New-xDscResourceProperty -Name ClientMACAddress -Type String -Attribute Required ` + -Description 'Client ID to set on the reservation For Windows clients it is the MAC address' + Name = New-xDscResourceProperty -Name Name -Type String -Attribute Write ` + -Description 'Reservation name' + AddressFamily = New-xDscResourceProperty -Name AddressFamily -Type String -Attribute Write ` + -ValidateSet 'IPv4' -Description 'Address family type' + Ensure = New-xDscResourceProperty -Name Ensure -Type String -Attribute Write ` + -ValidateSet 'Present','Absent' ` + -Description 'Whether option should be set or removed' +} + +New-xDscResource -Name MSFT_xDhcpServerReservation -Property $Properties.Values -ModuleName xDhcpServer -FriendlyName xDhcpServerReservation diff --git a/deployment/dsc/azshcihost/xDhcpServer/2.0.0.0/Misc/New-DhcpServerScopeResource.ps1 b/deployment/dsc/azshcihost/xDhcpServer/2.0.0.0/Misc/New-DhcpServerScopeResource.ps1 new file mode 100644 index 0000000..a2eadb0 --- /dev/null +++ b/deployment/dsc/azshcihost/xDhcpServer/2.0.0.0/Misc/New-DhcpServerScopeResource.ps1 @@ -0,0 +1,24 @@ +$Properties = @{ + ScopeID = New-xDscResourceProperty -Name ScopeID -Type String -Attribute Read ` + -Description 'ScopeId for which properties are set' + Name = New-xDscResourceProperty -Name Name -Type String -Attribute Required ` + -Description 'Name of DHCP Scope' + AddressFamily = New-xDscResourceProperty -Name AddressFamily -Type String -Attribute Write ` + -ValidateSet 'IPv4' -Description 'Address family type' + IPStartRange = New-xDscResourceProperty -Name IPStartRange -Type String -Attribute Key ` + -Description 'Starting address to set for this scope' + IPEndRange = New-xDscResourceProperty -Name IPEndRange -Type String -Attribute Key ` + -Description 'Ending address to set for this scope' + SubnetMask = New-xDscResourceProperty -Name SubnetMask -Type String -Attribute Write ` + -Description 'Subnet mask for the scope specified in IP address format' + LeaseDuration = New-xDscResourceProperty -Name LeaseDuration -Type String -Attribute Write ` + -Description 'Time interval for which an IP address should be leased' + State = New-xDscResourceProperty -Name State -Type String -Attribute Write ` + -ValidateSet 'Active','Inactive' ` + -Description 'Whether scope should be active or inactive' + Ensure = New-xDscResourceProperty -Name Ensure -Type String -Attribute Write ` + -ValidateSet 'Present','Absent' ` + -Description 'Whether scope should be set or removed' +} + +New-xDscResource -Name MSFT_xDhcpServerScope -Property $Properties.Values -ModuleName xDhcpServer -FriendlyName xDhcpServerScope diff --git a/deployment/dsc/azshcihost/xDhcpServer/2.0.0.0/Modules/CommonResourceHelper.psm1 b/deployment/dsc/azshcihost/xDhcpServer/2.0.0.0/Modules/CommonResourceHelper.psm1 new file mode 100644 index 0000000..bf18d83 --- /dev/null +++ b/deployment/dsc/azshcihost/xDhcpServer/2.0.0.0/Modules/CommonResourceHelper.psm1 @@ -0,0 +1,265 @@ +<# + .SYNOPSIS + Creates and throws an invalid argument exception. + + .PARAMETER Message + The message explaining why this error is being thrown. + + .PARAMETER ArgumentName + The name of the invalid argument that is causing this error to be thrown. +#> +function New-InvalidArgumentException +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $Message, + + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $ArgumentName + ) + + $argumentException = New-Object -TypeName 'ArgumentException' ` + -ArgumentList @($Message, $ArgumentName) + + $newObjectParameters = @{ + TypeName = 'System.Management.Automation.ErrorRecord' + ArgumentList = @($argumentException, $ArgumentName, 'InvalidArgument', $null) + } + + $errorRecord = New-Object @newObjectParameters + + throw $errorRecord +} + +<# + .SYNOPSIS + Creates and throws an invalid operation exception. + + .PARAMETER Message + The message explaining why this error is being thrown. + + .PARAMETER ErrorRecord + The error record containing the exception that is causing this terminating error. +#> +function New-InvalidOperationException +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $Message, + + [Parameter()] + [ValidateNotNull()] + [System.Management.Automation.ErrorRecord] + $ErrorRecord + ) + + if ($null -eq $ErrorRecord) + { + $invalidOperationException = New-Object -TypeName 'InvalidOperationException' ` + -ArgumentList @($Message) + } + else + { + $invalidOperationException = New-Object -TypeName 'InvalidOperationException' ` + -ArgumentList @($Message, $ErrorRecord.Exception) + } + + $newObjectParameters = @{ + TypeName = 'System.Management.Automation.ErrorRecord' + ArgumentList = @( + $invalidOperationException.ToString(), + 'MachineStateIncorrect', + 'InvalidOperation', + $null + ) + } + + $errorRecordToThrow = New-Object @newObjectParameters + + throw $errorRecordToThrow +} + +<# + .SYNOPSIS + Creates and throws an object not found exception. + + .PARAMETER Message + The message explaining why this error is being thrown. + + .PARAMETER ErrorRecord + The error record containing the exception that is causing this terminating error. +#> +function New-ObjectNotFoundException +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $Message, + + [Parameter()] + [ValidateNotNull()] + [System.Management.Automation.ErrorRecord] + $ErrorRecord + ) + + if ($null -eq $ErrorRecord) + { + $exception = New-Object -TypeName 'System.Exception' ` + -ArgumentList @($Message) + } + else + { + $exception = New-Object -TypeName 'System.Exception' ` + -ArgumentList @($Message, $ErrorRecord.Exception) + } + + $newObjectParameters = @{ + TypeName = 'System.Management.Automation.ErrorRecord' + ArgumentList = @( + $exception.ToString(), + 'MachineStateIncorrect', + 'ObjectNotFound', + $null + ) + } + + $errorRecordToThrow = New-Object @newObjectParameters + + throw $errorRecordToThrow +} + +<# + .SYNOPSIS + Creates and throws an invalid result exception. + + .PARAMETER Message + The message explaining why this error is being thrown. + + .PARAMETER ErrorRecord + The error record containing the exception that is causing this terminating error. +#> +function New-InvalidResultException +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $Message, + + [Parameter()] + [ValidateNotNull()] + [System.Management.Automation.ErrorRecord] + $ErrorRecord + ) + + if ($null -eq $ErrorRecord) + { + $exception = New-Object -TypeName 'System.Exception' ` + -ArgumentList @($Message) + } + else + { + $exception = New-Object -TypeName 'System.Exception' ` + -ArgumentList @($Message, $ErrorRecord.Exception) + } + + $newObjectParameters = @{ + TypeName = 'System.Management.Automation.ErrorRecord' + ArgumentList = @( + $exception.ToString(), + 'MachineStateIncorrect', + 'InvalidResult', + $null + ) + } + + $errorRecordToThrow = New-Object @newObjectParameters + + throw $errorRecordToThrow +} + +<# + .SYNOPSIS + Retrieves the localized string data based on the machine's culture. + Falls back to en-US strings if the machine's culture is not supported. + + .PARAMETER ResourceName + The name of the resource as it appears before '.strings.psd1' of the localized string file. + For example: + For WindowsOptionalFeature: MSFT_WindowsOptionalFeature + For Service: MSFT_ServiceResource + For Registry: MSFT_RegistryResource + For Helper: SqlServerDscHelper + + .PARAMETER ScriptRoot + Optional. The root path where to expect to find the culture folder. This is only needed + for localization in helper modules. This should not normally be used for resources. +#> +function Get-LocalizedData +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $ResourceName, + + [Parameter()] + [ValidateNotNullOrEmpty()] + [System.String] + $ScriptRoot + ) + + if ( -not $ScriptRoot ) + { + $resourceDirectory = Join-Path -Path $PSScriptRoot -ChildPath $ResourceName + $localizedStringFileLocation = Join-Path -Path $resourceDirectory -ChildPath $PSUICulture + } + else + { + $localizedStringFileLocation = Join-Path -Path $ScriptRoot -ChildPath $PSUICulture + } + + if (-not (Test-Path -Path $localizedStringFileLocation)) + { + # Fallback to en-US + if ( -not $ScriptRoot ) + { + $localizedStringFileLocation = Join-Path -Path $resourceDirectory -ChildPath 'en-US' + } + else + { + $localizedStringFileLocation = Join-Path -Path $ScriptRoot -ChildPath 'en-US' + } + } + + Import-LocalizedData ` + -BindingVariable 'localizedData' ` + -FileName "$ResourceName.strings.psd1" ` + -BaseDirectory $localizedStringFileLocation + + return $localizedData +} + +Export-ModuleMember -Function @( + 'New-InvalidArgumentException', + 'New-InvalidOperationException', + 'New-ObjectNotFoundException', + 'New-InvalidResultException', + 'Get-LocalizedData' ) diff --git a/deployment/dsc/azshcihost/xDhcpServer/2.0.0.0/Modules/DhcpServerDsc.OptionValueHelper/OptionValueHelper.psm1 b/deployment/dsc/azshcihost/xDhcpServer/2.0.0.0/Modules/DhcpServerDsc.OptionValueHelper/OptionValueHelper.psm1 new file mode 100644 index 0000000..3b49c86 --- /dev/null +++ b/deployment/dsc/azshcihost/xDhcpServer/2.0.0.0/Modules/DhcpServerDsc.OptionValueHelper/OptionValueHelper.psm1 @@ -0,0 +1,758 @@ +# Load Localization Data +Import-Module -Name (Join-Path -Path (Split-Path -Path $PSScriptRoot -Parent) ` + -ChildPath 'CommonResourceHelper.psm1') + +$script:localizedData = Get-LocalizedData -ResourceName 'OptionValueHelper' -ScriptRoot $PSScriptRoot + +<# + .SYNOPSIS + Helper function to get a DHCP option value. + + .PARAMETER ApplyTo + Specify where to get the DHCP option from. + + .PARAMETER OptionId + The option ID. + + .PARAMETER VendorClass + The option vendor class. Use an empty string for standard class. + + .PARAMETER UserClass + The option user class. + + .PARAMETER ScopeId + If used, the option scope ID. + + .PARAMETER PolicyName + If used, the option policy name. + + .PARAMETER ReservedIP + If used, the option reserved IP. + + .PARAMETER AddressFamily + The option definition address family (IPv4 or IPv6). Currently only the IPv4 is supported. +#> +function Get-TargetResourceHelper +{ + [CmdletBinding()] + [OutputType([System.Collections.Hashtable])] + param + ( + [Parameter(Mandatory = $true)] + [ValidateSet('Server','Scope','Policy','ReservedIP')] + [String] + $ApplyTo, + + [parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [UInt32] + $OptionId, + + [parameter(Mandatory = $true)] + [AllowEmptyString()] + [String] + $VendorClass, + + [parameter(Mandatory = $true)] + [AllowEmptyString()] + [String] + $UserClass, + + [parameter()] + [AllowNull()] + [String] + $ScopeId, + + [parameter()] + [AllowNull()] + [String] + $PolicyName, + + [parameter()] + [AllowNull()] + [ipaddress] + $ReservedIP, + + [parameter(Mandatory = $true)] + [ValidateSet('IPv4')] + [String] + $AddressFamily + ) + + #region Input Validation + + # Check for DhcpServer module/role + Assert-Module -moduleName DHCPServer + + #endregion Input Validation + + # Checking if option needs to be configured for server, DHCP scope, Policy or reservedIP + switch ($ApplyTo) + { + + 'Server' + { + # Getting the dhcp server option Value + $serverGettingValueMessage = $localizedData.ServerGettingValueMessage -f $OptionId, $VendorClass, $UserClass + Write-Verbose $serverGettingValueMessage + + $parameters = @{ + OptionId = $OptionId + VendorClass = $VendorClass + userClass = $UserClass + } + $currentConfiguration = Get-DhcpServerv4OptionValue @parameters -ErrorAction SilentlyContinue + } + + 'Scope' + { + # Getting the dhcp server option Value + $scopeGettingValueMessage = $localizedData.ScopeGettingValueMessage -f $OptionId, $VendorClass, $UserClass, $ScopeId + Write-Verbose $scopeGettingValueMessage + + $parameters = @{ + OptionId = $OptionId + ScopeId = $ScopeId + VendorClass = $VendorClass + UserClass = $UserClass + } + $currentConfiguration = Get-DhcpServerv4OptionValue @parameters -ErrorAction SilentlyContinue + } + + 'Policy' + { + # Getting the dhcp policy option Value + $policyGettingValueMessage = $localizedData.PolicyGettingValueMessage -f $OptionId, $VendorClass, $ScopeId, $PolicyName + Write-Verbose $policyGettingValueMessage + + # Policy can exist on server or scope level, so we need to address both cases + if ($ScopeId) + { + $parameters = @{ + PolicyName = $PolicyName + OptionId = $OptionId + VendorClass = $VendorClass + ScopeId = $ScopeId + } + $currentConfiguration = Get-DhcpServerv4OptionValue @parameters -ErrorAction SilentlyContinue + } + else + { + $parameters = @{ + PolicyName = $PolicyName + OptionId = $OptionId + VendorClass = $VendorClass + } + $currentConfiguration = Get-DhcpServerv4OptionValue @parameters -ErrorAction SilentlyContinue + } + } + + 'ReservedIP' + { + # Getting the dhcp reserved IP option Value + $reservedIPGettingValueMessage = $localizedData.ReservedIPGettingValueMessage -f $OptionId, $VendorClass, $PolicyName, $ReservedIP + Write-Verbose $reservedIPGettingValueMessage + + $parameters = @{ + ReservedIP = $ReservedIP + OptionId = $OptionId + VendorClass = $VendorClass + UserClass = $UserClass + } + $currentConfiguration = Get-DhcpServerv4OptionValue @parameters -ErrorAction SilentlyContinue + } + } + + # Testing for null + if ($currentConfiguration) + { + $hashTable = @{ + ApplyTo = $ApplyTo + OptionId = $currentConfiguration.OptionID + Value = $currentConfiguration.Value + VendorClass = $currentConfiguration.VendorClass + UserClass = $currentConfiguration.UserClass + ScopeId = $currentConfiguration.ScopeId + PolicyName = $currentConfiguration.PolicyName + ReservedIP = $currentConfiguration.ReservedIP + AddressFamily = $AddressFamily + Ensure = 'Present' + } + } + else + { + $hashTable = @{ + ApplyTo = $null + OptionId = $null + Value = $null + VendorClass = $null + UserClass = $null + ScopeId = $null + PolicyName = $null + ReservedIP = $null + AddressFamily = $null + Ensure = 'Absent' + } + } + + $hashTable +} + +<# + .SYNOPSIS + Helper function to test a DHCP option value. + + .PARAMETER ApplyTo + Specify where to test the DHCP option. + + .PARAMETER OptionId + The option ID. + + .PARAMETER Value + The option data value. + + .PARAMETER VendorClass + The option vendor class. Use an empty string for standard class. + + .PARAMETER UserClass + The option user class. + + .PARAMETER ScopeId + If used, the option scope ID. + + .PARAMETER PolicyName + If used, the option policy name. + + .PARAMETER ReservedIP + If used, the option reserved IP. + + .PARAMETER AddressFamily + The option definition address family (IPv4 or IPv6). Currently only the IPv4 is supported. + + .PARAMETER Ensure + When set to 'Present', the option will be created. + When set to 'Absent', the option will be removed. +#> +function Test-TargetResourceHelper +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [ValidateSet('Server','Scope','Policy','ReservedIP')] + [String] + $ApplyTo, + + [parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [UInt32] + $OptionId, + + [parameter(Mandatory = $true)] + [String[]] + $Value, + + [parameter(Mandatory = $true)] + [AllowEmptyString()] + [String] + $VendorClass, + + [parameter(Mandatory = $true)] + [AllowEmptyString()] + [String] + $UserClass, + + [parameter()] + [AllowNull()] + [String] + $ScopeId, + + [parameter()] + [AllowNull()] + [String] + $PolicyName, + + [parameter()] + [AllowNull()] + [ipaddress] + $ReservedIP, + + [parameter(Mandatory = $true)] + [ValidateSet('IPv4')] + [String] + $AddressFamily, + + [Parameter()] + [ValidateSet('Present','Absent')] + [String] + $Ensure = 'Present' + ) + + # Checking if option needs to be configured for server, DHCP scope, Policy or reservedIP + switch ($ApplyTo) + { + 'Server' + { + # Trying to get the option value + $parameters = @{ + OptionId = $OptionId + VendorClass = $VendorClass + UserClass = $UserClass + AddressFamily = $AddressFamily + } + $currentConfiguration = Get-TargetResourceHelper -ApplyTo 'Server' @parameters + # Testing for Ensure = Present + if ($Ensure -eq 'Present') + { + # Since we need to compare an array of strings we need to use the Compare-Object cmdlet + if (($currentConfiguration.Ensure -eq 'Present') -and (@(Compare-Object -ReferenceObject $currentConfiguration.Value -DifferenceObject $Value -SyncWindow 0 -CaseSensitive).Length -eq 0)) + { + # Found an exact match + $serverExactMatchValueMessage = $localizedData.ServerExactMatchValueMessage -f $OptionId, $VendorClass, $UserClass + Write-Verbose $serverExactMatchValueMessage + $result = $true + } + else + { + # Not found Option Value + $serverNotFoundValueMessage = $localizedData.ServerNotFoundValueMessage -f $OptionId, $VendorClass, $UserClass + Write-Verbose $serverNotFoundValueMessage + $result = $false + } + } + # Ensure = 'Absent' + else + { + if ($currentConfiguration.Ensure -eq 'Present') + { + # Found a match, should return $false since it should not be here + $serverFoundAndRemoveValueMessage = $localizedData.ServerFoundAndRemoveValueMessage -f $OptionId, $VendorClass, $UserClass + Write-Verbose $serverFoundAndRemoveValueMessage + $result = $false + } + else + { + # Not found Option Value, return $true + $serverNotFoundDoNothingValueMessage = $localizedData.ServerNotFoundDoNothingValueMessage -f $OptionId, $VendorClass, $UserClass + Write-Verbose $serverNotFoundDoNothingValueMessage + $result = $true + } + } + } + + 'Scope' + { + # Trying to get the option value + $parameters = @{ + ScopeId = $ScopeId + OptionId = $OptionId + VendorClass = $VendorClass + UserClass = $UserClass + AddressFamily = $AddressFamily + } + $currentConfiguration = Get-TargetResourceHelper -ApplyTo 'Scope' @parameters + # Testing for Ensure = Present + if ($Ensure -eq 'Present') + { + if (($currentConfiguration.Ensure -eq 'Present') -and (@(Compare-Object -ReferenceObject $currentConfiguration.Value -DifferenceObject $Value -SyncWindow 0 -CaseSensitive).Length -eq 0)) + { + # Found an exact match + $scopeExactMatchValueMessage = $localizedData.ScopeExactMatchValueMessage -f $OptionId, $VendorClass, $UserClass, $ScopeId + Write-Verbose $scopeExactMatchValueMessage + $result = $true + } + else + { + # Not found Option Value + $scopeNotFoundValueMessage = $localizedData.ScopeNotFoundValueMessage -f $OptionId, $VendorClass, $UserClass, $ScopeId + Write-Verbose $scopeNotFoundValueMessage + $result = $false + } + } + + # Ensure = 'Absent' + else + { + if (($currentConfiguration.Ensure -eq 'Present')) + { + # Found a match, should return $false since it should not be here + $scopeFoundAndRemoveValueMessage = $localizedData.ScopeFoundAndRemoveValueMessage -f $OptionId, $VendorClass, $UserClass, $ScopeId + Write-Verbose $scopeFoundAndRemoveValueMessage + $result = $false + } + else + { + # Not found Option Value, return $true + $scopeNotFoundDoNothingValueMessage = $localizedData.ScopeNotFoundDoNothingValueMessage -f $OptionId, $VendorClass, $UserClass, $ScopeId + Write-Verbose $scopeNotFoundDoNothingValueMessage + $result = $true + } + } + } + + 'Policy' + { + # Trying to get the option value + $parameters = @{ + PolicyName = $PolicyName + OptionId = $OptionId + ScopeId = $ScopeId + VendorClass = $VendorClass + UserClass = $UserClass + AddressFamily = $AddressFamily + } + $currentConfiguration = Get-TargetResourceHelper -ApplyTo 'Policy' @parameters + # Testing for Ensure = Present + if ($Ensure -eq 'Present') + { + if (($currentConfiguration.Ensure -eq 'Present') -and (@(Compare-Object -ReferenceObject $currentConfiguration.Value -DifferenceObject $Value -SyncWindow 0 -CaseSensitive).Length -eq 0)) + { + # Found an exact match + $policyExactMatchValueMessage = $localizedData.PolicyExactMatchValueMessage -f $OptionId, $VendorClass, $PolicyName + Write-Verbose $policyExactMatchValueMessage + $result = $true + } + else + { + # Not found Option Value + $policyNotFoundValueMessage = $localizedData.PolicyNotFoundValueMessage -f $OptionId, $VendorClass, $PolicyName + Write-Verbose $policyNotFoundValueMessage + $result = $false + } + } + + # Ensure = 'Absent' + else + { + if (($currentConfiguration.Ensure -eq 'Present')) + { + # Found a match, should return $false since it should not be here + $policyFoundAndRemoveValueMessage = $localizedData.PolicyFoundAndRemoveValueMessage -f $OptionId, $VendorClass, $PolicyName + Write-Verbose $policyFoundAndRemoveValueMessage + $result = $false + } + else + { + # Not found Option Value, return $true + $policyNotFoundDoNothingValueMessage = $localizedData.PolicyNotFoundDoNothingValueMessage -f $OptionId, $VendorClass, $PolicyName + Write-Verbose $policyNotFoundDoNothingValueMessage + $result = $true + } + } + } + + 'ReservedIP' + { + # Trying to get the option value + $parameters = @{ + ReservedIP = $ReservedIP + OptionId = $OptionId + VendorClass = $VendorClass + UserClass = $UserClass + AddressFamily = $AddressFamily + } + $currentConfiguration = Get-TargetResourceHelper -ApplyTo 'ReservedIP' @parameters + # Testing for Ensure = Present + if ($Ensure -eq 'Present') + { + # Comparing only the data value, since we already found an option ID that matchs the other parameters + if (($currentConfiguration.Ensure -eq 'Present') -and (@(Compare-Object -ReferenceObject $currentConfiguration.Value -DifferenceObject $Value -SyncWindow 0 -CaseSensitive).Length -eq 0)) + { + # Found an exact match + $reservedIPExactMatchValueMessage = $localizedData.ReservedIPExactMatchValueMessage -f $OptionId, $VendorClass, $UserClass, $ReservedIP + Write-Verbose $reservedIPExactMatchValueMessage + $result = $true + } + else + { + # Not found Option Value + $reservedIPNotFoundValueMessage = $localizedData.ReservedIPNotFoundValueMessage -f $OptionId, $VendorClass, $UserClass, $ReservedIP + Write-Verbose $reservedIPNotFoundValueMessage + $result = $false + } + } + + # Ensure = 'Absent' + else + { + if ($currentConfiguration.Ensure -eq 'Present') + { + # Found a match, should return $false since it should not be here + $reservedIPFoundAndRemoveValueMessage = $localizedData.ReservedIPFoundAndRemoveValueMessage -f $OptionId, $VendorClass, $UserClass, $ScopeId, $ReservedIP + Write-Verbose $reservedIPFoundAndRemoveValueMessage + $result = $false + } + else + { + # Not found Option Value, return $true + $reservedIPNotFoundDoNothingValueMessage = $localizedData.ReservedIPNotFoundDoNothingValueMessage -f $OptionId, $VendorClass, $UserClass, $scopeId, $ReservedIP + Write-Verbose $reservedIPNotFoundDoNothingValueMessage + $result = $true + } + } + } + } + + $result +} + +<# + .SYNOPSIS + Helper function to set a DHCP option value. + + .PARAMETER ApplyTo + Specify where to set the DHCP option. + + .PARAMETER OptionId + The option ID. + + .PARAMETER Value + The option data value. + + .PARAMETER VendorClass + The option vendor class. Use an empty string for standard class. + + .PARAMETER UserClass + The option user class. + + .PARAMETER ScopeId + If used, the option scope ID. + + .PARAMETER PolicyName + If used, the option policy name. + + .PARAMETER ReservedIP + If used, the option reserved IP. + + .PARAMETER AddressFamily + The option definition address family (IPv4 or IPv6). Currently only the IPv4 is supported. + + .PARAMETER Ensure + When set to 'Present', the option will be created. + When set to 'Absent', the option will be removed. +#> +function Set-TargetResourceHelper +{ + [CmdletBinding()] + [OutputType([System.Boolean])] + param + ( + [Parameter(Mandatory = $true)] + [ValidateSet('Server','Scope','Policy','ReservedIP')] + [String] + $ApplyTo, + + [parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [UInt32] + $OptionId, + + [parameter(Mandatory = $true)] + [String[]] + $Value, + + [parameter(Mandatory = $true)] + [AllowEmptyString()] + [String] + $VendorClass, + + [parameter(Mandatory = $true)] + [AllowEmptyString()] + [String] + $UserClass, + + [parameter()] + [AllowNull()] + [String] + $ScopeId, + + [parameter()] + [AllowNull()] + [String] + $PolicyName, + + [parameter()] + [AllowNull()] + [ipaddress] + $ReservedIP, + + [parameter(Mandatory = $true)] + [ValidateSet('IPv4')] + [String] + $AddressFamily, + + [Parameter()] + [ValidateSet('Present','Absent')] + [String] + $Ensure = 'Present' + ) + + # Checking if option needs to be configured for server, DHCP scope, Policy or reservedIP + switch ($ApplyTo) + { + 'Server' + { + $parameters = @{ + OptionId = $OptionId + VendorClass = $VendorClass + UserClass = $UserClass + AddressFamily = $AddressFamily + } + $currentConfiguration = Get-TargetResourceHelper -ApplyTo 'Server' @parameters + + # Testing for Ensure = Present + if ($Ensure -eq 'Present') + { + $serverSettingValueMessage = $localizedData.ServerSettingValueMessage -f $OptionId, $VendorClass, $UserClass + Write-Verbose $serverSettingValueMessage + Set-DhcpServerv4OptionValue -OptionId $OptionId -Value $Value -VendorClass $VendorClass -UserClass $UserClass + } + + # Ensure = 'Absent' + else + { + # If it exists and Ensure is 'Present' we should remove it + if ($currentConfiguration.Ensure -eq 'Present') + { + $serverRemoveValueMessage = $localizedData.ServerRemoveValueMessage -f $OptionId, $VendorClass, $UserClass + Write-Verbose $serverRemoveValueMessage + Remove-DhcpServerv4OptionValue -OptionId $OptionId -VendorClass $VendorClass -UserClass $UserClass + } + } + } + + 'Scope' + { + $parameters = @{ + ScopeId = $ScopeId + OptionId = $OptionId + VendorClass = $VendorClass + UserClass = $UserClass + AddressFamily = $AddressFamily + } + $currentConfiguration = Get-TargetResourceHelper -ApplyTo 'Scope' @parameters + + # Testing for Ensure = Present + if ($Ensure -eq 'Present') + { + # If value should be present we just set it + $scopeSettingValueMessage = $localizedData.ScopeSettingValueMessage -f $OptionId, $VendorClass, $UserClass, $ScopeId + Write-Verbose $scopeSettingValueMessage + Set-DhcpServerv4OptionValue -ScopeId $ScopeId -OptionId $OptionId -Value $Value -VendorClass $VendorClass -UserClass $UserClass + } + + # Ensure = 'Absent' + else + { + # If it exists and Ensure is 'Present' we should remove it + if ($currentConfiguration.Ensure -eq 'Present') + { + $scopeRemoveValueMessage = $localizedData.ScopeRemoveValueMessage -f $OptionId, $VendorClass, $UserClass, $ScopeId + Write-Verbose $scopeRemoveValueMessage + Remove-DhcpServerv4OptionValue -ScopeId $ScopeId -OptionId $currentConfiguration.OptionId -VendorClass $VendorClass -UserClass $UserClass + } + } + } + + 'Policy' + { + # If $ScopeId exist + if ($ScopeId) + { + $parameters = @{ + PolicyName = $PolicyName + ScopeId = $ScopeId + OptionId = $OptionId + VendorClass = $VendorClass + UserClass = $UserClass + AddressFamily = $AddressFamily + } + $currentConfiguration = Get-TargetResourceHelper -ApplyTo 'Policy' @parameters + + # Testing for Ensure = Present + if ($Ensure -eq 'Present') + { + # If value should be present we just set it + $policyWithScopeSettingValueMessage = $localizedData.PolicyWithScopeSettingValueMessage -f $OptionId, $VendorClass, $PolicyName, $ScopeId + Write-Verbose $policyWithScopeSettingValueMessage + Set-DhcpServerv4OptionValue -PolicyName $PolicyName -OptionId $OptionId -ScopeId $ScopeId -Value $Value -VendorClass $VendorClass + } + + # Ensure = 'Absent' + else + { + # If it exists and Ensure is 'Present' we should remove it + if ($currentConfiguration.Ensure -eq 'Present') + { + $policyWithScopeRemoveValueMessage = $localizedData.policyWithScopeRemoveValueMessage -f $OptionId, $VendorClass, $PolicyName, $ScopeId + Write-Verbose $policyWithScopeRemoveValueMessage + Remove-DhcpServerv4OptionValue -PolicyName $PolicyName -ScopeId $ScopeId -OptionId $OptionId -VendorClass $VendorClass + } + } + } + # If $ScopeId is null + else + { + $parameters = @{ + PolicyName = $PolicyName + OptionId = $OptionId + VendorClass = $VendorClass + UserClass = $UserClass + AddressFamily = $AddressFamily + } + $currentConfiguration = Get-TargetResourceHelper -ApplyTo 'Policy' @parameters + # Testing for Ensure = Present + if ($Ensure -eq 'Present') + { + # If value should be present we just set it + $policySettingValueMessage = $localizedData.PolicySettingValueMessage -f $OptionId, $VendorClass, $PolicyName + Write-Verbose $policySettingValueMessage + Set-DhcpServerv4OptionValue -PolicyName $PolicyName -OptionId $OptionId -Value $Value -VendorClass $VendorClass + } + else + { + # If it exists and Ensure is 'Present' we should remove it + if ($currentConfiguration.Ensure -eq 'Present') + { + $policyRemoveValueMessage = $localizedData.PolicyRemoveValueMessage -f $OptionId, $VendorClass, $PolicyName + Write-Verbose $policyRemoveValueMessage + Remove-DhcpServerv4OptionValue -PolicyName $PolicyName -OptionId $OptionId -VendorClass $VendorClass + } + } + } + } + + 'ReservedIP' + { + $parameters = @{ + ReservedIP = $ReservedIP + OptionId = $OptionId + VendorClass = $VendorClass + UserClass = $UserClass + AddressFamily = $AddressFamily + } + $currentConfiguration = Get-TargetResourceHelper -ApplyTo 'ReservedIP' @parameters + + # Testing for Ensure = Present + if ($Ensure -eq 'Present') + { + # If value should be present we just set it + $reservedIPSettingValueMessage = $localizedData.ReservedIPSettingValueMessage -f $OptionId, $VendorClass, $UserClass, $ReservedIP + Write-Verbose $reservedIPSettingValueMessage + Set-DhcpServerv4OptionValue -ReservedIP $ReservedIP -OptionId $OptionId -Value $Value -VendorClass $VendorClass -UserClass $UserClass + } + + # Ensure = 'Absent' + else + { + # If it exists and Ensure is 'Present' we should remove it + if ($currentConfiguration.Ensure -eq 'Present') + { + $reservedIPRemoveValueMessage = $localizedData.ReservedIPRemoveValueMessage -f $OptionId, $VendorClass, $UserClass, $ReservedIP + Write-Verbose $reservedIPRemoveValueMessage + Remove-DhcpServerv4OptionValue -ReservedIP $ReservedIP -OptionId $OptionId -VendorClass $VendorClass -UserClass $UserClass + } + } + } + } +} diff --git a/deployment/dsc/azshcihost/xDhcpServer/2.0.0.0/Modules/DhcpServerDsc.OptionValueHelper/en-US/OptionValueHelper.strings.psd1 b/deployment/dsc/azshcihost/xDhcpServer/2.0.0.0/Modules/DhcpServerDsc.OptionValueHelper/en-US/OptionValueHelper.strings.psd1 new file mode 100644 index 0000000..babbb17 --- /dev/null +++ b/deployment/dsc/azshcihost/xDhcpServer/2.0.0.0/Modules/DhcpServerDsc.OptionValueHelper/en-US/OptionValueHelper.strings.psd1 @@ -0,0 +1,38 @@ +# Localized resources for helper module OptionValueHelper. + +ConvertFrom-StringData @' + ServerGettingValueMessage = Getting DHCP server option value "{0}" with vendor class "{1}" and user class "{2}". + ServerExactMatchValueMessage = Found DHCP server option value "{0}" with vendor class "{1}" and user class "{2}". + ServerRemoveValueMessage = Removing DHCP server option value "{0}" with vendor class "{1}" and user class "{2}". + ServerSettingValueMessage = Setting DHCP server option value "{0}" with vendor class "{1}" and user class "{2}". + ServerFoundAndRemoveValueMessage = Found DHCP server option value "{0}" with vendor class "{1}" and user class "{2}", should remove it. + ServerNotFoundDoNothingValueMessage = DHCP server option value "{0}" with vendor class "{1}" and user class "{2}" not found, should do nothing. + ServerNotFoundValueMessage = DHCP server option value "{0}" with vendor class "{1}" and user class "{2}" not found. + + ScopeGettingValueMessage = Getting DHCP scope option value "{0}" with vendor class "{1}" and user class "{2}" and Scope ID "{3}". + ScopeExactMatchValueMessage = Found DHCP server option value "{0}" with vendor class "{1}" and user class "{2}" and scope ID "{3}". + ScopeRemoveValueMessage = Removing DHCP server option value "{0}" with vendor class "{1}" and user class "{2}" and scope ID "{3}". + ScopeSettingValueMessage = Setting DHCP server option value "{0}" with vendor class "{1}" and user class "{2}" and scope ID "{3}". + ScopeFoundAndRemoveValueMessage = Found DHCP server option value "{0}" with vendor class "{1}" and user class "{2}" and scope ID "{3}", should remove it. + ScopeNotFoundDoNothingValueMessage = DHCP server option value "{0}" with vendor class "{1}" and user class "{2}" and scope ID "{3}" not found, should do nothing. + ScopeNotFoundValueMessage = DHCP server option value "{0}" with vendor class "{1}" and user class "{2}" and scope ID "{3}" not found. + + PolicyGettingValueMessage = Getting DHCP policy option value "{0}" and vendor class "{1}" and policy name "{2}" and Scope Id "{3}". + PolicyExactMatchValueMessage = Found DHCP policy option value "{0}" with vendor class "{1}" and policy name "{2}". + PolicyRemoveValueMessage = Removing DHCP policy option value "{0}" with vendor class "{1}" and policy name "{2}". + PolicyWithScopeRemoveValueMessage = Removing DHCP policy option value "{0}" with vendor class "{1}" and policy name "{2}" and scope ID "{3}". + PolicySettingValueMessage = Setting DHCP policy option value "{0}" with vendor class "{1}" and policy name "{2}". + PolicyWithScopeSettingValueMessage = Setting DHCP policy option value "{0}" with vendor class "{1}" and policy name "{2}" and scope ID "{3}". + PolicyFoundAndRemoveValueMessage = Found DHCP policy option value "{0}" with vendor class "{1}" and policy name "{2}", should remove it. + PolicyNotFoundDoNothingValueMessage = DHCP policy option value "{0}" with vendor class "{1}" and policy name "{2}" not found, should do nothing. + PolicyNotFoundValueMessage = DHCP policy option value "{0}" with vendor class "{1}" and policy name "{2}" not found. + + ReservedIPGettingValueMessage = Getting DHCP reserved IP option value "{0}" with vendor class "{1}" and user class "{2}" and reserved IP "{3}". + ReservedIPExactMatchValueMessage = Found DHCP reserved IP option value "{0}" with vendor class "{1}" and user class "{2}" and reserved IP "{3}". + ReservedIPRemoveValueMessage = Removing DHCP reserved IP option value "{0}" with vendor class "{1}" and user class "{2}" and reserved IP "{3}". + ReservedIPSettingValueMessage = Setting DHCP reserved IP option value "{0}" with vendor class "{1}" and user class "{2}" and reserved IP "{3}". + ReservedIPFoundAndRemoveValueMessage = Found DHCP reserved IP option value "{0}" with vendor class "{1}" and user class "{2}" and reserved IP "{3}", should remove it. + ReservedIPNotFoundDoNothingValueMessage = DHCP reserved IP option value "{0}" with vendor class "{1}" and user class "{2}" and reserved IP "{3}" not found, should do nothing. + ReservedIPNotFoundValueMessage = DHCP reserved IP option value "{0}" with vendor class "{1}" and user class "{2}" and reserved "{3}" not found. + +'@ diff --git a/deployment/dsc/azshcihost/xDhcpServer/2.0.0.0/README.md b/deployment/dsc/azshcihost/xDhcpServer/2.0.0.0/README.md new file mode 100644 index 0000000..abb5f18 --- /dev/null +++ b/deployment/dsc/azshcihost/xDhcpServer/2.0.0.0/README.md @@ -0,0 +1,321 @@ +# xDhcpServer + +The **xDhcpServer** DSC resources are used for configuring and managing a DHCP server. They include **xDhcpServerScope**, **xDhcpServerReservation**, **xDhcpServerOptions** and **xDhcpServerAuthorization**. + +This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). +For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments. + +## Branches + +### master + +[![Build status](https://ci.appveyor.com/api/projects/status/uan12tf7tfxhg7m5/branch/master?svg=true)](https://ci.appveyor.com/project/PowerShell/xDhcpServer/branch/master) +[![codecov](https://codecov.io/gh/PowerShell/xDhcpServer/branch/master/graph/badge.svg)](https://codecov.io/gh/PowerShell/xDhcpServer/branch/master) + +This is the branch containing the latest release - +no contributions should be made directly to this branch. + +### dev + +[![Build status](https://ci.appveyor.com/api/projects/status/uan12tf7tfxhg7m5/branch/dev?svg=true)](https://ci.appveyor.com/project/PowerShell/xDhcpServer/branch/dev) +[![codecov](https://codecov.io/gh/PowerShell/xDhcpServer/branch/dev/graph/badge.svg)](https://codecov.io/gh/PowerShell/xDhcpServer/branch/dev) + +This is the development branch +to which contributions should be proposed by contributors as pull requests. +This development branch will periodically be merged to the master branch, +and be released to [PowerShell Gallery](https://www.powershellgallery.com/). + +## Contributing + +Please check out common DSC Resources [contributing guidelines](https://github.com/PowerShell/DscResource.Kit/blob/master/CONTRIBUTING.md). + +## Resources + +* **xDhcpServerClass** manages DHCP Classes (Vendor or User). +* **xDhcpServerScope** sets a scope for consecutive range of possible IP addresses that the DHCP server can lease to clients on a subnet. +* **xDhcpServerReservation** sets lease assignments used to ensure that a specified client on a subnet can always use the same IP address. +* **xDhcpServerOptions** (DEPRECATED) currently supports setting DNS domain and DNS Server IP Address options at a DHCP server scope level. +* **xDhcpServerAuthorization** authorizes a DHCP in Active Directory. + * *This resource must run on an Active Directory domain controller.* +* **xDhcpServerOptionDefinition** manages DHCP option definitions. +* **DhcpServerOptionValue** manages an option value on server level. +* **DhcpScopeOptionValue** manages an option value on scope level. +* **DhcpReservedIPOptionValue** manages an option value on reserved IP level. +* **DhcpPolicyOptionValue** manages an option value on Policy level. + +### xDhcpServerScope + +* **ScopeId**: ScopeId of the DHCP scope +* **IPStartRange**: Starting address to set for this scope +* **IPEndRange**: Ending address to set for this scope +* **Name**: Name of this DHCP Scope +* **SubnetMask**: Subnet mask for the scope specified in IP address format +* **LeaseDuration**: Time interval for which an IP address should be leased + * This should be specified in the following format: `Days.Hours:Minutes:Seconds` + * For example, '`02.00:00:00`' is 2 days and '`08:00:00`' is 8 hours. +* **State**: Whether scope should be active or inactive. +* **Ensure**: Whether DHCP scope should be present or removed +* **ScopeID**: Scope Identifier. This is a read-only property for this resource. + +### xDhcpServerReservation + +* **ScopeID**: ScopeId for which reservations are set +* **IPAddress**: IP address of the reservation for which the properties are modified +* **ClientMACAddress**: Client MAC Address to set on the reservation +* **Name**: Reservation name +* **AddressFamily**: Address family type. Note: at this time, only IPv4 is supported. +* **Ensure**: Whether option should be set or removed + +### xDhcpServerOption (DEPRECATED) + +* **ScopeID**: ScopeID for which options are set +* **DnsServerIPAddress**: IP address of DNS Servers +* **DnsDomain**: Domain name of DNS Server +* **AddressFamily**: Address family type +* **Router**: The default gateway for clients +* **Ensure**: Whether option should be set or removed + +### xDhcpServerAuthorization + +* **Ensure**: Whether the DHCP server should be authorized. +* **DnsName**: FQDN of the server to authorize. If not specified, it defaults to the local hostname of the enacting node. +* **IPAddress**: IP v4 address of the server to authorized. If not specified, it default to the first IPv4 address of the enacting node. + +### xDhcpServerClass + + * **Name**: DHCP Class Name. + * **Type**: Class type, should be Vendor or User. + * **AsciiData**: Class Data in a ascii formated string. + * **AddressFamily**: Currently should be "IPv4". + * **Description**: Class Description. + * **Ensure**: Whether class should be set or removed. + + ### xDhcpServerOptionDefinition + + * **OptionID**: Option ID, should be a number between 1 and 255. + * **VendorClass**: Vendor class. Use an empty string for standard option class. + * **Name**: Option name. + * **Type**: Option data type. { Byte | Word | Dword | DwordDword | IPv4Address | String | BinaryData | EncapsulatedData } + * **Multivalued**: Whether option is multivalued or not. + * **Description**: Option description. + * **AddressFamily**: Sets the address family for the option definition. Currently only IPv4 is supported. { IPv4 } + * **Ensure**: Whether option should be set or removed. { *Present* | Absent } + + ### DhcpScopeOptionValue + + * **ScopeId**: Scope ID where to set the option value. + * **OptionId**: Option ID, specify an integer between 1 and 255. + * **Value**: Option data value. Could be an array of string for a multivalued option. + * **VendorClass**: Vendor class. Use an empty string for default vendor class. + * **UserClass**: User class. Use an empty string for default user class. + * **AddressFamily**: Sets the address family for the option definition. Currently only IPv4 is supported. { IPv4 } + * **Ensure**: Whether option should be set or removed. { *Present* | Absent } + +### DhcpServerOptionValue + + * **OptionId**: Option ID, specify an integer between 1 and 255. + * **Value**: Option data value. Could be an array of string for a multivalued option. + * **VendorClass**: Vendor class. Use an empty string for default vendor class. + * **UserClass**: User class. Use an empty string for default user class. + * **AddressFamily**: Sets the address family for the option definition. Currently only IPv4 is supported. { IPv4 } + * **Ensure**: Whether option should be set or removed. { *Present* | Absent } + + ### DhcpReservedIPOptionValue + + * **ReservedIP**: Reserved IP to set the option value. + * **OptionId**: Option ID, specify an integer between 1 and 255. + * **Value**: Option data value. Could be an array of string for a multivalued option. + * **VendorClass**: Vendor class. Use an empty string for default vendor class. + * **UserClass**: User class. Use an empty string for default user class. + * **AddressFamily**: Sets the address family for the option definition. Currently only IPv4 is supported. { IPv4 } + * **Ensure**: Whether option should be set or removed. { *Present* | Absent } + + ### DhcpPolicyOptionValue + + * **PolicyName**: Dhcp Policy Name. + * **OptionId**: Option ID, specify an integer between 1 and 255. + * **Value**: Option data value. Could be an array of string for a multivalued option. + * **ScopeId**: Scope ID to get policy values from. Do not use it to get an option from server level. + * **VendorClass**: Vendor class. Use an empty string for default vendor class. + * **AddressFamily**: Sets the address family for the option definition. Currently only IPv4 is supported. { IPv4 } + * **Ensure**: Whether option should be set or removed. { *Present* | Absent } + + +## Versions + +### Unreleased + +### 2.0.0.0 +* BREAKING CHANGE: Switch to ScopeId as a key property for xDhcpServerScope ([issue #43](https://github.com/PowerShell/xDhcpServer/issues/48). [Bartek Bielawski (@bielawb)](https://github.com/bielawb) + +### 1.7.0.0 + +* Changes to xDhcpServer + * Updated year in LICENSE file. + * Updated year in module manifest. + * Added Codecov and status badges to README.md. + * Update appveyor.yml to use the default template. +* Added xDhcpServerOptionDefinition +* Added DhcpScopeOptionValue +* Added DhcpServerOptionValue +* Added DhcpReservedIPOptionValue +* Added DhcpPolicyOptionValue + +### 1.6.0.0 +added xDhcpServerClass + +### 1.5.0.0 +* Converted AppVeyor.yml to pull Pester from PSGallery instead of Chocolatey +* Bug Fix fixes xDhcpServerOption\Get-TargetResource not returning Router property + +### 1.4.0.0 + +* Bug Fix fixes localization bug in xDhcpServerScope option enumeration + +### 1.3.0.0 + +* Added **xDhcpServerAuthorization** resource. +* Bug Fix LeaseDuration is no longer mandatory for xDhcpServerScope resource. +* Bug Fix DnsServerIPAddress is no longer mandatory for xDhcpServerOption resource. +* Bug Fix corrects verbose display output in xDhcpServerOption resource. + +### 1.2 + +* Fix "Cannot set default gateway on xDhcpServerOption". + +### 1.1 + +* Bug fix, enables creating more than 1 DHCP server scope. + +### 1.0 + +* Initial release with the following resources + * **xDhcpServerScope** + * **xDhcpServerReservation** + * **xDhcpServerOptions** + +## Examples + +### Creating a DHCP Server Scope + +```powershell +configuration Sample_xDhcpsServerScope_NewScope +{ + Import-DscResource -module xDHCpServer + xDhcpServerScope Scope + { + ScopeId = '192.168.1.0' + Ensure = 'Present' + IPEndRange = '192.168.1.254' + IPStartRange = '192.168.1.1' + Name = 'PowerShellScope' + SubnetMask = '255.255.255.0' + LeaseDuration = ((New-TimeSpan -Hours 8 ).ToString()) + State = 'Active' + AddressFamily = 'IPv4' + } +} +``` + +### Resizing existing scope with different values for EndRange + +```powershell +configuration Sample_xDhcpsServerScope_ResizeScope +{ + Import-DscResource -module xDHCpServer + xDhcpServerScope SmallerScope + { + ScopeId = '192.168.1.0' + Ensure = 'Present' + IPEndRange = '192.168.1.100' + IPStartRange = '192.168.1.1' + Name = 'PowerShellScope' + SubnetMask = '255.255.255.0' + LeaseDuration = ((New-TimeSpan -Hours 8 ).ToString()) + State = 'Active' + AddressFamily = 'IPv4' + } +} +``` + +### Reserving an IP address within a DHCP server + +```powershell +configuration Sample_xDhcpServerReservation_IPReservation +{ + Import-DscResource -module xDHCpServer + xDhcpServerReservation PullServerIP + { + Ensure = 'Present' + ScopeID = '192.168.1.0' + ClientMACAddress = '00155D8A54A1' + IPAddress = '192.168.1.2' + Name = 'DSCPullServer' + AddressFamily = 'IPv4' + } +} +``` + +### Setting the domain name, DNS server and default gateway option for a DHCP scope + +```powershell +configuration Sample_xDhcpServerOption_SetScopeOption +{ + Import-DscResource -module xDHCpServer + xDhcpServerOption Option + { + Ensure = 'Present' + ScopeID = '192.168.1.0' + DnsDomain = 'contoso.com' + DnsServerIPAddress = '192.168.1.22','192.168.1.1' + AddressFamily = 'IPv4' + Router = '192.168.1.1' + } +} +``` + +### Authorizing the local DHCP server + +```powershell +configuration Sample_Local_xDhcpServerAuthorization +{ + Import-DscResource -module xDHCpServer + xDhcpServerAuthorization LocalServerActivation + { + Ensure = 'Present' + } +} +``` + +### Authorizing a remote DHCP server + +```powershell +configuration Sample_Remote_xDhcpServerAuthorization +{ + Import-DscResource -module xDHCpServer + xDhcpServerAuthorization RemoteServerActivation + { + Ensure = 'Present' + DnsName = 'servertoauthorize.contoso.com' + IPAddress = '192.168.0.1' + } +} +``` + +### Adding a DHCP Server class + +```powershell +configuration Sample_DHCPServerClass +{ + Import-DscResource -module xDHCpServer + xDhcpServerClass DHCPServerClass + { + ensure = 'Present' + Name = 'VendorClass' + Type = 'Vendor' + AsciiData = 'sampledata' + AddressFamily = 'IPv4' + Description = 'Vendor Class Description' + } +} +``` diff --git a/deployment/dsc/azshcihost/xDhcpServer/2.0.0.0/Samples/DhcpPolicyOptionValue/DhcpPolicyOptionValue.ps1 b/deployment/dsc/azshcihost/xDhcpServer/2.0.0.0/Samples/DhcpPolicyOptionValue/DhcpPolicyOptionValue.ps1 new file mode 100644 index 0000000..e2c04e7 --- /dev/null +++ b/deployment/dsc/azshcihost/xDhcpServer/2.0.0.0/Samples/DhcpPolicyOptionValue/DhcpPolicyOptionValue.ps1 @@ -0,0 +1,36 @@ +<# + .SYNOPSiS + This example sets an option ID 8 (cookie servers) on a policy at server level and at scope level. +#> +configuration Example +{ + Import-DscResource -ModuleName PSDscResources + Import-DscResource -moduleName xDhcpServer + WindowsFeature DHCP + { + Name = 'DHCP' + Ensure = 'Present' + } + + DhcpPolicyOptionValue policyOptionValue_ID-008 + { + OptionId = 8 + Value = '1.1.1.1' + ScopeId = '' + VendorClass = '' + AddressFamily = 'IPv4' + PolicyName = 'TestPolicy' + Ensure = 'Present' + } + + DhcpPolicyOptionValue policyOptionValue_ID-008-scope + { + OptionId = 8 + Value = '1.1.1.1' + ScopeId = '192.168.0.0' + VendorClass = '' + AddressFamily = 'IPv4' + PolicyName = 'TestPolicy' + Ensure = 'Present' + } +} diff --git a/deployment/dsc/azshcihost/xDhcpServer/2.0.0.0/Samples/DhcpReservedIPOptionValue/DhcpReservedIPOptionValue.ps1 b/deployment/dsc/azshcihost/xDhcpServer/2.0.0.0/Samples/DhcpReservedIPOptionValue/DhcpReservedIPOptionValue.ps1 new file mode 100644 index 0000000..19023e8 --- /dev/null +++ b/deployment/dsc/azshcihost/xDhcpServer/2.0.0.0/Samples/DhcpReservedIPOptionValue/DhcpReservedIPOptionValue.ps1 @@ -0,0 +1,25 @@ +<# + .SYNOPSiS + This example sets an option ID 8 (cookie servers) on a a reserved IP level. +#> +configuration Example +{ + Import-DscResource -ModuleName PSDscResources + Import-DscResource -moduleName xDhcpServer + WindowsFeature DHCP + { + Name = 'DHCP' + Ensure = 'Present' + } + + DhcpReservedIPOptionValue ReservedIPOptionValue_ID-008 + { + ReservedIP = '192.168.0.1' + OptionId = 8 + Value ='1.1.1.1' + VendorClass = '' + UserClass = '' + AddressFamily = 'IPv4' + Ensure = 'Present' + } +} diff --git a/deployment/dsc/azshcihost/xDhcpServer/2.0.0.0/Samples/DhcpScopeOptionValue/DhcpScopeOptionValue.ps1 b/deployment/dsc/azshcihost/xDhcpServer/2.0.0.0/Samples/DhcpScopeOptionValue/DhcpScopeOptionValue.ps1 new file mode 100644 index 0000000..876eeac --- /dev/null +++ b/deployment/dsc/azshcihost/xDhcpServer/2.0.0.0/Samples/DhcpScopeOptionValue/DhcpScopeOptionValue.ps1 @@ -0,0 +1,47 @@ +<# + .SYNOPSiS + This example shows how to substitute the xDhcpServerOption resource, setting the gateway (option 3), DNS Servers (option 6) and domain name (Option 15). +#> +configuration Example +{ + Import-DscResource -ModuleName PSDscResources + Import-DscResource -moduleName xDhcpServer + WindowsFeature DHCP + { + Name = 'DHCP' + Ensure = 'Present' + } + + # Setting scope gateway + DhcpScopeOptionValue scopeOptionGateway + { + OptionId = 3 + Value = 1.1.1.1 + ScopeId = '1.1.1.0' + VendorClass = '' + UserClass = '' + AddressFamily = 'IPv4' + } + + # Setting scope DNS servers + DhcpScopeOptionValue scopeOptionDNS + { + OptionId = 6 + Value = 1.1.1.1,2.2.2.2 + ScopeId = '1.1.1.0' + VendorClass = '' + UserClass = '' + AddressFamily = 'IPv4' + } + + # Setting scope DNS domain name + DhcpScopeOptionValue scopeOptionDNSDomainName + { + OptionId = 15 + Value = 'Contoso.com' + ScopeId = '1.1.1.0' + VendorClass = '' + UserClass = '' + AddressFamily = 'IPv4' + } +} diff --git a/deployment/dsc/azshcihost/xDhcpServer/2.0.0.0/Samples/DhcpServerOptionValue/DhcpServerOptionValue.ps1 b/deployment/dsc/azshcihost/xDhcpServer/2.0.0.0/Samples/DhcpServerOptionValue/DhcpServerOptionValue.ps1 new file mode 100644 index 0000000..14ab1fc --- /dev/null +++ b/deployment/dsc/azshcihost/xDhcpServer/2.0.0.0/Samples/DhcpServerOptionValue/DhcpServerOptionValue.ps1 @@ -0,0 +1,24 @@ +<# + .SYNOPSiS + This example sets an option ID 8 (cookie servers) on a server level. +#> +configuration Example +{ + Import-DscResource -ModuleName PSDscResources + Import-DscResource -moduleName xDhcpServer + WindowsFeature DHCP + { + Name = 'DHCP' + Ensure = 'Present' + } + + DhcpServerOptionValue ServerOptionValue_ID-008 + { + OptionId = 8 + Value = '1.1.1.1' + VendorClass = '' + UserClass = '' + AddressFamily = 'IPv4' + Ensure = 'Present' + } +} diff --git a/deployment/dsc/azshcihost/xDhcpServer/2.0.0.0/Samples/SampleConfiguration.ps1 b/deployment/dsc/azshcihost/xDhcpServer/2.0.0.0/Samples/SampleConfiguration.ps1 new file mode 100644 index 0000000..48a1889 --- /dev/null +++ b/deployment/dsc/azshcihost/xDhcpServer/2.0.0.0/Samples/SampleConfiguration.ps1 @@ -0,0 +1,74 @@ +configuration Sample_xDhcpsServerScope_NewScope + { + Import-DscResource -module xDHCpServer + WindowsFeature DHCP + { + Name = 'DHCP' + Ensure = 'Present' + } + xDhcpServerScope Scope + { + Ensure = 'Present' + IPStartRange = '192.168.1.1' + IPEndRange = '192.168.1.254' + + Name = 'ContosoScope' + SubnetMask = '255.255.255.0' + LeaseDuration = '00:08:00' + State = 'Active' + AddressFamily = 'IPv4' + DependsOn = @('[WindowsFeature]DHCP') + } + xDhcpServerReservation PullServerIP + { + Ensure = 'Present' + ScopeID = '192.168.1.0' + ClientMACAddress = '00155D8A54A1' + IPAddress = '192.168.1.2' + Name = 'DSCPullServer' + AddressFamily = 'IPv4' + DependsOn = @('[WindowsFeature]DHCP') + } + xDhcpServerOption Option + { + Ensure = 'Present' + ScopeID = '192.168.1.0' + DnsDomain = 'contoso.com' + DnsServerIPAddress = '192.168.1.22','192.168.1.1' + AddressFamily = 'IPv4' + Router = '192.168.1.1' + DependsOn = @('[WindowsFeature]DHCP') + } + + xDhcpServerclass DHCPServerClass + { + ensure = 'Present' + Name = 'VendorClass' + Type = 'Vendor' + AsciiData = 'sampledata' + AddressFamily = 'IPv4' + Description = 'Vendor Class Description' + } + + xDhcpServerOptionDefinition DHCPServerOptionDefinition + { + Ensure = 'Present' + Name = 'Cisco AP c1700 Provisioning' + OptionID = '200' + Type = 'IPv4Address' + AddressFamily = 'IPv4' + VendorClass = 'Cisco AP c1700' + Description = 'Sample description' + } + + xDhcpServerOptionDefinition DHCPServerOptionDefinition + { + Ensure = 'Present' + Name = 'sample name' + OptionID = '200' + Type = 'IPv4Address' + AddressFamily = 'IPv4' + VendorClass = '' #default option class + Description = 'Sample description' + } + } diff --git a/deployment/dsc/azshcihost/xDhcpServer/2.0.0.0/TestSampleUsingAzure.ps1 b/deployment/dsc/azshcihost/xDhcpServer/2.0.0.0/TestSampleUsingAzure.ps1 new file mode 100644 index 0000000..364c28a --- /dev/null +++ b/deployment/dsc/azshcihost/xDhcpServer/2.0.0.0/TestSampleUsingAzure.ps1 @@ -0,0 +1,17 @@ +# To Run this test create a Windows Server 2012 or 2012 R2 VM in Azure +# and run this script. It will prompt you to choose a VM, choose the VM. +# wait a while, RDP into the machine and manually verify that the DHCP Server +# is configured as specified in .\samples\SampleConfiguration.ps1 + +Write-Verbose -Message 'Publishing configuration ...' -Verbose +Publish-AzureVMDscConfiguration -ConfigurationPath .\Samples\SampleConfiguration.ps1 -Verbose -force +Write-Verbose -Message 'Choosing VM ...' -Verbose +$vm = get-azurevm | Out-GridView -Title 'choose vm to test with' -OutputMode Single +if($vm) +{ + Write-Verbose -Message 'Setting Extension ...' -Verbose + Set-AzureVMDscExtension -ConfigurationArchive SampleConfiguration.ps1.zip -ConfigurationName Sample_xDhcpsServerScope_NewScope -VM $vm -Verbose + Write-Verbose -Message 'Updating Vm ...' -Verbose + $vm | Update-AzureVM +} +Write-Verbose -Message 'Done' -Verbose diff --git a/deployment/dsc/azshcihost/xDhcpServer/2.0.0.0/Tests/Unit/CommonResourceHelper.Tests.ps1 b/deployment/dsc/azshcihost/xDhcpServer/2.0.0.0/Tests/Unit/CommonResourceHelper.Tests.ps1 new file mode 100644 index 0000000..5416383 --- /dev/null +++ b/deployment/dsc/azshcihost/xDhcpServer/2.0.0.0/Tests/Unit/CommonResourceHelper.Tests.ps1 @@ -0,0 +1,191 @@ +Describe 'CommonResourceHelper Unit Tests' { + BeforeAll { + # Import the CommonResourceHelper module to test + $dscResourcesFolderFilePath = Join-Path -Path (Split-Path -Path (Split-Path -Path $PSScriptRoot -Parent) -Parent) ` + -ChildPath 'Modules' + + Import-Module -Name (Join-Path -Path $dscResourcesFolderFilePath ` + -ChildPath 'CommonResourceHelper.psm1') -Force + } + + InModuleScope 'CommonResourceHelper' { + Describe 'Get-LocalizedData' { + $mockTestPath = { + return $mockTestPathReturnValue + } + + $mockImportLocalizedData = { + $BaseDirectory | Should -Be $mockExpectedLanguagePath + } + + BeforeEach { + Mock -CommandName Test-Path -MockWith $mockTestPath -Verifiable + Mock -CommandName Import-LocalizedData -MockWith $mockImportLocalizedData -Verifiable + } + + Context 'When loading localized data for Swedish' { + $mockExpectedLanguagePath = 'sv-SE' + $mockTestPathReturnValue = $true + + It 'Should call Import-LocalizedData with sv-SE language' { + Mock -CommandName Join-Path -MockWith { + return 'sv-SE' + } -Verifiable + + { Get-LocalizedData -ResourceName 'DummyResource' } | Should -Not -Throw + + Assert-MockCalled -CommandName Join-Path -Exactly -Times 2 -Scope It + Assert-MockCalled -CommandName Test-Path -Exactly -Times 1 -Scope It + Assert-MockCalled -CommandName Import-LocalizedData -Exactly -Times 1 -Scope It + } + + $mockExpectedLanguagePath = 'en-US' + $mockTestPathReturnValue = $false + + It 'Should call Import-LocalizedData and fallback to en-US if sv-SE language does not exist' { + Mock -CommandName Join-Path -MockWith { + return $ChildPath + } -Verifiable + + { Get-LocalizedData -ResourceName 'DummyResource' } | Should -Not -Throw + + Assert-MockCalled -CommandName Join-Path -Exactly -Times 3 -Scope It + Assert-MockCalled -CommandName Test-Path -Exactly -Times 1 -Scope It + Assert-MockCalled -CommandName Import-LocalizedData -Exactly -Times 1 -Scope It + } + + Context 'When $ScriptRoot is set to a path' { + $mockExpectedLanguagePath = 'sv-SE' + $mockTestPathReturnValue = $true + + It 'Should call Import-LocalizedData with sv-SE language' { + Mock -CommandName Join-Path -MockWith { + return 'sv-SE' + } -Verifiable + + { Get-LocalizedData -ResourceName 'DummyResource' -ScriptRoot '.' } | Should -Not -Throw + + Assert-MockCalled -CommandName Join-Path -Exactly -Times 1 -Scope It + Assert-MockCalled -CommandName Test-Path -Exactly -Times 1 -Scope It + Assert-MockCalled -CommandName Import-LocalizedData -Exactly -Times 1 -Scope It + } + + $mockExpectedLanguagePath = 'en-US' + $mockTestPathReturnValue = $false + + It 'Should call Import-LocalizedData and fallback to en-US if sv-SE language does not exist' { + Mock -CommandName Join-Path -MockWith { + return $ChildPath + } -Verifiable + + { Get-LocalizedData -ResourceName 'DummyResource' -ScriptRoot '.' } | Should -Not -Throw + + Assert-MockCalled -CommandName Join-Path -Exactly -Times 2 -Scope It + Assert-MockCalled -CommandName Test-Path -Exactly -Times 1 -Scope It + Assert-MockCalled -CommandName Import-LocalizedData -Exactly -Times 1 -Scope It + } + } + } + + Context 'When loading localized data for English' { + Mock -CommandName Join-Path -MockWith { + return 'en-US' + } -Verifiable + + $mockExpectedLanguagePath = 'en-US' + $mockTestPathReturnValue = $true + + It 'Should call Import-LocalizedData with en-US language' { + { Get-LocalizedData -ResourceName 'DummyResource' } | Should -Not -Throw + } + } + + Assert-VerifiableMock + } + + Describe 'New-InvalidResultException' { + Context 'When calling with Message parameter only' { + It 'Should throw the correct error' { + $mockErrorMessage = 'Mocked error' + + { New-InvalidResultException -Message $mockErrorMessage } | Should -Throw $mockErrorMessage + } + } + + Context 'When calling with both the Message and ErrorRecord parameter' { + It 'Should throw the correct error' { + $mockErrorMessage = 'Mocked error' + $mockExceptionErrorMessage = 'Mocked exception error message' + + $mockException = New-Object -TypeName System.Exception -ArgumentList $mockExceptionErrorMessage + $mockErrorRecord = New-Object -TypeName System.Management.Automation.ErrorRecord -ArgumentList $mockException, $null, 'InvalidResult', $null + + { New-InvalidResultException -Message $mockErrorMessage -ErrorRecord $mockErrorRecord } | Should -Throw ('System.Exception: {0} ---> System.Exception: {1}' -f $mockErrorMessage, $mockExceptionErrorMessage) + } + } + + Assert-VerifiableMock + } + + Describe 'New-ObjectNotFoundException' { + Context 'When calling with Message parameter only' { + It 'Should throw the correct error' { + $mockErrorMessage = 'Mocked error' + + { New-ObjectNotFoundException -Message $mockErrorMessage } | Should -Throw $mockErrorMessage + } + } + + Context 'When calling with both the Message and ErrorRecord parameter' { + It 'Should throw the correct error' { + $mockErrorMessage = 'Mocked error' + $mockExceptionErrorMessage = 'Mocked exception error message' + + $mockException = New-Object -TypeName System.Exception -ArgumentList $mockExceptionErrorMessage + $mockErrorRecord = New-Object -TypeName System.Management.Automation.ErrorRecord -ArgumentList $mockException, $null, 'InvalidResult', $null + + { New-ObjectNotFoundException -Message $mockErrorMessage -ErrorRecord $mockErrorRecord } | Should -Throw ('System.Exception: {0} ---> System.Exception: {1}' -f $mockErrorMessage, $mockExceptionErrorMessage) + } + } + + Assert-VerifiableMock + } + + Describe 'New-InvalidOperationException' { + Context 'When calling with Message parameter only' { + It 'Should throw the correct error' { + $mockErrorMessage = 'Mocked error' + + { New-InvalidOperationException -Message $mockErrorMessage } | Should -Throw $mockErrorMessage + } + } + + Context 'When calling with both the Message and ErrorRecord parameter' { + It 'Should throw the correct error' { + $mockErrorMessage = 'Mocked error' + $mockExceptionErrorMessage = 'Mocked exception error message' + + $mockException = New-Object -TypeName System.Exception -ArgumentList $mockExceptionErrorMessage + $mockErrorRecord = New-Object -TypeName System.Management.Automation.ErrorRecord -ArgumentList $mockException, $null, 'InvalidResult', $null + + { New-InvalidOperationException -Message $mockErrorMessage -ErrorRecord $mockErrorRecord } | Should -Throw ('System.InvalidOperationException: {0} ---> System.Exception: {1}' -f $mockErrorMessage, $mockExceptionErrorMessage) + } + } + + Assert-VerifiableMock + } + + Describe 'New-InvalidArgumentException' { + Context 'When calling with both the Message and ArgumentName parameter' { + It 'Should throw the correct error' { + $mockErrorMessage = 'Mocked error' + $mockArgumentName = 'MockArgument' + + { New-InvalidArgumentException -Message $mockErrorMessage -ArgumentName $mockArgumentName } | Should -Throw ('Parameter name: {0}' -f $mockArgumentName) + } + } + + Assert-VerifiableMock + } + } +} diff --git a/deployment/dsc/azshcihost/xDhcpServer/2.0.0.0/Tests/Unit/Helper.Tests.ps1 b/deployment/dsc/azshcihost/xDhcpServer/2.0.0.0/Tests/Unit/Helper.Tests.ps1 new file mode 100644 index 0000000..71f0653 --- /dev/null +++ b/deployment/dsc/azshcihost/xDhcpServer/2.0.0.0/Tests/Unit/Helper.Tests.ps1 @@ -0,0 +1,78 @@ +$script:ModuleName = 'Helper' + +#region HEADER +$script:moduleRoot = Split-Path -Parent (Split-Path -Parent $PSScriptRoot) +if ( (-not (Test-Path -Path (Join-Path -Path $moduleRoot -ChildPath 'DSCResource.Tests'))) -or + (-not (Test-Path -Path (Join-Path -Path $moduleRoot -ChildPath 'DSCResource.Tests\TestHelper.psm1'))) ) +{ + & git @('clone','https://github.com/PowerShell/DscResource.Tests.git',(Join-Path -Path $moduleRoot -ChildPath '\DSCResource.Tests\')) +} +else +{ + & git @('-C',(Join-Path -Path $moduleRoot -ChildPath '\DSCResource.Tests\'),'pull') +} +Import-Module (Join-Path -Path $script:moduleRoot -ChildPath 'DSCResource.Tests\TestHelper.psm1') -Force +Import-Module (Join-Path -Path $script:moduleRoot -ChildPath 'DSCResources\Helper.psm1') + +#endregion + +# Begin Testing +InModuleScope $script:ModuleName { + + $testParams = @{ + ScopeId = '192.168.1.0' + IPStartRange = '192.168.1.10' + IPEndRange = '192.168.1.99' + SubnetMask = '255.255.255.0' + AddressFamily = 'IPv4' + } + + #region Function Assert-ScopeParameter + Describe 'Helper\Assert-ScopeParameter' { + It 'Should not throw when parameters are correct' { + { Assert-ScopeParameter @testParams } | Should -Not -Throw + } + + It 'Should return nothing when parameters are correct' { + Assert-ScopeParameter @testParams | Should -BeNullOrEmpty + } + + It 'Should throw an exception with ErrorId <ErrorId> and information about incorrect <Parameter> (<Value>)' { + param ( + [String]$Parameter, + [String]$Value, + [String]$ErrorPattern, + [String]$ErrorId + ) + $brokenTestParams = $testParams.Clone() + $brokenTestParams[$Parameter] = $Value + { Assert-ScopeParameter @brokenTestParams } | Should -Throw -ExpectedMessage $ErrorPattern -ErrorId $ErrorId + } -TestCases @( + @{ + Parameter = 'ScopeId' + Value = '192.168.1.42' + ErrorPattern = 'Value of byte 4 in ScopeId (42) is not valid.' + ErrorId = 'ScopeIdOrMaskIncorrect' + } + @{ + Parameter = 'IPStartRange' + Value = '192.168.0.1' + ErrorPattern = 'Value of byte 3 in IPStartRange (0) is not valid.' + ErrorId = 'ScopeIdOrMaskIncorrect' + } + @{ + Parameter = 'IPEndRange' + Value = '192.167.1.100' + ErrorPattern = 'Value of byte 2 in IPEndRange (167) is not valid.' + ErrorId = 'ScopeIdOrMaskIncorrect' + } + @{ + Parameter = 'IPEndRange' + Value = '192.168.1.2' + ErrorPattern = 'not valid. Start should be lower than end.' + ErrorId = 'RangeNotCorrect' + } + ) + } + #endregion +} diff --git a/deployment/dsc/azshcihost/xDhcpServer/2.0.0.0/Tests/Unit/MSFT_DhcpPolicyOptionValue.Tests.ps1 b/deployment/dsc/azshcihost/xDhcpServer/2.0.0.0/Tests/Unit/MSFT_DhcpPolicyOptionValue.Tests.ps1 new file mode 100644 index 0000000..37c7e3c --- /dev/null +++ b/deployment/dsc/azshcihost/xDhcpServer/2.0.0.0/Tests/Unit/MSFT_DhcpPolicyOptionValue.Tests.ps1 @@ -0,0 +1,211 @@ +#region HEADER + +# Unit Test Template Version: 1.2.1 +$script:moduleRoot = Split-Path -Parent (Split-Path -Parent $PSScriptRoot) +if ( (-not (Test-Path -Path (Join-Path -Path $script:moduleRoot -ChildPath 'DSCResource.Tests'))) -or ` + (-not (Test-Path -Path (Join-Path -Path $script:moduleRoot -ChildPath 'DSCResource.Tests\TestHelper.psm1'))) ) +{ + & git @('clone','https://github.com/PowerShell/DscResource.Tests.git',(Join-Path -Path $script:moduleRoot -ChildPath 'DSCResource.Tests')) +} + +Import-Module -Name (Join-Path -Path $script:moduleRoot -ChildPath (Join-Path -Path 'DSCResource.Tests' -ChildPath 'TestHelper.psm1')) -Force + +$TestEnvironment = Initialize-TestEnvironment ` + -DSCModuleName 'xDhcpServer' ` + -DSCResourceName 'MSFT_DhcpPolicyOptionValue' ` + -TestType Unit + +#endregion HEADER + +function Invoke-TestSetup { +} + +function Invoke-TestCleanup { + Restore-TestEnvironment -TestEnvironment $TestEnvironment +} + +# Begin Testing +try +{ + Invoke-TestSetup + + InModuleScope 'MSFT_DhcpPolicyOptionValue' { + + $policyName = 'Test Policy' + $optionId = 67 + $value = @('test Value') + $scopeId = '10.1.1.0' + $vendorClass = '' + $addressFamily = 'IPv4' + $ensure = 'Present' + + $testParams = @{ + PolicyName = $policyName + OptionId = $optionId + ScopeId = $scopeId + VendorClass = $vendorClass + AddressFamily = $addressFamily + } + + $getFakeDhcpPolicyv4OptionValue = { + return @{ + PolicyName = $policyName + OptionId = $optionId + Value = $value + ScopeId = $scopeId + VendorClass = $vendorClass + AddressFamily = $addressFamily + } + } + + $getFakeDhcpPolicyv4OptionValueID168 = { + return @{ + PolicyName = $policyName + OptionId = 168 + Value = $value + ScopeId = $scopeId + VendorClass = $vendorClass + AddressFamily = $addressFamily + } + } + + $getFakeDhcpPolicyv4OptionValueDifferentValue = { + return @{ + PolicyName = $policyName + OptionId = $optionId + Value = @('DifferentValue') + ScopeId = $scopeId + VendorClass = $vendorClass + AddressFamily = $addressFamily + } + } + + + Describe 'xDhcpServer\Get-TargetResource' { + + Mock Assert-Module -ModuleName OptionValueHelper -ParameterFilter { $ModuleName -eq 'DHCPServer' } { } + Mock Get-DhcpServerv4OptionValue -ModuleName OptionValueHelper -MockWith $GetFakeDhcpPolicyv4OptionValue + + It 'Should call "Assert-Module" to ensure "DHCPServer" module is available' { + + $result = Get-TargetResource @testParams + + Assert-MockCalled -CommandName Assert-Module -Scope It -ModuleName OptionValueHelper + } + + It 'Returns a "System.Collections.Hashtable" object type' { + + $result = Get-TargetResource @testParams + $result | Should BeOfType [System.Collections.Hashtable] + } + + It 'Returns "Absent" when the option value does not exist' { + + Mock Get-DhcpServerv4OptionValue -ModuleName OptionValueHelper {return $null} + + $result = Get-TargetResource @testParams + $result.Ensure | Should -Be 'Absent' + } + + It 'Returns all correct values'{ + + Mock Get-DhcpServerv4OptionValue -ModuleName OptionValueHelper -MockWith $getFakeDhcpPolicyv4OptionValueDifferentValue + + $result = Get-TargetResource @testParams + $result.Ensure | Should Be $ensure + $result.OptionId | Should Be $optionId + $result.PolicyName | Should Be $policyName + $result.Value | Should Be @('DifferentValue') + $result.VendorClass | Should Be $vendorClass + $result.AddressFamily | Should Be $addressFamily + } + + It 'Returns the properties as $null when the option does not exist' { + + Mock Get-DhcpServerv4OptionValue -ModuleName OptionValueHelper {return $null} + + $result = Get-TargetResource @testParams + $result.Ensure | Should Be 'Absent' + $result.OptionId | Should Be $null + $result.PolicyName | Should Be $null + $result.Value | Should Be $null + $result.VendorClass | Should Be $null + $result.AddressFamily | Should Be $null + } + } + + + Describe 'xDhcpServer\Test-TargetResource' { + + Mock Assert-Module -ModuleName OptionValueHelper -ParameterFilter { $ModuleName -eq 'DHCPServer' } { } + + It 'Returns a "System.Boolean" object type' { + + Mock Get-DhcpServerv4OptionValue -ModuleName OptionValueHelper -MockWith $GetFakeDhcpPolicyv4OptionValue + + $result = Test-TargetResource @testParams -Ensure 'Present' -Value $value + $result | Should BeOfType [System.Boolean] + } + + It 'Returns $true when the option exists and Ensure = Present' { + + Mock Get-DhcpServerv4OptionValue -ModuleName OptionValueHelper -MockWith $GetFakeDhcpPolicyv4OptionValue + + $result = Test-TargetResource @testParams -Ensure 'Present' -Value $value + $result | Should Be $true + } + + It 'Returns $false when the option does not exist and Ensure = Present' { + + Mock Get-DhcpServerv4OptionValue -ModuleName OptionValueHelper {return $null} + + $result = Test-TargetResource @testParams -Ensure 'Present' -Value $value + $result | Should Be $false + } + + It 'Returns $false when the option exists and Ensure = Absent ' { + + Mock Get-DhcpServerv4OptionValue -ModuleName OptionValueHelper -MockWith $GetFakeDhcpPolicyv4OptionValue + + $result = Test-TargetResource @testParams -Ensure 'Absent' -Value $value + $result | Should Be $false + } + } + + Describe 'xDhcpServer\Set-TargetResource' { + + Mock -CommandName Assert-Module -ModuleName OptionValueHelper -ParameterFilter { $ModuleName -eq 'DHCPServer' } + + Mock Remove-DhcpServerv4OptionValue -ModuleName OptionValueHelper + Mock Set-DhcpServerv4OptionValue -ModuleName OptionValueHelper + + It 'Should call "Set-DhcpServerv4Optionvalue" when "Ensure" = "Present" and definition does not exist' { + + Mock Get-DhcpServerv4OptionValue -ModuleName OptionValueHelper {return $null} + + Set-TargetResource @testParams -Ensure 'Present' -Value $value + Assert-MockCalled -CommandName Set-DhcpServerv4OptionValue -Scope It -ModuleName OptionValueHelper + } + + It 'Should call "Remove-DhcpServerv4OptionValue" when "Ensure" = "Absent" and Definition does exist' { + + Mock Get-DhcpServerv4OptionValue -ModuleName OptionValueHelper -MockWith $GetFakeDhcpPolicyv4OptionValue + + Set-TargetResource @testParams -Ensure 'Absent' -Value $value + Assert-MockCalled -CommandName Remove-DhcpServerv4OptionValue -ModuleName OptionValueHelper -Scope It + } + + It 'Should call "Set-DhcpServerv4OptionValue" when "Ensure" = "Present" and option value is different' { + + Mock Get-DhcpServerv4OptionValue -ModuleName OptionValueHelper -MockWith $getFakeDhcpPolicyv4OptionValueDifferentValue + + Set-TargetResource @testParams -Ensure 'Present' -Value $value + Assert-MockCalled -CommandName Set-DhcpServerv4OptionValue -ModuleName OptionValueHelper -Scope It + } + } + } +} +finally +{ + Invoke-TestCleanup +} diff --git a/deployment/dsc/azshcihost/xDhcpServer/2.0.0.0/Tests/Unit/MSFT_DhcpReservedIPOptionValue.Tests.ps1 b/deployment/dsc/azshcihost/xDhcpServer/2.0.0.0/Tests/Unit/MSFT_DhcpReservedIPOptionValue.Tests.ps1 new file mode 100644 index 0000000..bd5613f --- /dev/null +++ b/deployment/dsc/azshcihost/xDhcpServer/2.0.0.0/Tests/Unit/MSFT_DhcpReservedIPOptionValue.Tests.ps1 @@ -0,0 +1,208 @@ +#region HEADER + +# Unit Test Template Version: 1.2.1 +$script:moduleRoot = Split-Path -Parent (Split-Path -Parent $PSScriptRoot) +if ( (-not (Test-Path -Path (Join-Path -Path $script:moduleRoot -ChildPath 'DSCResource.Tests'))) -or ` + (-not (Test-Path -Path (Join-Path -Path $script:moduleRoot -ChildPath 'DSCResource.Tests\TestHelper.psm1'))) ) +{ + & git @('clone','https://github.com/PowerShell/DscResource.Tests.git',(Join-Path -Path $script:moduleRoot -ChildPath 'DSCResource.Tests')) +} + +Import-Module -Name (Join-Path -Path $script:moduleRoot -ChildPath (Join-Path -Path 'DSCResource.Tests' -ChildPath 'TestHelper.psm1')) -Force + +$TestEnvironment = Initialize-TestEnvironment ` + -DSCModuleName 'xDhcpServer' ` + -DSCResourceName 'MSFT_DhcpReservedIPOptionValue' ` + -TestType Unit + +#endregion HEADER + +function Invoke-TestSetup { +} + +function Invoke-TestCleanup { + Restore-TestEnvironment -TestEnvironment $TestEnvironment +} + +# Begin Testing +try +{ + Invoke-TestSetup + + InModuleScope 'MSFT_DhcpReservedIPOptionValue' { + + $optionId = 67 + $reservedIP = '1.1.1.1' + $value = [array] @('testValue1','testValue2') + $vendorClass = '' + $userClass = '' + $addressFamily = 'IPv4' + $ensure = 'Present' + + $testParams = @{ + OptionId = $optionId + ReservedIP = $reservedIP + VendorClass = $vendorClass + UserClass = $userClass + AddressFamily = $addressFamily + } + + $getFakeDhcpReservedIPv4OptionValue = { + return @{ + OptionId = $optionId + Value = $value + VendorClass = $vendorClass + UserClass = $userClass + AddressFamily = $addressFamily + } + } + + $getFakeDhcpReservedIPv4OptionValueID168 = { + return @{ + OptionId = 168 + Value = $value + VendorClass = $vendorClass + UserClass = $userClass + AddressFamily = $addressFamily + } + } + + $getFakeDhcpReservedIPv4OptionValueDifferentValue = { + return @{ + OptionId = $optionId + Value = @('DifferentValue') + VendorClass = $vendorClass + UserClass = $userClass + AddressFamily = $addressFamily + } + } + + Describe 'xDhcpServer\Get-TargetResource' { + + Mock Assert-Module -ModuleName OptionValueHelper -ParameterFilter { $ModuleName -eq 'DHCPServer' } { } + Mock Get-DhcpServerv4OptionValue -ModuleName OptionValueHelper -MockWith $GetFakeDhcpReservedIPv4OptionValue + + It 'Should call "Assert-Module" to ensure "DHCPServer" module is available' { + + $result = Get-TargetResource @testParams + + Assert-MockCalled -CommandName Assert-Module -Scope It -ModuleName OptionValueHelper + } + + It 'Returns a "System.Collections.Hashtable" object type' { + + $result = Get-TargetResource @testParams + $result | Should BeOfType [System.Collections.Hashtable] + } + + It 'Returns "Absent" when the option value does not exist' { + + Mock Get-DhcpServerv4OptionValue -ModuleName OptionValueHelper {return $null} + + $result = Get-TargetResource @testParams + $result.Ensure | Should Be 'Absent' + } + + It 'Returns all correct values'{ + + Mock Get-DhcpServerv4OptionValue -ModuleName OptionValueHelper -MockWith $getFakeDhcpReservedIPv4OptionValueDifferentValue + + $result = Get-TargetResource @testParams + $result.Ensure | Should Be $ensure + $result.OptionId | Should Be $optionId + $result.Value | Should Be @('DifferentValue') + $result.VendorClass | Should Be $vendorClass + $result.UserClass | Should Be $userClass + $result.AddressFamily | Should Be $addressFamily + } + + It 'Returns the properties as $null when the option does not exist' { + + Mock Get-DhcpServerv4OptionValue -ModuleName OptionValueHelper {return $null} + + $result = Get-TargetResource @testParams + $result.Ensure | Should Be 'Absent' + $result.OptionId | Should Be $null + $result.ReservedIP | Should Be $null + $result.Value | Should Be $null + $result.VendorClass | Should Be $null + $result.UserClass | Should Be $null + $result.AddressFamily | Should Be $null + } + } + + + Describe 'xDhcpServer\Test-TargetResource' { + + Mock Assert-Module -ModuleName OptionValueHelper -ParameterFilter { $ModuleName -eq 'DHCPServer' } { } + + It 'Returns a "System.Boolean" object type' { + + Mock Get-DhcpServerv4OptionValue -ModuleName OptionValueHelper -MockWith $GetFakeDhcpReservedIPv4OptionValue + + $result = Test-TargetResource @testParams -Ensure 'Present' -Value $value + $result | Should BeOfType [System.Boolean] + } + + It 'Returns $true when the option exists and Ensure = Present' { + + Mock Get-DhcpServerv4OptionValue -ModuleName OptionValueHelper -MockWith $GetFakeDhcpReservedIPv4OptionValue + + $result = Test-TargetResource @testParams -Ensure 'Present' -Value $value + $result | Should Be $true + } + + It 'Returns $false when the option does not exist and Ensure = Present' { + + Mock Get-DhcpServerv4OptionValue -ModuleName OptionValueHelper {return $null} + + $result = Test-TargetResource @testParams -Ensure 'Present' -Value $value + $result | Should Be $false + } + + It 'Returns $false when the option exists and Ensure = Absent ' { + + Mock Get-DhcpServerv4OptionValue -ModuleName OptionValueHelper -MockWith $GetFakeDhcpReservedIPv4OptionValue + + $result = Test-TargetResource @testParams -Ensure 'Absent' -Value $value + $result | Should Be $false + } + } + + Describe 'xDhcpServer\Set-TargetResource' { + + Mock -CommandName Assert-Module -ModuleName OptionValueHelper -ParameterFilter { $ModuleName -eq 'DHCPServer' } + + Mock Remove-DhcpServerv4OptionValue -ModuleName OptionValueHelper + Mock Set-DhcpServerv4OptionValue -ModuleName OptionValueHelper + + It 'Should call "Set-DhcpServerv4Optionvalue" when "Ensure" = "Present" and definition does not exist' { + + Mock Get-DhcpServerv4OptionValue -ModuleName OptionValueHelper {return $null} + + Set-TargetResource @testParams -Ensure 'Present' -Value $value + Assert-MockCalled -CommandName Set-DhcpServerv4OptionValue -Scope It -ModuleName OptionValueHelper + } + + It 'Should call "Remove-DhcpServerv4OptionValue" when "Ensure" = "Absent" and Definition does exist' { + + Mock Get-DhcpServerv4OptionValue -ModuleName OptionValueHelper -MockWith $GetFakeDhcpReservedIPv4OptionValue + + Set-TargetResource @testParams -Ensure 'Absent' -Value $value + Assert-MockCalled -CommandName Remove-DhcpServerv4OptionValue -ModuleName OptionValueHelper -Scope It + } + + It 'Should call "Set-DhcpServerv4OptionValue" when "Ensure" = "Present" and option value is different' { + + Mock Get-DhcpServerv4OptionValue -ModuleName OptionValueHelper -MockWith $getFakeDhcpReservedIPv4OptionValueDifferentValue + + Set-TargetResource @testParams -Ensure 'Present' -Value $value + Assert-MockCalled -CommandName Set-DhcpServerv4OptionValue -ModuleName OptionValueHelper -Scope It + } + } + } +} +finally +{ + Invoke-TestCleanup +} diff --git a/deployment/dsc/azshcihost/xDhcpServer/2.0.0.0/Tests/Unit/MSFT_DhcpScopeOptionValue.Tests.ps1 b/deployment/dsc/azshcihost/xDhcpServer/2.0.0.0/Tests/Unit/MSFT_DhcpScopeOptionValue.Tests.ps1 new file mode 100644 index 0000000..3110cb5 --- /dev/null +++ b/deployment/dsc/azshcihost/xDhcpServer/2.0.0.0/Tests/Unit/MSFT_DhcpScopeOptionValue.Tests.ps1 @@ -0,0 +1,211 @@ +#region HEADER + +# Unit Test Template Version: 1.2.1 +$script:moduleRoot = Split-Path -Parent (Split-Path -Parent $PSScriptRoot) +if ( (-not (Test-Path -Path (Join-Path -Path $script:moduleRoot -ChildPath 'DSCResource.Tests'))) -or ` + (-not (Test-Path -Path (Join-Path -Path $script:moduleRoot -ChildPath 'DSCResource.Tests\TestHelper.psm1'))) ) +{ + & git @('clone','https://github.com/PowerShell/DscResource.Tests.git',(Join-Path -Path $script:moduleRoot -ChildPath 'DSCResource.Tests')) +} + +Import-Module -Name (Join-Path -Path $script:moduleRoot -ChildPath (Join-Path -Path 'DSCResource.Tests' -ChildPath 'TestHelper.psm1')) -Force + +$TestEnvironment = Initialize-TestEnvironment ` + -DSCModuleName 'xDhcpServer' ` + -DSCResourceName 'MSFT_DhcpScopeOptionValue' ` + -TestType Unit + +#endregion HEADER + +function Invoke-TestSetup { +} + +function Invoke-TestCleanup { + Restore-TestEnvironment -TestEnvironment $TestEnvironment +} + +# Begin Testing +try +{ + Invoke-TestSetup + + InModuleScope 'MSFT_DhcpScopeOptionValue' { + + $optionId = 67 + $value = @('test Value') + $scopeId = '10.1.1.0' + $vendorClass = '' + $userClass = '' + $addressFamily = 'IPv4' + $ensure = 'Present' + + $testParams = @{ + OptionId = $optionId + ScopeId = $scopeId + VendorClass = $vendorClass + UserClass = $userClass + AddressFamily = $addressFamily + } + + $getFakeDhcpScopev4OptionValue = { + return @{ + OptionId = $optionId + Value = $value + ScopeId = $scopeId + VendorClass = $vendorClass + UserClass = $userClass + AddressFamily = $addressFamily + } + } + + $getFakeDhcpScopev4OptionValueID168 = { + return @{ + OptionId = 168 + Value = $value + ScopeId = $scopeId + VendorClass = $vendorClass + UserClass = $userClass + AddressFamily = $addressFamily + } + } + + $getFakeDhcpScopev4OptionValueDifferentValue = { + return @{ + OptionId = $optionId + Value = @('DifferentValue') + ScopeId = $scopeId + VendorClass = $vendorClass + UserClass = $userClass + AddressFamily = $addressFamily + } + } + + Describe 'xDhcpServer\Get-TargetResource' { + + Mock Assert-Module -ModuleName OptionValueHelper -ParameterFilter { $ModuleName -eq 'DHCPServer' } { } + Mock Get-DhcpServerv4OptionValue -ModuleName OptionValueHelper -MockWith $GetFakeDhcpScopev4OptionValue + + It 'Should call "Assert-Module" to ensure "DHCPServer" module is available' { + + $result = Get-TargetResource @testParams + + Assert-MockCalled -CommandName Assert-Module -Scope It -ModuleName OptionValueHelper + } + + It 'Returns a "System.Collections.Hashtable" object type' { + + $result = Get-TargetResource @testParams + $result | Should BeOfType [System.Collections.Hashtable] + } + + It 'Returns "Absent" when the option value does not exist' { + + Mock Get-DhcpServerv4OptionValue -ModuleName OptionValueHelper {return $null} + + $result = Get-TargetResource @testParams + $result.Ensure | Should -Be 'Absent' + } + + It 'Returns all correct values'{ + + Mock Get-DhcpServerv4OptionValue -ModuleName OptionValueHelper -MockWith $getFakeDhcpScopev4OptionValueDifferentValue + + $result = Get-TargetResource @testParams + $result.Ensure | Should Be $ensure + $result.OptionId | Should Be $optionId + $result.ScopeId | Should Be $scopeId + $result.Value | Should Be @('DifferentValue') + $result.VendorClass | Should Be $vendorClass + $result.UserClass | Should Be $userClass + $result.AddressFamily | Should Be $addressFamily + } + + It 'Returns the properties as $null when the option does not exist' { + + Mock Get-DhcpServerv4OptionValue -ModuleName OptionValueHelper {return $null} + + $result = Get-TargetResource @testParams + $result.Ensure | Should Be 'Absent' + $result.OptionId | Should Be $null + $result.ScopeId | Should Be $null + $result.Value | Should Be $null + $result.VendorClass | Should Be $null + $result.UserClass | Should Be $null + $result.AddressFamily | Should Be $null + } + } + + Describe 'xDhcpServer\Test-TargetResource' { + + Mock Assert-Module -ModuleName OptionValueHelper -ParameterFilter { $ModuleName -eq 'DHCPServer' } { } + + It 'Returns a "System.Boolean" object type' { + + Mock Get-DhcpServerv4OptionValue -ModuleName OptionValueHelper -MockWith $GetFakeDhcpScopev4OptionValue + + $result = Test-TargetResource @testParams -Ensure 'Present' -Value $value + $result | Should BeOfType [System.Boolean] + } + + It 'Returns $true when the option exists and Ensure = Present' { + + Mock Get-DhcpServerv4OptionValue -ModuleName OptionValueHelper -MockWith $GetFakeDhcpScopev4OptionValue + + $result = Test-TargetResource @testParams -Ensure 'Present' -Value $value + $result | Should Be $true + } + + It 'Returns $false when the option does not exist and Ensure = Present' { + + Mock Get-DhcpServerv4OptionValue -ModuleName OptionValueHelper {return $null} + + $result = Test-TargetResource @testParams -Ensure 'Present' -Value $value + $result | Should Be $false + } + + It 'Returns $false when the option exists and Ensure = Absent ' { + + Mock Get-DhcpServerv4OptionValue -ModuleName OptionValueHelper -MockWith $GetFakeDhcpScopev4OptionValue + + $result = Test-TargetResource @testParams -Ensure 'Absent' -Value $value + $result | Should Be $false + } + } + + Describe 'xDhcpServer\Set-TargetResource' { + + Mock -CommandName Assert-Module -ModuleName OptionValueHelper -ParameterFilter { $ModuleName -eq 'DHCPServer' } + + Mock Remove-DhcpServerv4OptionValue -ModuleName OptionValueHelper + Mock Set-DhcpServerv4OptionValue -ModuleName OptionValueHelper + + It 'Should call "Set-DhcpServerv4Optionvalue" when "Ensure" = "Present" and definition does not exist' { + + Mock Get-DhcpServerv4OptionValue -ModuleName OptionValueHelper {return $null} + + Set-TargetResource @testParams -Ensure 'Present' -Value $value + Assert-MockCalled -CommandName Set-DhcpServerv4OptionValue -Scope It -ModuleName OptionValueHelper + } + + It 'Should call "Remove-DhcpServerv4OptionValue" when "Ensure" = "Absent" and Definition does exist' { + + Mock Get-DhcpServerv4OptionValue -ModuleName OptionValueHelper -MockWith $GetFakeDhcpScopev4OptionValue + + Set-TargetResource @testParams -Ensure 'Absent' -Value $value + Assert-MockCalled -CommandName Remove-DhcpServerv4OptionValue -ModuleName OptionValueHelper -Scope It + } + + It 'Should call "Set-DhcpServerv4OptionValue" when "Ensure" = "Present" and option value is different' { + + Mock Get-DhcpServerv4OptionValue -ModuleName OptionValueHelper -MockWith $getFakeDhcpScopev4OptionValueDifferentValue + + Set-TargetResource @testParams -Ensure 'Present' -Value $value + Assert-MockCalled -CommandName Set-DhcpServerv4OptionValue -ModuleName OptionValueHelper -Scope It + } + } + } +} +finally +{ + Invoke-TestCleanup +} diff --git a/deployment/dsc/azshcihost/xDhcpServer/2.0.0.0/Tests/Unit/MSFT_DhcpServerOptionValue.Tests.ps1 b/deployment/dsc/azshcihost/xDhcpServer/2.0.0.0/Tests/Unit/MSFT_DhcpServerOptionValue.Tests.ps1 new file mode 100644 index 0000000..b583782 --- /dev/null +++ b/deployment/dsc/azshcihost/xDhcpServer/2.0.0.0/Tests/Unit/MSFT_DhcpServerOptionValue.Tests.ps1 @@ -0,0 +1,204 @@ +#region HEADER + +# Unit Test Template Version: 1.2.1 +$script:moduleRoot = Split-Path -Parent (Split-Path -Parent $PSScriptRoot) +if ( (-not (Test-Path -Path (Join-Path -Path $script:moduleRoot -ChildPath 'DSCResource.Tests'))) -or ` + (-not (Test-Path -Path (Join-Path -Path $script:moduleRoot -ChildPath 'DSCResource.Tests\TestHelper.psm1'))) ) +{ + & git @('clone','https://github.com/PowerShell/DscResource.Tests.git',(Join-Path -Path $script:moduleRoot -ChildPath 'DSCResource.Tests')) +} + +Import-Module -Name (Join-Path -Path $script:moduleRoot -ChildPath (Join-Path -Path 'DSCResource.Tests' -ChildPath 'TestHelper.psm1')) -Force + +$TestEnvironment = Initialize-TestEnvironment ` + -DSCModuleName 'xDhcpServer' ` + -DSCResourceName 'MSFT_DhcpServerOptionValue' ` + -TestType Unit + +#endregion HEADER + +function Invoke-TestSetup { +} + +function Invoke-TestCleanup { + Restore-TestEnvironment -TestEnvironment $TestEnvironment +} + +# Begin Testing +try +{ + Invoke-TestSetup + + InModuleScope 'MSFT_DhcpServerOptionValue' { + + $optionId = 67 + $value = @('test Value') + $vendorClass = '' + $userClass = '' + $addressFamily = 'IPv4' + $ensure = 'Present' + + $testParams = @{ + OptionId = $optionId + VendorClass = $vendorClass + UserClass = $userClass + AddressFamily = $addressFamily + } + + $getFakeDhcpServerv4OptionValue = { + return @{ + OptionId = $optionId + Value = $value + VendorClass = $vendorClass + UserClass = $userClass + AddressFamily = $addressFamily + } + } + + $getFakeDhcpServerv4OptionValueID168 = { + return @{ + OptionId = 168 + Value = $value + VendorClass = $vendorClass + UserClass = $userClass + AddressFamily = $addressFamily + } + } + + $getFakeDhcpServerv4OptionValueDifferentValue = { + return @{ + OptionId = $optionId + Value = @('DifferentValue') + VendorClass = $vendorClass + UserClass = $userClass + AddressFamily = $addressFamily + } + } + + Describe 'xDhcpServer\Get-TargetResource' { + + Mock Assert-Module -ModuleName OptionValueHelper -ParameterFilter { $ModuleName -eq 'DHCPServer' } { } + Mock Get-DhcpServerv4OptionValue -ModuleName OptionValueHelper -MockWith $GetFakeDhcpServerv4OptionValue + + It 'Should call "Assert-Module" to ensure "DHCPServer" module is available' { + + $result = Get-TargetResource @testParams + + Assert-MockCalled -CommandName Assert-Module -Scope It -ModuleName OptionValueHelper + } + + It 'Returns a "System.Collections.Hashtable" object type' { + + $result = Get-TargetResource @testParams + $result | Should BeOfType [System.Collections.Hashtable] + } + + It 'Returns "Absent" when the option value does not exist' { + + Mock Get-DhcpServerv4OptionValue -ModuleName OptionValueHelper {return $null} + + $result = Get-TargetResource @testParams + $result.Ensure | Should -Be 'Absent' + } + + It 'Returns all correct values'{ + + Mock Get-DhcpServerv4OptionValue -ModuleName OptionValueHelper -MockWith $getFakeDhcpServerv4OptionValueDifferentValue + + $result = Get-TargetResource @testParams + $result.Ensure | Should Be $ensure + $result.OptionId | Should Be $optionId + $result.Value | Should Be @('DifferentValue') + $result.VendorClass | Should Be $vendorClass + $result.UserClass | Should Be $userClass + $result.AddressFamily | Should Be $addressFamily + } + + It 'Returns the properties as $null when the option does not exist' { + + Mock Get-DhcpServerv4OptionValue -ModuleName OptionValueHelper {return $null} + + $result = Get-TargetResource @testParams + $result.Ensure | Should Be 'Absent' + $result.OptionId | Should Be $null + $result.Value | Should Be $null + $result.VendorClass | Should Be $null + $result.UserClass | Should Be $null + $result.AddressFamily | Should Be $null + } + } + + Describe 'xDhcpServer\Test-TargetResource' { + + Mock Assert-Module -ModuleName OptionValueHelper -ParameterFilter { $ModuleName -eq 'DHCPServer' } { } + + It 'Returns a "System.Boolean" object type' { + + Mock Get-DhcpServerv4OptionValue -ModuleName OptionValueHelper -MockWith $GetFakeDhcpServerv4OptionValue + + $result = Test-TargetResource @testParams -Ensure 'Present' -Value $value + $result | Should BeOfType [System.Boolean] + } + + It 'Returns $true when the option exists and Ensure = Present' { + + Mock Get-DhcpServerv4OptionValue -ModuleName OptionValueHelper -MockWith $GetFakeDhcpServerv4OptionValue + + $result = Test-TargetResource @testParams -Ensure 'Present' -Value $value + $result | Should Be $true + } + + It 'Returns $false when the option does not exist and Ensure = Present' { + + Mock Get-DhcpServerv4OptionValue -ModuleName OptionValueHelper {return $null} + + $result = Test-TargetResource @testParams -Ensure 'Present' -Value $value + $result | Should Be $false + } + + It 'Returns $false when the option exists and Ensure = Absent ' { + + Mock Get-DhcpServerv4OptionValue -ModuleName OptionValueHelper -MockWith $GetFakeDhcpServerv4OptionValue + + $result = Test-TargetResource @testParams -Ensure 'Absent' -Value $value + $result | Should Be $false + } + } + + Describe 'xDhcpServer\Set-TargetResource' { + + Mock -CommandName Assert-Module -ModuleName OptionValueHelper -ParameterFilter { $ModuleName -eq 'DHCPServer' } + + Mock Remove-DhcpServerv4OptionValue -ModuleName OptionValueHelper + Mock Set-DhcpServerv4OptionValue -ModuleName OptionValueHelper + + It 'Should call "Set-DhcpServerv4Optionvalue" when "Ensure" = "Present" and definition does not exist' { + + Mock Get-DhcpServerv4OptionValue -ModuleName OptionValueHelper {return $null} + + Set-TargetResource @testParams -Ensure 'Present' -Value $value + Assert-MockCalled -CommandName Set-DhcpServerv4OptionValue -Scope It -ModuleName OptionValueHelper + } + + It 'Should call "Remove-DhcpServerv4OptionValue" when "Ensure" = "Absent" and Definition does exist' { + + Mock Get-DhcpServerv4OptionValue -ModuleName OptionValueHelper -MockWith $GetFakeDhcpServerv4OptionValue + + Set-TargetResource @testParams -Ensure 'Absent' -Value $value + Assert-MockCalled -CommandName Remove-DhcpServerv4OptionValue -ModuleName OptionValueHelper -Scope It + } + + It 'Should call "Set-DhcpServerv4OptionValue" when "Ensure" = "Present" and option value is different' { + + Mock Get-DhcpServerv4OptionValue -ModuleName OptionValueHelper -MockWith $getFakeDhcpServerv4OptionValueDifferentValue + + Set-TargetResource @testParams -Ensure 'Present' -Value $value + Assert-MockCalled -CommandName Set-DhcpServerv4OptionValue -ModuleName OptionValueHelper -Scope It + } + } + } +} +finally +{ + Invoke-TestCleanup +} diff --git a/deployment/dsc/azshcihost/xDhcpServer/2.0.0.0/Tests/Unit/MSFT_xDhcpServerAuthorization.Tests.ps1 b/deployment/dsc/azshcihost/xDhcpServer/2.0.0.0/Tests/Unit/MSFT_xDhcpServerAuthorization.Tests.ps1 new file mode 100644 index 0000000..5adcea1 --- /dev/null +++ b/deployment/dsc/azshcihost/xDhcpServer/2.0.0.0/Tests/Unit/MSFT_xDhcpServerAuthorization.Tests.ps1 @@ -0,0 +1,209 @@ +$Global:DSCModuleName = 'xDhcpServer' +$Global:DSCResourceName = 'MSFT_xDhcpServerAuthorization' + +#region HEADER +[String] $moduleRoot = Split-Path -Parent (Split-Path -Parent (Split-Path -Parent $Script:MyInvocation.MyCommand.Path)) +if ( (-not (Test-Path -Path (Join-Path -Path $moduleRoot -ChildPath 'DSCResource.Tests'))) -or ` + (-not (Test-Path -Path (Join-Path -Path $moduleRoot -ChildPath 'DSCResource.Tests\TestHelper.psm1'))) ) +{ + & git @('clone','https://github.com/PowerShell/DscResource.Tests.git',(Join-Path -Path $moduleRoot -ChildPath '\DSCResource.Tests\')) +} +else +{ + & git @('-C',(Join-Path -Path $moduleRoot -ChildPath '\DSCResource.Tests\'),'pull') +} +Import-Module (Join-Path -Path $moduleRoot -ChildPath 'DSCResource.Tests\TestHelper.psm1') -Force +$TestEnvironment = Initialize-TestEnvironment ` + -DSCModuleName $Global:DSCModuleName ` + -DSCResourceName $Global:DSCResourceName ` + -TestType Unit +#endregion + +# TODO: Other Optional Init Code Goes Here... + +# Begin Testing +try +{ + + #region Pester Tests + + # The InModuleScope command allows you to perform white-box unit testing on the internal + # (non-exported) code of a Script Module. + InModuleScope $Global:DSCResourceName { + + #region Pester Test Initialization + + ## Mock missing functions + function Get-DhcpServerInDc { } + function Add-DhcpServerInDc { } + ## http://virtualengine.co.uk/2015/mocking-missing-cmdlet-pipelines-with-pester/ + function Remove-DhcpServerInDc { [CmdletBinding()] param ( [Parameter(ValueFromPipeline)] $someValue ) } + + ## Test TargetResource parameters with Ensure = 'Present' + $testPresentParams = @{ Ensure = 'Present'; DnsName = 'test1.contoso.com'; IPAddress = '192.168.1.1'; } + ## Test TargetResource parameters with Ensure = 'Absent' + $testAbsentParams = @{ Ensure = 'Absent'; DnsName = 'test1.contoso.com'; IPAddress = '192.168.1.1'; } + + ## Authorised server list with test1.contoso.com authorised + $fakeDhcpServersPresent = @( + @{ IPAddress = '192.168.1.1'; DnsName = 'test1.contoso.com'; }, + @{ IPAddress = '192.168.1.2'; DnsName = 'test2.contoso.com'; }, + @{ IPAddress = '192.168.1.3'; DnsName = 'test3.contoso.com'; } + ) + ## Authorised server list with test1.contoso.com not authorised + $fakeDhcpServersAbsent = @( + @{ IPAddress = '192.168.1.2'; DnsName = 'test2.contoso.com'; }, + @{ IPAddress = '192.168.1.3'; DnsName = 'test3.contoso.com'; } + ) + ## Authorised server list with mismathed DnsName, but matched IPAddress + $fakeDhcpServersMismatchDnsName = @( + @{ IPAddress = '192.168.1.1'; DnsName = 'test11.contoso.com'; }, + @{ IPAddress = '192.168.1.2'; DnsName = 'test2.contoso.com'; }, + @{ IPAddress = '192.168.1.3'; DnsName = 'test3.contoso.com'; } + ) + ## Authorised server list with mismathed IPAddress, but matched DnsName + $fakeDhcpServersMismatchIPAddress = @( + @{ IPAddress = '192.168.1.11'; DnsName = 'test1.contoso.com'; }, + @{ IPAddress = '192.168.1.2'; DnsName = 'test2.contoso.com'; }, + @{ IPAddress = '192.168.1.3'; DnsName = 'test3.contoso.com'; } + ) + + #endregion + + #region Function Get-TargetResource + Describe "$($Global:DSCResourceName)\Get-TargetResource" { + + Mock Assert-Module { }; + + It 'Returns a [System.Collection.Hashtable] type' { + Mock Get-DhcpServerInDC { return $fakeDhcpServersPresent; } + + $result = Get-TargetResource @testPresentParams; + + $result -is [System.Collections.Hashtable] | Should Be $true; + } + It 'Returns Ensure is Present when DHCP server authorization exists' { + Mock Get-DhcpServerInDC { return $fakeDhcpServersPresent; } + + $result = Get-TargetResource @testPresentParams + + $result.Ensure | Should Be 'Present'; + } + It 'Returns Ensure is Absent when DHCP server authorization does not exist' { + Mock Get-DhcpServerInDC { } + + $result = Get-TargetResource @testPresentParams; + + $result.Ensure | Should Be 'Absent'; + } + + } + #endregion Function Get-TargetResource + + #region Function Test-TargetResource + Describe "$($Global:DSCResourceName)\Test-TargetResource" { + Mock Assert-Module { }; + + It 'Returns a [System.Boolean] type' { + Mock Get-DhcpServerInDC { return $fakeDhcpServersPresent; } + + $result = Test-TargetResource @testPresentParams; + + $result -is [System.Boolean] | Should Be $true; + } + It 'Fails when DHCP Server authorization does not exist and Ensure is Present' { + Mock Get-DhcpServerInDC { return $fakeDhcpServersAbsent; } + + Test-TargetResource @testPresentParams | Should Be $false; + } + It 'Fails when DHCP Server authorization does exist and Ensure is Absent' { + Mock Get-DhcpServerInDC { return $fakeDhcpServersPresent; } + + Test-TargetResource @testAbsentParams | Should Be $false; + } + It 'Fails when DHCP Server authorization does exist, Ensure is Present but DnsName is wrong' { + Mock Get-DhcpServerInDC { return $fakeDhcpServersMismatchDnsName; } + + Test-TargetResource @testPresentParams | Should Be $false; + } + It 'Fails when DHCP Server authorization does exist, Ensure is Present but IPAddress is wrong' { + Mock Get-DhcpServerInDC { return $fakeDhcpServersMismatchIPAddress; } + + Test-TargetResource @testPresentParams | Should Be $false; + } + It 'Passes when DHCP Server authorization does exist and Ensure is Present' { + Mock Get-DhcpServerInDC { return $fakeDhcpServersPresent; } + + $result = Test-TargetResource @testPresentParams + + $result -is [System.Boolean] | Should Be $true; + } + It 'Passes when DHCP Server authorization does not exist and Ensure is Absent' { + Mock Get-DhcpServerInDC { return $fakeDhcpServersAbsent; } + + $result = Test-TargetResource @testAbsentParams + + $result -is [System.Boolean] | Should Be $true; + } + + } + #endregion Function Test-TargetResource + + #region Function Set-TargetResource + Describe "$($Global:DSCResourceName)\Set-TargetResource" { + Mock Assert-Module { }; + + It 'Calls Add-DhcpServerInDc when Ensure is Present' { + Mock Add-DhcpServerInDC { } + + Set-TargetResource @testPresentParams; + + Assert-MockCalled Add-DhcpServerInDC -Scope It; + } + It 'Calls Remove-DhcpServerInDc when Ensure is Present' { + Mock Get-DhcpServerInDC { return $fakeDhcpServersPresent; } + Mock Remove-DhcpServerInDC { } + + Set-TargetResource @testAbsentParams; + + Assert-MockCalled Remove-DhcpServerInDC -Scope It; + } + + } + #endregion Function Set-TargetResource + + #region Function Get-IPv4Address + Describe "$($Global:DSCResourceName)\Get-IPv4Address" { + + It 'Returns a IPv4 address' { + $result = Get-IPv4Address; + + $result -match '\d+\.\d+\.\d+\.\d+' | Should Be $true; + } + + } + #endregion Function Get-IPv4Address + + #region Function Get-Hostname + Describe "$($Global:DSCResourceName)\Get-Hostname" { + + It 'Returns at least the current NetBIOS name' { + $hostname = [System.Net.Dns]::GetHostname(); + + $result = Get-Hostname; + + $result -match $hostname | Should Be $true; + } + + } + #endregion Function Get-Hostname + + } #end InModuleScope + +} +finally +{ + #region FOOTER + Restore-TestEnvironment -TestEnvironment $TestEnvironment + #endregion +} diff --git a/deployment/dsc/azshcihost/xDhcpServer/2.0.0.0/Tests/Unit/MSFT_xDhcpServerClass.Tests.ps1 b/deployment/dsc/azshcihost/xDhcpServer/2.0.0.0/Tests/Unit/MSFT_xDhcpServerClass.Tests.ps1 new file mode 100644 index 0000000..18cffec --- /dev/null +++ b/deployment/dsc/azshcihost/xDhcpServer/2.0.0.0/Tests/Unit/MSFT_xDhcpServerClass.Tests.ps1 @@ -0,0 +1,167 @@ +$Global:DSCModuleName = 'xDhcpServer' +$Global:DSCResourceName = 'MSFT_xDhcpServerClass' + +#region HEADER +[String] $moduleRoot = Split-Path -Parent (Split-Path -Parent (Split-Path -Parent $Script:MyInvocation.MyCommand.Path)) +if ( (-not (Test-Path -Path (Join-Path -Path $moduleRoot -ChildPath 'DSCResource.Tests'))) -or ` + (-not (Test-Path -Path (Join-Path -Path $moduleRoot -ChildPath 'DSCResource.Tests\TestHelper.psm1'))) ) +{ + & git @('clone','https://github.com/PowerShell/DscResource.Tests.git',(Join-Path -Path $moduleRoot -ChildPath '\DSCResource.Tests\')) +} +else +{ + & git @('-C',(Join-Path -Path $moduleRoot -ChildPath '\DSCResource.Tests\'),'pull') +} +Import-Module (Join-Path -Path $moduleRoot -ChildPath 'DSCResource.Tests\TestHelper.psm1') -Force +$TestEnvironment = Initialize-TestEnvironment ` + -DSCModuleName $Global:DSCModuleName ` + -DSCResourceName $Global:DSCResourceName ` + -TestType Unit +#endregion + +# TODO: Other Optional Init Code Goes Here... + +# Begin Testing +try +{ + + #region Pester Tests + + # The InModuleScope command allows you to perform white-box unit testing on the internal + # (non-exported) code of a Script Module. + InModuleScope $Global:DSCResourceName { + + ## Mock missing functions + function Get-DhcpServerv4Class { } + function Add-DhcpServerv4Class { } + function Set-DhcpServerv4Class { } + function Remove-DhcpServerv4Class { } + + + + #region Pester Test Initialization + + $testClassName = 'Test Class'; + $testClassType = 'Vendor'; + $testAsciiData = 'test data'; + $testClassDescription = 'test class description'; + $testClassAddressFamily = 'IPv4'; + $testEnsure = 'Present' + + + $testParams = @{ + Name = $testClassName; + Type = $testClassType; + AsciiData = $testAsciiData; + AddressFamily = 'IPv4' + Description = $testClassDescription + #Ensure = $testEnsure + } + + $fakeDhcpServerClass = [PSCustomObject] @{ + 'Name'=$testClassName; + 'Type'=$testClassType + 'AsciiData' = $testAsciiData + 'Description' = $testClassDescription + 'AddressFamily' = $testClassAddressFamily + } + + + #endregion + + #region Function Get-TargetResource + Describe "$($Global:DSCResourceName)\Get-TargetResource" { + + Mock Assert-Module -ParameterFilter { $ModuleName -eq 'DHCPServer' } { } + + + It 'Calls "Assert-Module" to ensure "DHCPServer" module is available' { + Mock Get-DhcpServerv4Class { return $fakeDhcpServerClass; } + + $result = Get-TargetResource @testParams -Ensure Present; + + Assert-MockCalled Assert-Module -ParameterFilter { $ModuleName -eq 'DHCPServer' } -Scope It; + } + + + It 'Returns a "System.Collections.Hashtable" object type' { + Mock Get-DhcpServerv4Class { return $fakeDhcpServerClass; } + $result = Get-TargetResource @testParams -Ensure Present; + $result -is [System.Collections.Hashtable] | Should Be $true; + } + } + #endregion Function Get-TargetResource + + #region Function Test-TargetResource + Describe "$($Global:DSCResourceName)\Test-TargetResource" { + + Mock Assert-Module -ParameterFilter { $ModuleName -eq 'DHCPServer' } { } + + It 'Returns a "System.Boolean" object type' { + Mock Get-DhcpServerv4Class { return $fakeDhcpServerClass; } + + $result = Test-TargetResource @testParams -Ensure Present; + + $result -is [System.Boolean] | Should Be $true; + } + + It 'Passes when all parameters are correct' { + Mock Get-DhcpServerv4Class { return $fakeDhcpServerClass; } + + $result = Test-TargetResource @testParams -Ensure Present; + + $result | Should Be $true; + } + + } + #endregion Function Test-TargetResource + + #region Function Set-TargetResource + Describe "$($Global:DSCResourceName)\Set-TargetResource" { + + Mock Assert-Module -ParameterFilter { $ModuleName -eq 'DHCPServer' } { } + + It 'Calls "Add-DhcpServerv4Class" when "Ensure" = "Present" and class does not exist' { + Mock Set-DhcpServerv4Class { } + Mock Add-DhcpServerv4Class { } + + Set-TargetResource @testParams -Ensure Present; + + Assert-MockCalled Add-DhcpServerv4Class -Scope It; + } + + + It 'Calls "Remove-DhcpServerv4Class" when "Ensure" = "Absent" and scope does exist' { + + Mock Get-DhcpServerv4Class { return $fakeDhcpServerClass; } + Mock Remove-DhcpServerv4Class { } + + Set-TargetResource @testParams -Ensure 'Absent'; + + Assert-MockCalled Remove-DhcpServerv4Class -Scope It; + } + + + It 'Calls Set-DhcpServerv4Class when asciidata changes' { + + Mock Get-DhcpServerv4Class { return $fakeDhcpServerClass; } + Mock Set-DhcpServerv4Class { } + $testParams.AsciiData = 'differentdata' + Set-TargetResource @testParams -Ensure 'Present'; + + Assert-MockCalled Set-DhcpServerv4Class -Scope It; + } + + + }#End region Function Set-TargetResource + + + } #end InModuleScope + +} +finally +{ + #region FOOTER + Restore-TestEnvironment -TestEnvironment $TestEnvironment + #endregion +} diff --git a/deployment/dsc/azshcihost/xDhcpServer/2.0.0.0/Tests/Unit/MSFT_xDhcpServerOption.tests.ps1 b/deployment/dsc/azshcihost/xDhcpServer/2.0.0.0/Tests/Unit/MSFT_xDhcpServerOption.tests.ps1 new file mode 100644 index 0000000..83c828b --- /dev/null +++ b/deployment/dsc/azshcihost/xDhcpServer/2.0.0.0/Tests/Unit/MSFT_xDhcpServerOption.tests.ps1 @@ -0,0 +1,368 @@ +$Global:DSCModuleName = 'xDhcpServer' # Example xNetworking +$Global:DSCResourceName = 'MSFT_xDhcpServerOption' # Example MSFT_xFirewall + +#region HEADER +[String] $moduleRoot = Split-Path -Parent (Split-Path -Parent (Split-Path -Parent $Script:MyInvocation.MyCommand.Path)) +if ( (-not (Test-Path -Path (Join-Path -Path $moduleRoot -ChildPath 'DSCResource.Tests'))) -or ` + (-not (Test-Path -Path (Join-Path -Path $moduleRoot -ChildPath 'DSCResource.Tests\TestHelper.psm1'))) ) +{ + & git @('clone','https://github.com/PowerShell/DscResource.Tests.git',(Join-Path -Path $moduleRoot -ChildPath '\DSCResource.Tests\')) +} +else +{ + & git @('-C',(Join-Path -Path $moduleRoot -ChildPath '\DSCResource.Tests\'),'pull') +} +Import-Module (Join-Path -Path $moduleRoot -ChildPath 'DSCResource.Tests\TestHelper.psm1') -Force +$TestEnvironment = Initialize-TestEnvironment ` + -DSCModuleName $Global:DSCModuleName ` + -DSCResourceName $Global:DSCResourceName ` + -TestType Unit +#endregion + +# Begin Testing + +try +{ + InModuleScope $Global:DSCResourceName { + + #region Pester Test Initialization + # TODO: Optopnal Load Mock for use in Pester tests here... + #endregion + + $testScopeID = '192.168.1.0'; + $testDnsServerIPAddress = '192.168.1.10'; + $testDnsDomain = 'contoso.com'; + $testRouter = '192.168.1.1'; + + $testParams = @{ + ScopeID = $testScopeID; + DnsServerIPAddress = $testDnsServerIPAddress; + } + + $fakeDhcpServerv4Option = [PSCustomObject] @{ + ScopeID = $testScopeID; + DnsDomain = $testDnsDomain; + AddressFamily = 'IPv4'; + DnsServerIPAddress = $testDnsServerIPAddress; + Router = $testRouter; + } + + $fakeDhcpServerv4Scope = [PSCustomObject] @{ + ScopeID = $testScopeID; + } + + #region Function Get-TargetResource + Describe "$($Global:DSCResourceName)\Get-TargetResource" { + + It 'Returns all properties' { + Mock Get-DhcpServerv4Scope { return $fakeDhcpServerv4Scope; } + Mock Get-DhcpServerv4OptionValue { return $fakeDhcpServerv4Option } + $result = Get-TargetResource @testParams; + + $missingCount = + ( + $fakeDhcpServerv4Option.psobject.properties.ForEach{ + $result.ContainsKey($_.Name) + } | Where-Object { -not $_ } | Measure-Object + ).Count + + $missingCount | Should Be 0; + } + } + #endregion Function Get-TargetResource + + #region Function ValidateResourceProperties + Describe "$($Global:DSCResourceName)\ValidateResourceProperties" { + + $dnsDomainName = 'contoso.com' + $dnsIpAddress = @('2.1.1.2','2.1.1.3') + $routeripAddress = '1.1.1.2' + Mock -CommandName Set-DhcpServerv4OptionValue -ModuleName MSFT_xDhcpServerOption -MockWith { } + + # Absent removes the whole option, so this is not new to this issue. + # So not currently testing Absent and Apply = $true + foreach($params in @(@{Ensure='Present';Apply=$false},@{Ensure='Absent';Apply=$false},@{Ensure='Present';Apply=$true})) + { + It "Return true when DNS Server scalar match, apply: $($params.Apply), Ensure: $($params.Ensure)" { + Mock -CommandName Get-DhcpServerv4OptionValue -ModuleName MSFT_xDhcpServerOption -MockWith { + return @(new-object psobject -property @{OptionId=6;Value=$dnsIpAddress[1]}) + } + + $expectedReturn = $true + if($params.Ensure -eq 'Absent') + { + $expectedReturn = $false + } + if($params.Apply) + { + $expectedReturn = $null + } + $result = ValidateResourceProperties @params -scopeId '1.1.1.0' -DnsServerIPAddress $dnsIpAddress[1] -Verbose + + $result | should be $expectedReturn + Assert-MockCalled -ModuleName MSFT_xDhcpServerOption -commandName Get-DhcpServerv4OptionValue -Scope It + Assert-MockCalled -ModuleName MSFT_xDhcpServerOption -commandName set-DhcpServerv4OptionValue -Exactly 0 -Scope It + } + + It "Return true when DNS Server array match, apply: $($params.Apply), Ensure: $($params.Ensure)" { + Mock -CommandName Get-DhcpServerv4OptionValue -ModuleName MSFT_xDhcpServerOption -MockWith { + return @(new-object psobject -property @{OptionId=6;Value=$dnsIpAddress}) + } + + $expectedReturn = $true + if($params.Ensure -eq 'Absent') + { + $expectedReturn = $false + } + if($params.Apply) + { + $expectedReturn = $null + } + $result = ValidateResourceProperties @params -scopeId '1.1.1.0' -DnsServerIPAddress $dnsIpAddress -Verbose + + $result | should be $expectedReturn + Assert-MockCalled -ModuleName MSFT_xDhcpServerOption -commandName Get-DhcpServerv4OptionValue -Scope It + Assert-MockCalled -ModuleName MSFT_xDhcpServerOption -commandName set-DhcpServerv4OptionValue -Exactly 0 -Scope It + } + + It "Return false when DNS Server mismatch, apply: $($params.Apply), Ensure: $($params.Ensure)" { + Mock -CommandName Get-DhcpServerv4OptionValue -ModuleName MSFT_xDhcpServerOption -MockWith { + return @(new-object psobject -property @{OptionId=6;Value=$dnsIpAddress}) + } + + $expectedReturn = $false + $setMockCalledParams = @{} + if($params.Apply) + { + $expectedReturn = $null + } + else + { + $setMockCalledParams.Add('Exactly',$true) + $setMockCalledParams.Add('Times',0) + } + $result = ValidateResourceProperties @params -scopeId '1.1.1.0' -DnsServerIPAddress '1.2.2.1' -Verbose + + $result | should be $expectedReturn + Assert-MockCalled -ModuleName MSFT_xDhcpServerOption -commandName Get-DhcpServerv4OptionValue -Scope It + Assert-MockCalled -ModuleName MSFT_xDhcpServerOption -commandName set-DhcpServerv4OptionValue @setMockCalledParams -Scope It + } + + It "Return false when DNS Server empty, apply: $($params.Apply), Ensure: $($params.Ensure)" { + Mock -CommandName Get-DhcpServerv4OptionValue -ModuleName MSFT_xDhcpServerOption -MockWith { + return @(new-object psobject -property @{OptionId=15;Value=$dnsDomainName}) + } + + $expectedReturn = $false + $setMockCalledParams = @{} + if($params.Apply) + { + $expectedReturn = $null + } + else + { + $setMockCalledParams.Add('Exactly',$true) + $setMockCalledParams.Add('Times',0) + } + $result = ValidateResourceProperties @params -scopeId '1.1.1.0' -DnsServerIPAddress '1.2.2.1' -Verbose + + $result | should be $expectedReturn + Assert-MockCalled -ModuleName MSFT_xDhcpServerOption -commandName Get-DhcpServerv4OptionValue -Scope It + Assert-MockCalled -ModuleName MSFT_xDhcpServerOption -commandName set-DhcpServerv4OptionValue @setMockCalledParams -Scope It + } + + It "Return true when DNS domain name match, apply: $($params.Apply), Ensure: $($params.Ensure)" { + Mock -CommandName Get-DhcpServerv4OptionValue -ModuleName MSFT_xDhcpServerOption -MockWith { + return @(new-object psobject -property @{OptionId=15;Value=$dnsDomainName}) + } + + $expectedReturn = $true + if($params.Ensure -eq 'Absent') + { + $expectedReturn = $false + } + if($params.Apply) + { + $expectedReturn = $null + } + $result = ValidateResourceProperties @params -scopeId '1.1.1.0' -DnsDomain $dnsDomainName -Verbose + + $result | should be $expectedReturn + Assert-MockCalled -ModuleName MSFT_xDhcpServerOption -commandName Get-DhcpServerv4OptionValue -Scope It + Assert-MockCalled -ModuleName MSFT_xDhcpServerOption -commandName set-DhcpServerv4OptionValue -Exactly 0 -Scope It + } + + It "Return false when DNS domain name mismatch, apply: $($params.Apply), Ensure: $($params.Ensure)" { + Mock -CommandName Get-DhcpServerv4OptionValue -ModuleName MSFT_xDhcpServerOption -MockWith { + return @(new-object psobject -property @{OptionId=15;Value=$dnsDomainName}) + } + + $expectedReturn = $false + $setMockCalledParams = @{} + if($params.Apply) + { + $expectedReturn = $null + } + else + { + $setMockCalledParams.Add('Exactly',$true) + $setMockCalledParams.Add('Times',0) + } + $result = ValidateResourceProperties @params -scopeId '1.1.1.0' -DnsDomain 'wrong.com' -Verbose + + $result | should be $expectedReturn + Assert-MockCalled -ModuleName MSFT_xDhcpServerOption -commandName Get-DhcpServerv4OptionValue -Scope It + Assert-MockCalled -ModuleName MSFT_xDhcpServerOption -commandName set-DhcpServerv4OptionValue @setMockCalledParams -Scope It + } + + It "Return true when Router scalar match, apply: $($params.Apply), Ensure: $($params.Ensure)" { + Mock -CommandName Get-DhcpServerv4OptionValue -ModuleName MSFT_xDhcpServerOption -MockWith { + return @(new-object psobject -property @{OptionId=3;Value=$routeripAddress}) + } + + $expectedReturn = $true + if($params.Ensure -eq 'Absent') + { + $expectedReturn = $false + } + if($params.Apply) + { + $expectedReturn = $null + } + $result = ValidateResourceProperties @params -scopeId '1.1.1.0' -Router $routeripAddress -Verbose + + $result | should be $expectedReturn + Assert-MockCalled -ModuleName MSFT_xDhcpServerOption -commandName Get-DhcpServerv4OptionValue -Scope It + Assert-MockCalled -ModuleName MSFT_xDhcpServerOption -commandName set-DhcpServerv4OptionValue -Exactly 0 -Scope It + } + + It "Return true when Router array match, apply: $($params.Apply), Ensure: $($params.Ensure)" { + Mock -CommandName Get-DhcpServerv4OptionValue -ModuleName MSFT_xDhcpServerOption -MockWith { + return @(new-object psobject -property @{OptionId=3;Value=$routeripAddress}) + } + + $expectedReturn = $true + if($params.Ensure -eq 'Absent') + { + $expectedReturn = $false + } + if($params.Apply) + { + $expectedReturn = $null + } + $result = ValidateResourceProperties @params -scopeId '1.1.1.0' -Router $routeripAddress -Verbose + + $result | should be $expectedReturn + Assert-MockCalled -ModuleName MSFT_xDhcpServerOption -commandName Get-DhcpServerv4OptionValue -Scope It + Assert-MockCalled -ModuleName MSFT_xDhcpServerOption -commandName set-DhcpServerv4OptionValue -Exactly 0 -Scope It + } + + It "Return false when Router scalar mismatch, apply: $($params.Apply), Ensure: $($params.Ensure)" { + Mock -CommandName Get-DhcpServerv4OptionValue -ModuleName MSFT_xDhcpServerOption -MockWith { + return @(new-object psobject -property @{OptionId=3;Value=$routeripAddress}) + } + + $expectedReturn = $false + $setMockCalledParams = @{} + if($params.Apply) + { + $expectedReturn = $null + } + else + { + $setMockCalledParams.Add('Exactly',$true) + $setMockCalledParams.Add('Times',0) + } + $result = ValidateResourceProperties @params -scopeId '1.1.1.0' -Router '1.1.1.3' -Verbose + + $result | should be $expectedReturn + Assert-MockCalled -ModuleName MSFT_xDhcpServerOption -commandName Get-DhcpServerv4OptionValue -Scope It + Assert-MockCalled -ModuleName MSFT_xDhcpServerOption -commandName set-DhcpServerv4OptionValue @setMockCalledParams -Scope It + } + + It "Return false when Router array mismatch, apply: $($params.Apply), Ensure: $($params.Ensure)" { + Mock -CommandName Get-DhcpServerv4OptionValue -ModuleName MSFT_xDhcpServerOption -MockWith { + return @(new-object psobject -property @{OptionId=3;Value=$routeripAddress}) + } + + $expectedReturn = $false + $setMockCalledParams = @{} + if($params.Apply) + { + $expectedReturn = $null + } + else + { + $setMockCalledParams.Add('Exactly',$true) + $setMockCalledParams.Add('Times',0) + } + + + $result = ValidateResourceProperties @params -scopeId '1.1.1.0' -Router @('1.1.1.2','1.1.1.4') -Verbose + + $result | should be $expectedReturn + Assert-MockCalled -ModuleName MSFT_xDhcpServerOption -commandName Get-DhcpServerv4OptionValue -Scope It + Assert-MockCalled -ModuleName MSFT_xDhcpServerOption -commandName set-DhcpServerv4OptionValue @setMockCalledParams -Scope It + } + + It "Return false when Router array extra element, apply: $($params.Apply), Ensure: $($params.Ensure)" { + Mock -CommandName Get-DhcpServerv4OptionValue -ModuleName MSFT_xDhcpServerOption -MockWith { + return @(new-object psobject -property @{OptionId=3;Value=$routeripAddress}) + } + + $expectedReturn = $false + $setMockCalledParams = @{} + if($params.Apply) + { + $expectedReturn = $null + } + else + { + $setMockCalledParams.Add('Exactly',$true) + $setMockCalledParams.Add('Times',0) + } + + $result = ValidateResourceProperties @params -scopeId '1.1.1.0'-Router @('1.1.1.2','1.1.1.3', '1.1.1.4') -Verbose + + $result | should be $expectedReturn + Assert-MockCalled -ModuleName MSFT_xDhcpServerOption -commandName Get-DhcpServerv4OptionValue -Scope It + Assert-MockCalled -ModuleName MSFT_xDhcpServerOption -commandName set-DhcpServerv4OptionValue @setMockCalledParams -Scope It + } + + It "Return false when Router array missing element, apply: $($params.Apply), Ensure: $($params.Ensure)" { + Mock -CommandName Get-DhcpServerv4OptionValue -ModuleName MSFT_xDhcpServerOption -MockWith { + return @(new-object psobject -property @{OptionId=3;Value=$routeripAddress}) + } + + $expectedReturn = $false + $setMockCalledParams = @{} + if($params.Apply) + { + $expectedReturn = $null + } + else + { + $setMockCalledParams.Add('Exactly',$true) + $setMockCalledParams.Add('Times',0) + } + $result = ValidateResourceProperties @params -scopeId '1.1.1.0' -Router @('1.1.1.2','1.1.1.3') -Verbose + + $result | should be $expectedReturn + Assert-MockCalled -ModuleName MSFT_xDhcpServerOption -commandName Get-DhcpServerv4OptionValue -Scope It + Assert-MockCalled -ModuleName MSFT_xDhcpServerOption -commandName set-DhcpServerv4OptionValue @setMockCalledParams -Scope It + } + } + #endregion + + } #endregion InModuleScope + + } + #endregion + +} +finally +{ + #region FOOTER + Restore-TestEnvironment -TestEnvironment $TestEnvironment + #endregion +} diff --git a/deployment/dsc/azshcihost/xDhcpServer/2.0.0.0/Tests/Unit/MSFT_xDhcpServerOptionDefinition.Tests.ps1 b/deployment/dsc/azshcihost/xDhcpServer/2.0.0.0/Tests/Unit/MSFT_xDhcpServerOptionDefinition.Tests.ps1 new file mode 100644 index 0000000..8fb3cf2 --- /dev/null +++ b/deployment/dsc/azshcihost/xDhcpServer/2.0.0.0/Tests/Unit/MSFT_xDhcpServerOptionDefinition.Tests.ps1 @@ -0,0 +1,229 @@ +$Global:DSCModuleName = 'xDhcpServer' +$Global:DSCResourceName = 'MSFT_xDhcpServerOptionDefinition' + +#region HEADER + +# Unit Test Template Version: 1.2.1 +$script:moduleRoot = Split-Path -Parent (Split-Path -Parent $PSScriptRoot) +if ( (-not (Test-Path -Path (Join-Path -Path $script:moduleRoot -ChildPath 'DSCResource.Tests'))) -or ` + (-not (Test-Path -Path (Join-Path -Path $script:moduleRoot -ChildPath 'DSCResource.Tests\TestHelper.psm1'))) ) +{ + & git @('clone','https://github.com/PowerShell/DscResource.Tests.git',(Join-Path -Path $script:moduleRoot -ChildPath 'DSCResource.Tests')) +} + +Import-Module -Name (Join-Path -Path $script:moduleRoot -ChildPath (Join-Path -Path 'DSCResource.Tests' -ChildPath 'TestHelper.psm1')) -Force + + +$TestEnvironment = Initialize-TestEnvironment ` + -DSCModuleName 'xDhcpServer' ` + -DSCResourceName 'MSFT_xDhcpServerOptionDefinition' ` + -TestType Unit + +#endregion HEADER + + +function Invoke-TestSetup { +} + +function Invoke-TestCleanup { + Restore-TestEnvironment -TestEnvironment $TestEnvironment +} + +# Begin Testing + +try +{ + Invoke-TestSetup + + InModuleScope 'MSFT_xDhcpServerOptionDefinition' { + + $optionId = 22 + $name = 'Test name' + $addressFamily = 'IPv4' + $description = 'Test Description' + $type = 'IPv4Address' + $vendorClass = '' + $multiValued = $false + + $testParams = @{ + OptionId = $optionId + Name = $name + AddressFamily = $addressFamily + Description = $description + Type = $type + VendorClass = $vendorClass + MultiValued = $multiValued + } + + $fakeDhcpServerv4OptionDefinition = [PSCustomObject] @{ + OptionId = $optionId + Name = $name + AddressFamily = $addressFamily + Description = $description + Type = $type + VendorClass = $vendorClass + MultiValued = $multiValued + } + + Describe "xDhcpServer\Get-TargetResource" { + + Mock Assert-Module -ParameterFilter { $ModuleName -eq 'DHCPServer' } { } + Mock Get-DhcpServerv4OptionDefinition { return $fakeDhcpServerv4OptionDefinition } + Mock Set-DhcpServerv4OptionDefinition { } + Mock Add-DhcpServerv4OptionDefinition { } + Mock Remove-DhcpServerv4OptionDefinition { } + + It 'Should call "Assert-Module" to ensure "DHCPServer" module is available' { + + $result = Get-TargetResource -OptionId $OptionId -Name $Name -Type $Type -VendorClass $VendorClass -AddressFamily IPv4 -Ensure Present; + Assert-MockCalled -CommandName Assert-Module -Scope It + } + + It 'Returns a "System.Collections.Hashtable" object type' { + + $result = Get-TargetResource -OptionId $OptionId -Name $Name -Type $Type -VendorClass $VendorClass -AddressFamily IPv4 -Ensure Present; + $result -is [System.Collections.Hashtable] | Should Be $true; + } + } + + Describe "xDhcpServer\Test-TargetResource" { + + Mock Assert-Module -ParameterFilter { $ModuleName -eq 'DHCPServer' } { } + Mock Get-DhcpServerv4OptionDefinition { return $fakeDhcpServerv4OptionDefinition } + Mock Set-DhcpServerv4OptionDefinition { } + Mock Add-DhcpServerv4OptionDefinition { } + Mock Remove-DhcpServerv4OptionDefinition { } + + It 'Returns a "System.Boolean" object type' { + Mock Get-DhcpServerv4OptionDefinition { return $fakeDhcpServerv4OptionDefinition; } + + $result = Test-TargetResource @testParams -Ensure Present; + $result | Should BeOfType [System.Boolean] + } + + It 'Passes when all parameters are correct' { + Mock Get-DhcpServerv4OptionDefinition { return $fakeDhcpServerv4OptionDefinition; } + + $result = Test-TargetResource @testParams -Ensure Present; + $result | Should Be $true; + } + } + + Describe "xDhcpServer\Set-TargetResource" { + + Mock Assert-Module -ParameterFilter { $ModuleName -eq 'DHCPServer' } { } + Mock Set-DhcpServerv4OptionDefinition { } + Mock Add-DhcpServerv4OptionDefinition { } + Mock Remove-DhcpServerv4OptionDefinition { } + + It 'Should call "Add-DhcpServerv4OptionDefinition" when "Ensure" = "Present" and definition does not exist' { + + Mock Get-DhcpServerv4OptionDefinition { return $null } + + $TempParams = $testParams.Clone() + $TempParams.OptionId = 2 + Set-TargetResource @TempParams -Ensure 'Present' + + Assert-MockCalled Add-DhcpServerv4OptionDefinition -Scope It + } + + It 'Should call "Remove-DhcpServerv4OptionDefinition" when "Ensure" = "Absent" and Definition does exist' { + + Mock Get-DhcpServerv4OptionDefinition { return $fakeDhcpServerv4OptionDefinition } + + Set-TargetResource @testParams -Ensure 'Absent' + Assert-MockCalled -CommandName Remove-DhcpServerv4OptionDefinition -Scope It + } + + It 'Should call Set-DhcpServerv4OptionDefinition when "Ensure" = "Present" and Name or Description has changed' { + + Mock Get-DhcpServerv4OptionDefinition { return $fakeDhcpServerv4OptionDefinition } + + $TempParams = $testParams.Clone() + $TempParams.Description = 'New Description' + Set-TargetResource @TempParams -Ensure 'Present' + + Assert-MockCalled -CommandName Set-DhcpServerv4OptionDefinition -Scope It + } + + It 'Should call "Remove-DhcpServerv4OptionDefinition" and then "Add-DhcpServerv4OptionDefinition" when "Ensure" = "Present" and Type, MultiValued, VendorClass has changed' { + + Mock Get-DhcpServerv4OptionDefinition { return $fakeDhcpServerv4OptionDefinition } + + $TempParams = $testParams.Clone() + $TempParams.Type = 'Byte' + Set-TargetResource @TempParams -Ensure 'Present' + + Assert-MockCalled -CommandName Remove-DhcpServerv4OptionDefinition -Scope It + Assert-MockCalled -CommandName Add-DhcpServerv4OptionDefinition -Scope It + } + + + It 'Should call "Remove-DhcpServerv4OptionDefinition" and then "Add-DhcpServerv4OptionDefinition" when "Ensure" = "Present" and Type has changed' { + + Mock Get-DhcpServerv4OptionDefinition { return $fakeDhcpServerv4OptionDefinition } + + $TempParams = $testParams.Clone() + $TempParams.Type = 'Byte' + Set-TargetResource @tempParams -Ensure 'Present' + + Assert-MockCalled -CommandName Remove-DhcpServerv4OptionDefinition -Scope It + Assert-MockCalled -CommandName Add-DhcpServerv4OptionDefinition -Scope It + } + + + It 'Should call "Remove-DhcpServerv4OptionDefinition" and then "Add-DhcpServerv4OptionDefinition" when "Ensure" = "Present" and MultiValued has changed' { + + Mock Get-DhcpServerv4OptionDefinition { return $fakeDhcpServerv4OptionDefinition } + + $TempParams = $testParams.Clone() + $TempParams.MultiValued = $true + Set-TargetResource @TempParams -Ensure 'Present' + + Assert-MockCalled -CommandName Remove-DhcpServerv4OptionDefinition -Scope It -Times 1 -Exactly + Assert-MockCalled -CommandName Add-DhcpServerv4OptionDefinition -Scope It -Times 1 -Exactly + } + + It 'Should call "Remove-DhcpServerv4OptionDefinition" and then "Add-DhcpServerv4OptionDefinition" when "Ensure" = "Present" and VendorClass has changed' { + + Mock Get-DhcpServerv4OptionDefinition { return $fakeDhcpServerv4OptionDefinition } + + $TempParams = $testParams.Clone() + $TempParams.VendorClass = 'NewVendorClass' + Set-TargetResource @TempParams -Ensure 'Present' + + Assert-MockCalled -CommandName Remove-DhcpServerv4OptionDefinition -Scope It -Times 1 -Exactly + Assert-MockCalled -CommandName Add-DhcpServerv4OptionDefinition -Scope It -Times 1 -Exactly + } + + + It 'Should call "Remove-DhcpServerv4OptionDefinition" and then "Add-DhcpServerv4OptionDefinition" when "Ensure" = "Present" and VendorClass and Description has changed' { + + Mock Get-DhcpServerv4OptionDefinition { return $fakeDhcpServerv4OptionDefinition } + + $TempParams = $testParams.Clone() + $TempParams.VendorClass = 'NewVendorClass' + $TempParams.Description = 'New Description' + Set-TargetResource @TempParams -Ensure 'Present' + + Assert-MockCalled -CommandName Remove-DhcpServerv4OptionDefinition -Scope It -Times 1 -Exactly + Assert-MockCalled -CommandName Add-DhcpServerv4OptionDefinition -Scope It -Times 1 -Exactly + } + + It 'Should call "Set-DhcpServerv4OptionDefinition" when "Ensure" = "Present" and Description has changed' { + + Mock Get-DhcpServerv4OptionDefinition { return $fakeDhcpServerv4OptionDefinition } + + $TempParams = $testParams.Clone() + $TempParams.Description = 'New Description' + Set-TargetResource @testParams -Ensure 'Present' + + Assert-MockCalled -CommandName Set-DhcpServerv4OptionDefinition -Scope It + } + } + } +} +finally +{ + Invoke-TestCleanup +} diff --git a/deployment/dsc/azshcihost/xDhcpServer/2.0.0.0/Tests/Unit/MSFT_xDhcpServerScope.Tests.ps1 b/deployment/dsc/azshcihost/xDhcpServer/2.0.0.0/Tests/Unit/MSFT_xDhcpServerScope.Tests.ps1 new file mode 100644 index 0000000..3058a9f --- /dev/null +++ b/deployment/dsc/azshcihost/xDhcpServer/2.0.0.0/Tests/Unit/MSFT_xDhcpServerScope.Tests.ps1 @@ -0,0 +1,295 @@ +$Global:DSCModuleName = 'xDhcpServer' +$Global:DSCResourceName = 'MSFT_xDhcpServerScope' + +#region HEADER +[String] $moduleRoot = Split-Path -Parent (Split-Path -Parent (Split-Path -Parent $Script:MyInvocation.MyCommand.Path)) +if ( (-not (Test-Path -Path (Join-Path -Path $moduleRoot -ChildPath 'DSCResource.Tests'))) -or + (-not (Test-Path -Path (Join-Path -Path $moduleRoot -ChildPath 'DSCResource.Tests\TestHelper.psm1'))) ) +{ + & git @('clone','https://github.com/PowerShell/DscResource.Tests.git',(Join-Path -Path $moduleRoot -ChildPath '\DSCResource.Tests\')) +} +else +{ + & git @('-C',(Join-Path -Path $moduleRoot -ChildPath '\DSCResource.Tests\'),'pull') +} +Import-Module (Join-Path -Path $moduleRoot -ChildPath 'DSCResource.Tests\TestHelper.psm1') -Force +$TestEnvironment = Initialize-TestEnvironment ` + -DSCModuleName $Global:DSCModuleName ` + -DSCResourceName $Global:DSCResourceName ` + -TestType Unit +#endregion + +# Begin Testing +try +{ + #region Pester Tests + + # The InModuleScope command allows you to perform white-box unit testing on the internal + # (non-exported) code of a Script Module. + InModuleScope $Global:DSCResourceName { + + #region Pester Test Initialization + # TODO: Optional Load Mock for use in Pester tests here... + #endregion + + $testScopeName = 'Test Scope' + $testScopeID = '192.168.1.0' + $testIPStartRange = '192.168.1.10' + $testIPEndRange = '192.168.1.99' + $testSubnetMask = '255.255.255.0' + $testState = 'Active' + $testLeaseDuration = New-TimeSpan -Days 8 + $testDescription = 'Scope description' + $testAddressFamily = 'IPv4' + + $testParams = @{ + ScopeId = $testScopeID + Name = $testScopeName + IPStartRange = $testIPStartRange + IPEndRange = $testIPEndRange + SubnetMask = $testSubnetMask + } + + $fakeDhcpServerv4Scope = [PSCustomObject] @{ + ScopeID = $testScopeID + Name = $testScopeName + StartRange = $testIPStartRange + EndRange = $testIPEndRange + Description = $testDescription + SubnetMask = $testSubnetMask + LeaseDuration = $testLeaseDuration + State = $testState + AddressFamily = $testAddressFamily + } + + #region Function Get-TargetResource + Describe "$($Global:DSCResourceName)\Get-TargetResource" { + + Mock Assert-Module -ParameterFilter { $ModuleName -eq 'DHCPServer' } { } + Mock Assert-ScopeParameter -ParameterFilter { + $ScopeId -eq $testScopeID -and + $SubnetMask -eq $testSubnetMask -and + $IPStartRange -eq $testIPStartRange -and + $IPEndRange -eq $testIPEndRange -and + $AddressFamily -eq $testAddressFamily + } + + It 'Should call "Assert-Module" to ensure "DHCPServer" module is available' { + Mock Get-DhcpServerv4Scope { return $fakeDhcpServerv4Scope } + + $result = Get-TargetResource @testParams + + Assert-MockCalled Assert-Module -ParameterFilter { $ModuleName -eq 'DHCPServer' } -Scope It + } + + It 'Should call "Assert-ScopeParameter" to ensure parameters passed are correct' { + Mock Get-DhcpServerv4Scope { return $fakeDhcpServerv4Scope } + $result = Get-TargetResource @testParams + Assert-MockCalled Assert-Module -Scope It + } + + It 'Should return a "System.Collections.Hashtable" object type' { + Mock Get-DhcpServerv4Scope { return $fakeDhcpServerv4Scope } + Get-TargetResource @testParams | Should -BeOfType System.Collections.Hashtable + } + + It 'Should return all information about existing scope with specified ScopeId' { + Mock Get-DhcpServerv4Scope { return $fakeDhcpServerv4Scope } + $result = Get-TargetResource @testParams + $result.Name | Should -Be $testScopeName + $result.IPStartRange | Should -Be $testIPStartRange + $result.IPEndRange | Should -Be $testIPEndRange + $result.SubnetMask | Should -Be $testSubnetMask + $result.Description | Should -Be $testDescription + $result.LeaseDuration | Should -Be $testLeaseDuration + $result.State | Should -Be $testState + $result.AddressFamily | Should -Be $testAddressFamily + $result.Ensure | Should -Be Present + } + + It 'Should return basic information about missing scope with specified ScopeId' { + Mock Get-DhcpServerv4Scope {} + $result = Get-TargetResource @testParams + $result.Name | Should -BeNullOrEmpty + $result.IPStartRange | Should -BeNullOrEmpty + $result.IPEndRange | Should -BeNullOrEmpty + $result.SubnetMask | Should -BeNullOrEmpty + $result.Description | Should -BeNullOrEmpty + $result.LeaseDuration | Should -BeNullOrEmpty + $result.State | Should -BeNullOrEmpty + $result.AddressFamily | Should -Be $testAddressFamily + $result.Ensure | Should -Be Absent + } + } + #endregion Function Get-TargetResource + + #region Function Test-TargetResource + Describe "$($Global:DSCResourceName)\Test-TargetResource" { + + Mock Assert-Module -ParameterFilter { $ModuleName -eq 'DHCPServer' } { } + + It 'Should return a "System.Boolean" object type' { + Mock Get-DhcpServerv4Scope { return $fakeDhcpServerv4Scope } + Test-TargetResource @testParams | Should -BeOfType System.Boolean + } + + It 'Should pass when all parameters are correct' { + Mock Get-DhcpServerv4Scope { return $fakeDhcpServerv4Scope } + Test-TargetResource @testParams | Should -BeTrue + } + + It 'Should pass when optional <Parameter> parameter is correct' { + param ( + $Parameter, + $Value + ) + Mock Get-DhcpServerv4Scope { return $fakeDhcpServerv4Scope } + $optionalParameters = @{ + $Parameter = $Value + } + Test-TargetResource @testParams @optionalParameters | Should -BeTrue + } -TestCases @( + @{ + Parameter = 'Description' + Value = $testDescription + } + @{ + Parameter = 'LeaseDuration' + Value = $testLeaseDuration.ToString() + } + @{ + Parameter = 'State' + Value = $testState + } + ) + + It 'Should pass when "Ensure" = "Absent" and scope does not exist' { + Mock Get-DhcpServerv4Scope { } + Test-TargetResource @testParams -Ensure 'Absent' | Should -BeTrue + } + + It 'Should fail when <parameter> parameter is incorrect' { + param ( + $Parameter, + $Value + ) + Mock Get-DhcpServerv4Scope { return $fakeDhcpServerv4Scope } + $testNameParams = $testParams.Clone() + $testNameParams[$Parameter] = $Value + Test-TargetResource @testNameParams | Should -BeFalse + } -TestCases @( + @{ + Parameter = 'Name' + Value = 'IncorrectName' + } + @{ + Parameter = 'IPStartRange' + Value = '192.168.1.1' + } + @{ + Parameter = 'IPEndRange' + Value = '192.168.1.254' + } + @{ + Parameter = 'SubnetMask' + Value = '255.255.255.128' + } + @{ + Parameter = 'Description' + Value = 'Wrong description' + } + @{ + Parameter = 'LeaseDuration' + Value = '08:00:00' + } + @{ + Parameter = 'State' + Value = 'Inactive' + } + @{ + Parameter = 'Ensure' + Value = 'Absent' + } + ) + } + #endregion + + #region Function Set-TargetResource + Describe "$($Global:DSCResourceName)\Set-TargetResource" { + + Mock Assert-Module -ParameterFilter { $ModuleName -eq 'DHCPServer' } { } + + It 'Should call "Add-DhcpServerv4Scope" when "Ensure" = "Present" and scope does not exist' { + Mock Get-DhcpServerv4Scope { } + Mock Add-DhcpServerv4Scope { } + + Set-TargetResource @testParams + + Assert-MockCalled Add-DhcpServerv4Scope -Scope It -Times 1 -Exactly -ParameterFilter { + $StartRange -eq $testIPStartRange -and + $EndRange -eq $testIPEndRange -and + $SubnetMask -eq $testSubnetMask -and + $Name -eq $testScopeName + } + } + + It 'Should call "Remove-DhcpServerv4Scope" when "Ensure" = "Absent" and scope does exist' { + Mock Get-DhcpServerv4Scope { return $fakeDhcpServerv4Scope } + Mock Remove-DhcpServerv4Scope { } + + Set-TargetResource @testParams -Ensure 'Absent' + + Assert-MockCalled Remove-DhcpServerv4Scope -Scope It -Times 1 -Exactly -ParameterFilter { $ScopeId -eq $testScopeID } + } + + It 'Should call "Set-DhcpServerv4Scope" when "Ensure" = "Present" and scope does exist' { + Mock Get-DhcpServerv4Scope { return $fakeDhcpServerv4Scope } + Mock Set-DhcpServerv4Scope { } + + Set-TargetResource @testParams -LeaseDuration '08:00:00' + + Assert-MockCalled Set-DhcpServerv4Scope -Scope It -Times 1 -Exactly -ParameterFilter { + $ScopeId -eq $testScopeID -and + $LeaseDuration -eq (New-TimeSpan -Hours 8) + } + } + + It 'Should call "Remove-DhcpServerv4Scope" when "Ensure" = "Present", scope does exist but "SubnetMask" is incorrect' { + Mock Get-DhcpServerv4Scope { return $fakeDhcpServerv4Scope } + Mock Remove-DhcpServerv4Scope { } + Mock Set-DhcpServerv4Scope { } + $testSubnetMaskParams = $testParams.Clone() + $testSubnetMaskParams['SubnetMask'] = '255.255.255.128' + + Set-TargetResource @testSubnetMaskParams + + Assert-MockCalled Remove-DhcpServerv4Scope -Scope It -Times 1 -Exactly -ParameterFilter { $ScopeId -eq $testScopeID } + Assert-MockCalled Add-DhcpServerv4Scope -Scope It -Times 1 -Exactly -ParameterFilter { + $StartRange -eq $testIPStartRange -and + $EndRange -eq $testIPEndRange -and + $SubnetMask -eq '255.255.255.128' -and + $Name -eq $testScopeName + } + + } + + } + #endregion + + #region Function Set-TargetResource + Describe "$($Global:DSCResourceName)\Validate-ResourceProperties" { + # TODO: Complete Tests... + } + #endregion + + } + #endregion +} +finally +{ + #region FOOTER + Restore-TestEnvironment -TestEnvironment $TestEnvironment + #endregion + + # TODO: Other Optional Cleanup Code Goes Here... +} diff --git a/deployment/dsc/azshcihost/xDhcpServer/2.0.0.0/xDhcpServer.psd1 b/deployment/dsc/azshcihost/xDhcpServer/2.0.0.0/xDhcpServer.psd1 new file mode 100644 index 0000000..02ca4f9 --- /dev/null +++ b/deployment/dsc/azshcihost/xDhcpServer/2.0.0.0/xDhcpServer.psd1 @@ -0,0 +1,64 @@ +@{ +# Version number of this module. +moduleVersion = '2.0.0.0' + +# ID used to uniquely identify this module +GUID = '286890c9-a6c3-4605-9cd5-03c8413c8325' + +# Author of this module +Author = 'Microsoft Corporation' + +# Company or vendor of this module +CompanyName = 'Microsoft Corporation' + +# Copyright statement for this module +Copyright = '(c) 2018 Microsoft Corporation. All rights reserved.' + +# Description of the functionality provided by this module +Description = 'Module with DSC Resources for DHCP Server area' + +# Minimum version of the Windows PowerShell engine required by this module +PowerShellVersion = '4.0' + +# Minimum version of the common language runtime (CLR) required by this module +# CLRVersion = '4.0' + +# Functions to export from this module +FunctionsToExport = '*' + +# Cmdlets to export from this module +CmdletsToExport = '*' + +# Private data to pass to the module specified in RootModule/ModuleToProcess. This may also contain a PSData hashtable with additional module metadata used by PowerShell. +PrivateData = @{ + + PSData = @{ + + # Tags applied to this module. These help with module discovery in online galleries. + Tags = @('DesiredStateConfiguration', 'DSC', 'DSCResourceKit', 'DSCResource') + + # A URL to the license for this module. + LicenseUri = 'https://github.com/PowerShell/xDhcpServer/blob/master/LICENSE' + + # A URL to the main website for this project. + ProjectUri = 'https://github.com/PowerShell/xDhcpServer' + + # A URL to an icon representing this module. + # IconUri = '' + + # ReleaseNotes of this module + ReleaseNotes = '* BREAKING CHANGE: Switch to ScopeId as a key property for xDhcpServerScope ([issue 43](https://github.com/PowerShell/xDhcpServer/issues/48). [Bartek Bielawski (@bielawb)](https://github.com/bielawb) + +' + + } # End of PSData hashtable + +} # End of PrivateData hashtable +} + + + + + + + diff --git a/deployment/dsc/azshcihost/xHyper-V/3.17.0.0/CHANGELOG.md b/deployment/dsc/azshcihost/xHyper-V/3.17.0.0/CHANGELOG.md new file mode 100644 index 0000000..d82d90d --- /dev/null +++ b/deployment/dsc/azshcihost/xHyper-V/3.17.0.0/CHANGELOG.md @@ -0,0 +1,198 @@ +# Change log for xHyper-V + +## Unreleased + +## 3.17.0.0 + +* MSFT_xVMNetworkAdapter: + * Added NetworkSettings to be able to statically set IPAddress. + * Added option for Vlan tagging. You can now setup a Network Adapeter as an access switch on a specific Vlan. + +## 3.16.0.0 + +* MSFT_xVMHyperV: + * Moved localization string data to own file. + * Fixed code styling issues. + * Fixed bug where StartupMemory was not evaluated in Test-TargetResource. + * Redo of abandoned PRs: + * [PR #148](https://github.com/PowerShell/xHyper-V/pull/148), Fixes [Issue #149](https://github.com/PowerShell/xHyper-V/issues/149). + * [PR #67](https://github.com/PowerShell/xHyper-V/pull/67), Fixes [Issue #145](https://github.com/PowerShell/xHyper-V/issues/145). + * Fixed Get throws error when NetworkAdapters are not attached or missing properties. + +## 3.15.0.0 + +* Explicitly removed extra hidden files from release package. + +## 3.14.0.0 + +* MSFT_xVMHost: + * Added support to Enable / Disable VM Live Migration. Fixes [Issue #155](https://github.com/PowerShell/xHyper-V/issues/155). + +## 3.13.0.0 + +* MSFT_xVMSwitch: + * Changed 'Id' parameter form read only to optional so the VMSwitch ID can be set on Windows Server 2016. This is important for SDN setups where the VMSwitch ID must remain the same when a Hyper-V host is re-installed. + * Update appveyor.yml to use the default template. + * Added default template files .codecov.yml, .gitattributes, and .gitignore, and + .vscode folder. + +## 3.12.0.0 + +* Changes to xHyper-V + * Removed alignPropertyValuePairs from the Visual Studio Code default style + formatting settings (issue #110). + +## 3.11.0.0 + +* Added the following resources: + * MSFT_xVMHardDiskDrive to manage additional attached VHD/Xs. + * MSFT_xVMScsiController to manage virtual machine SCSI controllers. +* MSFT_xVMSwitch: + * Added parameter to specify the Load Balancing Algorithm of a vSwitch with Switch Embedded Teaming (SET). + +## 3.10.0.0 + +* MSFT_xVMHyperV: + * Added support for configuring automatic snapshots. + +## 3.9.0.0 + +* MSFT_xVMHyperV: + * Enable / disable dynamic memory for client and server SKUs in identical way. + * Increased xVMHyperV StartupMemory and MinimumMemory limits from 17GB to 64GB. + * EnableGuestService works on localized OS (language independent). + * Adds missing Hyper-V-PowerShell feature in examples. +* Added the following resources: + * MSFT_xVMProcessor to manage virtual machine processor options. + * MSFT_xVMHost to managing Hyper-V host settings. +* MSFT_xVMSwitch: + * Added support for Switch Embedded Teaming (SET) in Server 2016. + * Fixed a bug where Get-TargetResource threw an error if a non External switch + is used. + * Updated unit tests to use template version 1.2.0. + * Style fixes. + * Added support for Localization. +* xHyper-V module: + * Added vs code formatting rule settings. + * Fix Markdown rule violations in Readme.md. + * Added .MetaTestOptIn.json for Markdown common test to be included. + * Added Appveyor badge for Dev branch in Readme.md and moved to Branches section. + * Added missing properties for all resources in Readme.md. + * Added and corrected missing / wrong DataTypes and Dsc attributes in Readme.md. + * Updated Readme to match DscResources style. + * Created change log and linked to it from Readme. + * Removed version info from Readme. + * Updated appveyor.yml to use Appveyor module. + * Examples: + * Removed code from Readme and linked to example files instead. + * Moved code to new example files where there was only code in Readme. + * Codecov: + * Updated appveyor.yml to include codecov. + * Added .codecov.yml. + * Added codecov badges to Readme. +* MSFT_xVHD: + * Support setting the disk type. + * Added unit tests. + * Added example Sample\_xVHD\_FixedVHD.ps1 + * Style fixes + +## 3.8.0.0 + +* Fix bug in xVMDvdDrive with hardcoded VM Name. +* Corrected Markdown rule violations in Readme.md. + +## 3.7.0.0 + +* Adding a new resource + * MSFT_xVMNetworkAdapter: Attaches a new VM network adapter to the management + OS or VM. + +## 3.6.0.0 + +* xVHD: Updated incorrect property name MaximumSize in error message +* Fix Markdown rule violations in Readme.md identified by [markdownlint](https://github.com/mivok/markdownlint/blob/master/docs/RULES.md). +* Created standard Unit/Integration test folder structure. +* Moved unit tests into Unit test folder. +* Renamed the unit tests to meet standards. +* Added the following resources: + * xVMDvdDrive to manage DVD drives attached to a Hyper-V virtual machine. + +## 3.5.0.0 + +* Converted appveyor.yml to install Pester from PSGallery instead of from Chocolatey. +* MSFT_xVMHyperV: Fixed bug in Test-TargetResource throwing when a Vhd's ParentPath + property was null. + +## 3.4.0.0 + +* MSFT_xVMHyperV: Fixed bug causing Test-TargetResource to fail when VM had snapshots. +* MSFT_xVMHyperV: Adds localization support. +* MSFT_xVMSwitch: Fixes bug where virtual switches are duplicated when + BandwidthReservationMode is not specified. + +## 3.3.0.0 + +* xHyperV: Added SecureBoot parameter to enable control of the secure boot BIOS + setting on generation 2 VMs. + * Fixed drive letter when mounting VHD when calling resource xVhdFile. Fixes #20. +* MSFT_xVMHyperV: Changed the SwitchName parameter to string[] to support + assigning multiple NICs to virtual machines. +* MSFT_xVMHyperV: Changed the MACAddress parameter to string[] to support + assigning multiple MAC addresses to virtual machines. +* MSFT_xVMHyperV: Added enabling of Guest Service Interface. +* MSFT_xVMSwitch: Added the BandwidthReservationMode parameter which specifies + how minimum bandwidth is to be configured on a virtual switch + +## 3.2.0.0 + +* Added data type System.String to CheckSum parameter of Get/Set/Test-TargetResource + functions and aligned indentation. +* Minor fixes + +## 3.1.0.0 + +* xVMHyperV: Fixed bug in mof schema (Generation property had two types) +* xVhdFileDirectory: Fixed typo in type comparison +* Readme updates + +## 3.0.0.0 + +* Decoupled VM generation from underlying VHD format in xVMHyperV resource. + * __Breaking change:__ xVMHyperV resource: Generation property type changed + from a String to an Integer. + * The initial generation property was tied to the virtual disk format which was + incorrect and has been rectified. + * This change will only impact configurations that have previously explicitly + specified the VM generation is either "vhd" or "vhdx". + +## 2.4.0.0 + +* Fixed VM power state issue in xVMHyperV resource + +## 2.3.0 + +* Fixed check for presence of param AllowManagementOS. + +## 2.2.1 + +## 2.1 + +* Added logic to automatically adjust VM's startup memory when only minimum and + maximum memory is specified in configuration +* Fixed the issue that a manually stopped VM cannot be brought back to running + state with DSC + +## 2.0 + +* Added xVhdFileDirectory Resource +* Allowed name to be specified with the extension in xVhd (e.g. the Vhd name could + either be "sample" or "sample.vhd") +* When a VHD cannot be removed because it is already being used by another process, + an error will be thrown. + +## 1.0.0.0 + +* Initial release with the following resources + * xVhd + * xVMHyperV + * xVMSwitch diff --git a/deployment/dsc/azshcihost/xHyper-V/3.17.0.0/DSCResources/HyperVCommon/HyperVCommon.psm1 b/deployment/dsc/azshcihost/xHyper-V/3.17.0.0/DSCResources/HyperVCommon/HyperVCommon.psm1 new file mode 100644 index 0000000..25ad1d5 --- /dev/null +++ b/deployment/dsc/azshcihost/xHyper-V/3.17.0.0/DSCResources/HyperVCommon/HyperVCommon.psm1 @@ -0,0 +1,436 @@ +#region localizeddata +if (Test-Path "${PSScriptRoot}\${PSUICulture}") +{ + Import-LocalizedData ` + -BindingVariable LocalizedData ` + -Filename HyperVCommon.strings.psd1 ` + -BaseDirectory "${PSScriptRoot}\${PSUICulture}" +} +else +{ + # fallback to en-US + Import-LocalizedData ` + -BindingVariable LocalizedData ` + -Filename HyperVCommon.strings.psd1 ` + -BaseDirectory "${PSScriptRoot}\en-US" +} +#endregion + +<# + .SYNOPSIS + Throws an InvalidOperation custom exception. + + .PARAMETER ErrorId + The error Id of the exception. + + .PARAMETER ErrorMessage + The error message text to set in the exception. +#> +function New-InvalidOperationError +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $ErrorId, + + [Parameter(Mandatory = $true)] + [System.String] + $ErrorMessage + ) + + $exception = New-Object -TypeName System.InvalidOperationException ` + -ArgumentList $ErrorMessage + $errorCategory = [System.Management.Automation.ErrorCategory]::InvalidOperation + $errorRecord = New-Object -TypeName System.Management.Automation.ErrorRecord ` + -ArgumentList $exception, $ErrorId, $errorCategory, $null + throw $errorRecord +} # end function New-InvalidOperationError + +<# + .SYNOPSIS + Throws an InvalidArgument custom exception. + + .PARAMETER ErrorId + The error Id of the exception. + + .PARAMETER ErrorMessage + The error message text to set in the exception. +#> +function New-InvalidArgumentError +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $ErrorId, + + [Parameter(Mandatory = $true)] + [System.String] + $ErrorMessage + ) + + $exception = New-Object -TypeName System.ArgumentException ` + -ArgumentList $ErrorMessage + $errorCategory = [System.Management.Automation.ErrorCategory]::InvalidArgument + $errorRecord = New-Object -TypeName System.Management.Automation.ErrorRecord ` + -ArgumentList $exception, $ErrorId, $errorCategory, $null + throw $errorRecord +} # end function New-InvalidArgumentError + +<# + .SYNOPSIS + Sets one or more virtual machine properties, powering the VM + off if required. + + .PARAMETER Name + Name of the virtual machine to apply the changes to. + + .PARAMETER VMName + Name of the virtual machine to apply the changes to. + + .PARAMETER VMCommand + The Hyper-V cmdlet name to call to enact the changes. + + .PARAMETER ChangeProperty + The collection of cmdlet parameter names and values to pass to the command. + + .PARAMETER WaitForIP + Waits for the virtual machine to report an IP address when transitioning + into a running state. + + .PARAMETER RestartIfNeeded + Power cycle the virtual machine if changes are required. +#> +function Set-VMProperty +{ + [CmdletBinding(DefaultParameterSetName = 'Name')] + param + ( + [Parameter(Mandatory = $true, ParameterSetName = 'Name')] + [System.String] + $Name, + + [Parameter(Mandatory = $true, ParameterSetName = 'VMName')] + [System.String] + $VMName, + + [Parameter(Mandatory = $true)] + [System.String] + $VMCommand, + + [Parameter(Mandatory = $true)] + [System.Collections.Hashtable] + $ChangeProperty, + + [Parameter()] + [System.Boolean] + $WaitForIP, + + [Parameter()] + [System.Boolean] + $RestartIfNeeded + ) + + if ($PSBoundParameters.ContainsKey('VMName')) + { + # Add the -Name property to the ChangeProperty hashtable for splatting + $ChangeProperty['VMName'] = $VMName + + # Set the common parameters for splatting against Get-VM and Set-VMState + $vmCommonProperty = @{ Name = $VMName; } + + # Ensure that the name parameter is set for verbose messages + $Name = $VMName + } + else + { + # Add the -Name property to the ChangeProperty hashtable for splatting + $ChangeProperty['Name'] = $Name + + # Set the common parameters for splatting against Get-VM and Set-VMState + $vmCommonProperty = @{ Name = $Name; } + } + + $vmObject = Get-VM @vmCommonProperty + $vmOriginalState = $vmObject.State + + if ($vmOriginalState -ne 'Off' -and $RestartIfNeeded) + { + # Turn the vm off to make changes + Set-VMState @vmCommonProperty -State Off + + Write-Verbose -Message ($localizedData.UpdatingVMProperties -f $Name) + # Make changes using the passed hashtable + & $VMCommand @ChangeProperty + + # Cannot move an off VM to a paused state - only to running state + if ($vmOriginalState -eq 'Running') + { + Set-VMState @vmCommonProperty -State Running -WaitForIP $WaitForIP + } + + Write-Verbose -Message ($localizedData.VMPropertiesUpdated -f $Name) + + # Cannot restore a vm to a paused state + if ($vmOriginalState -eq 'Paused') + { + Write-Warning -Message ($localizedData.VMStateWillBeOffWarning -f $Name) + } + } + elseif ($vmOriginalState -eq 'Off') + { + Write-Verbose -Message ($localizedData.UpdatingVMProperties -f $Name) + & $VMCommand @ChangeProperty + Write-Verbose -Message ($localizedData.VMPropertiesUpdated -f $Name) + } + else + { + $errorMessage = $localizedData.CannotUpdatePropertiesOnlineError -f $Name, $vmOriginalState + New-InvalidOperationError -ErrorId RestartRequired -ErrorMessage $errorMessage + } +} #end function + +<# + .SYNOPSIS + Sets one or more virtual machine properties, powering the VM + off if required. + + .PARAMETER Name + Name of the virtual machine to apply the changes to. + + .PARAMETER State + The target power state of the virtual machine. + + .PARAMETER ChangeProperty + The collection of cmdlet parameter names and values to pass to the command. + + .PARAMETER WaitForIP + Waits for the virtual machine to be report an IP address when transitioning + into a running state. +#> +function Set-VMState +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [Alias('VMName')] + [System.String] + $Name, + + [Parameter(Mandatory = $true)] + [ValidateSet('Running','Paused','Off')] + [System.String] + $State, + + [Parameter()] + [System.Boolean] + $WaitForIP + ) + + switch ($State) + { + 'Running' { + $vmCurrentState = (Get-VM -Name $Name).State + if ($vmCurrentState -eq 'Paused') + { + # If VM is in paused state, use resume-vm to make it running + Write-Verbose -Message ($localizedData.ResumingVM -f $Name) + Resume-VM -Name $Name + } + elseif ($vmCurrentState -eq 'Off') + { + # If VM is Off, use start-vm to make it running + Write-Verbose -Message ($localizedData.StartingVM -f $Name) + Start-VM -Name $Name + } + + if ($WaitForIP) + { + Wait-VMIPAddress -Name $Name -Verbose + } + } + 'Paused' { + if ($vmCurrentState -ne 'Off') + { + Write-Verbose -Message ($localizedData.SuspendingVM -f $Name) + Suspend-VM -Name $Name + } + } + 'Off' { + if ($vmCurrentState -ne 'Off') + { + Write-Verbose -Message ($localizedData.StoppingVM -f $Name) + Stop-VM -Name $Name -Force -WarningAction SilentlyContinue + } + } + } +} #end function + +<# + .SYNOPSIS + Waits for a virtual machine to be assigned an IP address. + + .PARAMETER Name + Name of the virtual machine to apply the changes to. + + .PARAMETER Timeout + Number of seconds to wait before timing out. Defaults to 300 (5 minutes). +#> +function Wait-VMIPAddress +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [Alias('VMName')] + [System.String] + $Name, + + [Parameter()] + [System.Int32] + $Timeout = 300 + ) + + [System.Int32] $elapsedSeconds = 0 + while ((Get-VMNetworkAdapter -VMName $Name).IpAddresses.Count -lt 2) + { + Write-Verbose -Message ($localizedData.WaitingForVMIPAddress -f $Name) + Start-Sleep -Seconds 3; + + $elapsedSeconds += 3 + if ($elapsedSeconds -gt $Timeout) + { + $errorMessage = $localizedData.WaitForVMIPAddressTimeoutError -f $Name, $Timeout + New-InvalidOperationError -ErrorId 'WaitVmTimeout' -ErrorMessage $errorMessage + } + } +} #end function + +<# + .SYNOPSIS + Ensures that the specified PowerShell module(s) are installed. + + .PARAMETER Name + Name of the PowerShell module to check is installed. +#> +function Assert-Module +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [System.String[]] + $Name + ) + + if (-not (Get-Module -Name $Name -ListAvailable )) + { + $errorMessage = $localizedData.RoleMissingError -f $Name + New-InvalidOperationError -ErrorId MissingRole -ErrorMessage $errorMessage + } +} #end function + +<# + .SYNOPSIS + Converts a number of seconds, minutes, hours or days into a System.TimeSpan object. + + .PARAMETER TimeInterval + The total number of seconds, minutes, hours or days to convert. + + .PARAMETER TimeSpanType + Convert using specified interval type. +#> +function ConvertTo-TimeSpan +{ + [CmdletBinding()] + [OutputType([System.TimeSpan])] + param + ( + [Parameter(Mandatory = $true)] + [System.UInt32] + $TimeInterval, + + [Parameter(Mandatory = $true)] + [ValidateSet('Seconds','Minutes','Hours','Days')] + [System.String] + $TimeIntervalType + ) + + $newTimeSpanParams = @{ } + switch ($TimeIntervalType) + { + 'Seconds' { $newTimeSpanParams['Seconds'] = $TimeInterval } + 'Minutes' { $newTimeSpanParams['Minutes'] = $TimeInterval } + 'Hours' { $newTimeSpanParams['Hours'] = $TimeInterval } + 'Days' { $newTimeSpanParams['Days'] = $TimeInterval } + } + return (New-TimeSpan @newTimeSpanParams) +} #end function ConvertTo-TimeSpan + +<# + .SYNOPSIS + Converts a System.TimeSpan into the number of seconds, minutes, hours or days. + + .PARAMETER TimeSpan + TimeSpan to convert into an integer + + .PARAMETER TimeSpanType + Convert timespan into the total number of seconds, minutes, hours or days. +#> +function ConvertFrom-TimeSpan +{ + [CmdletBinding()] + [OutputType([System.Int32])] + param + ( + [Parameter(Mandatory = $true)] + [System.TimeSpan] + $TimeSpan, + + [Parameter(Mandatory = $true)] + [ValidateSet('Seconds','Minutes','Hours','Days')] + [System.String] + $TimeSpanType + ) + + switch ($TimeSpanType) + { + 'Seconds' { return $TimeSpan.TotalSeconds -as [System.UInt32] } + 'Minutes' { return $TimeSpan.TotalMinutes -as [System.UInt32] } + 'Hours' { return $TimeSpan.TotalHours -as [System.UInt32] } + 'Days' { return $TimeSpan.TotalDays -as [System.UInt32] } + } +} #end function ConvertFrom-TimeSpan + +<# + .SYNOPSIS + Helper function for retrieving a virtual machine, ensuring only one VM is resolved + + .PARAMETER VMName + Name of the Hyper-V virtual machine to return +#> +function Get-VMHyperV +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $VMName + ) + + $vm = Get-VM -Name $VMName -ErrorAction SilentlyContinue + + # Check if 1 or 0 VM with name = $name exist + if ($vm.count -gt 1) + { + $errorMessage = $localizedData.MoreThanOneVMExistsError -f $VMName + New-InvalidArgumentError -ErrorId 'MultipleVMsFound' -ErrorMessage $errorMessage + } + + return $vm +} #end function Get-VMHyperV diff --git a/deployment/dsc/azshcihost/xHyper-V/3.17.0.0/DSCResources/HyperVCommon/en-us/HyperVCommon.strings.psd1 b/deployment/dsc/azshcihost/xHyper-V/3.17.0.0/DSCResources/HyperVCommon/en-us/HyperVCommon.strings.psd1 new file mode 100644 index 0000000..ae731d3 --- /dev/null +++ b/deployment/dsc/azshcihost/xHyper-V/3.17.0.0/DSCResources/HyperVCommon/en-us/HyperVCommon.strings.psd1 @@ -0,0 +1,16 @@ +ConvertFrom-StringData @' + UpdatingVMProperties = Updating VM '{0}' properties. + VMPropertiesUpdated = VM '{0}' properties have been updated. + WaitingForVMIPAddress = Waiting for IP Address for VM '{0}' ... + StoppingVM = Stopping VM '{0}'. + SuspendingVM = Suspending VM '{0}'. + StartingVM = Starting VM '{0}'. + ResumingVM = Resuming VM '{0}'. + + VMStateWillBeOffWarning = VM '{0}' state will be 'OFF' and not 'Paused'. + + CannotUpdatePropertiesOnlineError = Can not change properties for VM '{0}' in '{1}' state unless 'RestartIfNeeded' is set to true. + WaitForVMIPAddressTimeoutError = Waiting for VM '{0}' IP address timed out after {1} seconds. + RoleMissingError = Please ensure that '{0}' role is installed with its PowerShell module. + MoreThanOneVMExistsError = More than one VM with the name '{0}' exists. +'@ diff --git a/deployment/dsc/azshcihost/xHyper-V/3.17.0.0/DSCResources/MSFT_xVHD/MSFT_xVHD.psm1 b/deployment/dsc/azshcihost/xHyper-V/3.17.0.0/DSCResources/MSFT_xVHD/MSFT_xVHD.psm1 new file mode 100644 index 0000000..f1e769c --- /dev/null +++ b/deployment/dsc/azshcihost/xHyper-V/3.17.0.0/DSCResources/MSFT_xVHD/MSFT_xVHD.psm1 @@ -0,0 +1,381 @@ +<# +.SYNOPSIS + Gets MSFT_xVHD resource current state. + +.PARAMETER Name + The desired VHD file name. + +.PARAMETER Path + The desired Path where the VHD will be created. + +.PARAMETER Generation + Virtual disk format. +#> +function Get-TargetResource +{ + [CmdletBinding()] + [OutputType([System.Collections.Hashtable])] + param + ( + [Parameter(Mandatory = $true)] + [String] + $Name, + + [Parameter(Mandatory = $true)] + [String] + $Path, + + [Parameter()] + [ValidateSet("Vhd","Vhdx")] + [String] + $Generation = "Vhd" + ) + + # Check if Hyper-V module is present for Hyper-V cmdlets + if (!(Get-Module -ListAvailable -Name Hyper-V)) + { + Throw 'Please ensure that Hyper-V role is installed with its PowerShell module' + } + + # Construct the full path for the vhdFile + $vhdName = GetNameWithExtension -Name $Name -Generation $Generation + $vhdFilePath = Join-Path -Path $Path -ChildPath $vhdName + Write-Verbose -Message "Vhd full path is $vhdFilePath" + + $vhd = Get-VHD -Path $vhdFilePath -ErrorAction SilentlyContinue + + $ensure = 'Absent' + if ($vhd) + { + $ensure = 'Present' + } + + @{ + Name = $Name + Path = $Path + ParentPath = $vhd.ParentPath + Generation = $vhd.VhdFormat + Ensure = $ensure + ID = $vhd.DiskIdentifier + Type = $vhd.VhdType + FileSizeBytes = $vhd.FileSize + MaximumSizeBytes = $vhd.Size + IsAttached = $vhd.Attached + } +} + +<# +.SYNOPSIS + Configures MSFT_xVHD resource state. + +.PARAMETER Name + The desired VHD file name. + +.PARAMETER Path + The desired Path where the VHD will be created. + +.PARAMETER ParentPath + Parent VHD file path, for differencing disk. + +.PARAMETER MaximumSizeBytes + Maximum size of VHD to be created. + +.PARAMETER Type + Virtual disk type. + +.PARAMETER Generation + Virtual disk format. + +.PARAMETER Ensure + Ensures that the VHD is Present or Absent. +#> +function Set-TargetResource +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [String] + $Name, + + [Parameter(Mandatory = $true)] + [String] + $Path, + + [Parameter()] + [String] + $ParentPath, + + [Parameter()] + [Uint64] + $MaximumSizeBytes, + + [Parameter()] + [ValidateSet('Dynamic', 'Fixed', 'Differencing')] + [String] + $Type = 'Dynamic', + + [Parameter()] + [ValidateSet('Vhd', 'Vhdx')] + [String] + $Generation = 'Vhd', + + [Parameter()] + [ValidateSet('Present', 'Absent')] + [String] + $Ensure = 'Present' + ) + + # Construct the full path for the vhdFile + $vhdName = GetNameWithExtension -Name $Name -Generation $Generation + $vhdFilePath = Join-Path -Path $Path -ChildPath $vhdName + Write-Verbose -Message "Vhd full path is $vhdFilePath" + + Write-Verbose -Message "Checking if $vhdFilePath is $Ensure ..." + + # If vhd should be absent, delete it + if ($Ensure -eq 'Absent') + { + if (Test-Path -Path $vhdFilePath) + { + Write-Verbose -Message "$vhdFilePath is not $Ensure" + Remove-Item -Path $vhdFilePath -Force -ErrorAction Stop + } + Write-Verbose -Message "$vhdFilePath is $Ensure" + } + + else + { + # Check if the Vhd is present + try + { + $vhd = Get-VHD -Path $vhdFilePath -ErrorAction Stop + + # If this is a differencing disk, check the parent path + if ($ParentPath) + { + Write-Verbose -Message "Checking if $vhdFilePath parent path is $ParentPath ..." + + # If the parent path is not set correct, fix it + if ($vhd.ParentPath -ne $ParentPath) + { + Write-Verbose -Message "$vhdFilePath parent path is not $ParentPath." + Set-VHD -Path $vhdFilePath -ParentPath $ParentPath + Write-Verbose -Message "$vhdFilePath parent path is now $ParentPath." + } + else + { + Write-Verbose -Message "$vhdFilePath is $Ensure and parent path is set to $ParentPath." + } + } + + # This is a fixed disk, check the size + elseif ($PSBoundParameters.ContainsKey('MaximumSizeBytes')) + { + Write-Verbose -Message "Checking if $vhdFilePath size is $MaximumSizeBytes ..." + + # If the size is not correct, fix it + if ($vhd.Size -ne $MaximumSizeBytes) + { + Write-Verbose -Message "$vhdFilePath size is not $MaximumSizeBytes." + Resize-VHD -Path $vhdFilePath -SizeBytes $MaximumSizeBytes + Write-Verbose -Message "$vhdFilePath size is now $MaximumSizeBytes." + } + else + { + Write-Verbose -Message "$vhdFilePath is $Ensure and size is $MaximumSizeBytes." + } + } + + if ($vhd.Type -ne $Type) + { + Write-Verbose -Message 'This module can''t convert disk types' + } + } + + # Vhd file is not present + catch + { + Write-Verbose -Message "$vhdFilePath is not $Ensure" + if ($ParentPath) + { + $null = New-VHD -Path $vhdFilePath -ParentPath $ParentPath + } + else + { + $params = @{ + Path = $vhdFilePath + SizeBytes = $MaximumSizeBytes + $Type = $True + } + $null = New-VHD @params + } + + Write-Verbose -Message "$vhdFilePath is now $Ensure" + } + } +} + +<# +.SYNOPSIS + Tests if MSFT_xVHD resource state is in the desired state or not. + +.PARAMETER Name + The desired VHD file name. + +.PARAMETER Path + The desired Path where the VHD will be created. + +.PARAMETER ParentPath + Parent VHD file path, for differencing disk. + +.PARAMETER MaximumSizeBytes + Maximum size of VHD to be created. + +.PARAMETER Type + Virtual disk type. + +.PARAMETER Generation + Virtual disk format. + +.PARAMETER Ensure + Ensures that the VHD is Present or Absent. +#> +function Test-TargetResource +{ + [CmdletBinding()] + [OutputType([System.Boolean])] + param + ( + [Parameter(Mandatory = $true)] + [String] + $Name, + + [Parameter(Mandatory = $true)] + [String] + $Path, + + [Parameter()] + [String] + $ParentPath, + + [Parameter()] + [Uint64] + $MaximumSizeBytes, + + [Parameter()] + [ValidateSet('Vhd', 'Vhdx')] + [String] + $Generation = 'Vhd', + + [Parameter()] + [ValidateSet('Dynamic', 'Fixed', 'Differencing')] + [String] + $Type = 'Dynamic', + + [Parameter()] + [ValidateSet('Present', 'Absent')] + [String] + $Ensure = 'Present' + ) + + # Check if Hyper-V module is present for Hyper-V cmdlets + if (!(Get-Module -ListAvailable -Name Hyper-V)) + { + Throw "Please ensure that Hyper-V role is installed with its PowerShell module" + } + + # input validation + if ($Type -ne 'Differencing' -and -not $MaximumSizeBytes) + { + Throw 'Specify MaximumSizeBytes property for Fixed and Dynamic VHDs.' + } + + if ($ParentPath -and $Type -ne 'Differencing') + { + Throw 'Parent path is only supported for Differencing disks' + } + + if (-not $ParentPath -and $Type -eq 'Differencing') + { + Throw 'Differencing requires a parent path' + } + + if ($ParentPath) + { + if (!(Test-Path -Path $ParentPath)) + { + Throw "$ParentPath does not exists" + } + + # Check if the generation matches parenting disk + if ($Generation -and ($ParentPath.Split('.')[-1] -ne $Generation)) + { + Throw "Generation $Generation should match ParentPath extension $($ParentPath.Split('.')[-1])" + } + } + + if (!(Test-Path -Path $Path)) + { + Throw "$Path does not exists" + } + + # Construct the full path for the vhdFile + $vhdName = GetNameWithExtension -Name $Name -Generation $Generation + $vhdFilePath = Join-Path -Path $Path -ChildPath $vhdName + Write-Verbose -Message "Vhd full path is $vhdFilePath" + + # Add the logic here and at the end return either $true or $false. + $result = Test-VHD -Path $vhdFilePath -ErrorAction SilentlyContinue + Write-Verbose -Message "Vhd $vhdFilePath is present:$result and Ensure is $Ensure" + return ($result -and ($Ensure -eq "Present")) +} + +<# +.SYNOPSIS + Appends generation appropriate file extension if not already specified. + +.PARAMETER Name + The desired VHD file name. + +.PARAMETER Generation + Virtual disk format. +#> +function GetNameWithExtension +{ + param + ( + [Parameter(Mandatory = $true)] + [String] + $Name, + + [Parameter(Mandatory = $true)] + [String] + $Generation = 'Vhd' + ) + + # If the name ends with vhd or vhdx don't append the generation to the vhdname. + if ($Name -like '*.vhd' -or $Name -like '*.vhdx') + { + $extension = $Name.Split('.')[-1] + if ($Generation -ne $extension) + { + throw "the extension $extension on the name does not match the generation $Generation" + } + else + { + Write-Verbose -Message "Vhd full name is $vhdName" + $vhdName = $Name + } + } + else + { + # Append generation to the name + $vhdName = "$Name.$Generation" + Write-Verbose -Message "Vhd full name is $vhdName" + } + + $vhdName +} + +Export-ModuleMember -Function *-TargetResource diff --git a/deployment/dsc/azshcihost/xHyper-V/3.17.0.0/DSCResources/MSFT_xVHD/MSFT_xVHD.schema.mof b/deployment/dsc/azshcihost/xHyper-V/3.17.0.0/DSCResources/MSFT_xVHD/MSFT_xVHD.schema.mof new file mode 100644 index 0000000..f41e9d2 --- /dev/null +++ b/deployment/dsc/azshcihost/xHyper-V/3.17.0.0/DSCResources/MSFT_xVHD/MSFT_xVHD.schema.mof @@ -0,0 +1,15 @@ +[ClassVersion("1.0.0"), FriendlyName("xVHD")] +class MSFT_xVHD : OMI_BaseResource +{ + [Key, Description("Name of the VHD File")] String Name; + [Key, Description("Folder where the VHD will be created")] String Path; + [Write, Description("Parent VHD file path, for differencing disk")] String ParentPath; + [Write, Description("Maximum size of Vhd to be created")] Uint64 MaximumSizeBytes; + [Write, Description("Virtual disk format - Vhd or Vhdx"), ValueMap{"Vhd","Vhdx"}, Values{"Vhd","Vhdx"}] String Generation; + [Write, Description("Should the VHD be created or deleted"), ValueMap{"Present","Absent"}, Values{"Present","Absent"}] String Ensure; + [Read, Description("Virtual Disk Identifier")] String ID; + [Write, Description("Type of Vhd - Dynamic, Fixed, Differencing"), ValueMap{"Dynamic","Fixed","Differencing"}, Values{"Dynamic","Fixed","Differencing"}] String Type; + [Read, Description("Current size of the VHD")] Uint64 FileSizeBytes; + [Read, Description("Is the VHD attached to a VM or not")] Boolean IsAttached; +}; + diff --git a/deployment/dsc/azshcihost/xHyper-V/3.17.0.0/DSCResources/MSFT_xVMDvdDrive/MSFT_xVMDvdDrive.psm1 b/deployment/dsc/azshcihost/xHyper-V/3.17.0.0/DSCResources/MSFT_xVMDvdDrive/MSFT_xVMDvdDrive.psm1 new file mode 100644 index 0000000..fd7e010 --- /dev/null +++ b/deployment/dsc/azshcihost/xHyper-V/3.17.0.0/DSCResources/MSFT_xVMDvdDrive/MSFT_xVMDvdDrive.psm1 @@ -0,0 +1,437 @@ +#region localizeddata +if (Test-Path "${PSScriptRoot}\${PSUICulture}") +{ + Import-LocalizedData ` + -BindingVariable LocalizedData ` + -Filename MSFT_xVMDvdDrive.strings.psd1 ` + -BaseDirectory "${PSScriptRoot}\${PSUICulture}" +} +else +{ + #fallback to en-US + Import-LocalizedData ` + -BindingVariable LocalizedData ` + -Filename MSFT_xVMDvdDrive.strings.psd1 ` + -BaseDirectory "${PSScriptRoot}\en-US" +} +#endregion + +# Import the common HyperV functions +Import-Module -Name ( Join-Path ` + -Path (Split-Path -Path $PSScriptRoot -Parent) ` + -ChildPath '\HyperVCommon\HyperVCommon.psm1' ) + +<# + .SYNOPSIS + Returns the current status of the VM DVD Drive. + + .PARAMETER VMName + Specifies the name of the virtual machine to which the DVD drive is to be added. + + .PARAMETER ControllerNumber + Specifies the number of the controller to which the DVD drive is to be added. + + .PARAMETER ControllerLocation + Specifies the number of the location on the controller at which the DVD drive is to be added. +#> +function Get-TargetResource +{ + [CmdletBinding()] + [OutputType([System.Collections.Hashtable])] + param + ( + [parameter(Mandatory = $true)] + [System.String] + $VMName, + + [parameter(Mandatory = $true)] + [System.Uint32] + $ControllerNumber, + + [parameter(Mandatory = $true)] + [System.Uint32] + $ControllerLocation + ) + + Write-Verbose -Message ( @( + "$($MyInvocation.MyCommand): " + $($LocalizedData.GettingVMDVDDriveMessage ` + -f $VMName,$ControllerNumber,$ControllerLocation) + ) -join '' ) + + Test-ParameterValid @PSBoundParameters + + $dvdDrive = Get-VMDvdDrive @PSBoundParameters + + if ($dvdDrive) + { + $returnValue = @{ + VMName = $VMName + ControllerLocation = $ControllerLocation + ControllerNumber = $ControllerNumber + Path = $dvdDrive.Path + Ensure = 'Present' + } + } + else + { + $returnValue = @{ + VMName = $VMName + ControllerLocation = $ControllerLocation + ControllerNumber = $ControllerNumber + Path = '' + Ensure = 'Absent' + } + } # if + + $returnValue +} # Get-TargetResource + +<# + .SYNOPSIS + Adds, removes or changes the mounted ISO on a VM DVD Drive. + + .PARAMETER VMName + Specifies the name of the virtual machine to which the DVD drive is to be added. + + .PARAMETER ControllerNumber + Specifies the number of the controller to which the DVD drive is to be added. + + .PARAMETER ControllerLocation + Specifies the number of the location on the controller at which the DVD drive is to be added. + + .PARAMETER Path + Specifies the full path to the virtual hard disk file or physical hard disk volume for the + added DVD drive. + + .PARAMETER Ensure + Specifies if the DVD Drive should exist or not. +#> +function Set-TargetResource +{ + [CmdletBinding()] + param + ( + [parameter(Mandatory = $true)] + [System.String] + $VMName, + + [parameter(Mandatory = $true)] + [System.Uint32] + $ControllerLocation, + + [parameter(Mandatory = $true)] + [System.Uint32] + $ControllerNumber, + + [System.String] + $Path, + + [ValidateSet("Present","Absent")] + [System.String] + $Ensure = 'Present' + ) + + Write-Verbose -Message ( @( + "$($MyInvocation.MyCommand): " + $($LocalizedData.SettingVMDVDDriveMessage ` + -f $VMName,$ControllerNumber,$ControllerLocation) + ) -join '' ) + + $null = $PSBoundParameters.Remove('Path') + $null = $PSBoundParameters.Remove('Ensure') + + # Get the current status of the VM DVD Drive + $dvdDrive = Get-TargetResource @PSBoundParameters + + if ($Ensure -eq 'Present') + { + # The DVD Drive should exist + if ($dvdDrive.Ensure -eq 'Present') + { + # The DVD Drive already exists + if (-not [String]::IsNullOrWhiteSpace($Path) ` + -and ($Path -ne $dvdDrive.Path)) + { + # The current path assigned to the DVD Drive needs to be changed. + Write-Verbose -Message ( @( + "$($MyInvocation.MyCommand): " + $($LocalizedData.VMDVDDriveChangePathMessage) ` + -f $VMName,$ControllerNumber,$ControllerLocation,$Path ` + ) -join '' ) + + Set-VMDvdDrive @PSBoundParameters -Path $Path + } + } + else + { + # The DVD Drive does not exist but should. Change required. + Write-Verbose -Message ( @( + "$($MyInvocation.MyCommand): " + $($LocalizedData.VMDVDDriveAddMessage) ` + -f $VMName,$ControllerNumber,$ControllerLocation,$Path ` + ) -join '' ) + + if (-not [String]::IsNullOrWhiteSpace($Path)) { + $PSBoundParameters.Add('Path',$Path) + } # if + + Add-VMDvdDrive @PSBoundParameters + } # if + } + else + { + # The DVD Drive should not exist + if ($dvdDrive.Ensure -eq 'Present') + { + # The DVD Drive does exist, but should not. Change required. + Write-Verbose -Message ( @( + "$($MyInvocation.MyCommand): " + $($LocalizedData.VMDVDDriveRemoveMessage) ` + -f $VMName,$ControllerNumber,$ControllerLocation ` + ) -join '' ) + + Remove-VMDvdDrive @PSBoundParameters + } # if + } # if +} # Set-TargetResource + +<# + .SYNOPSIS + Tests the state of a VM DVD Drive and the mounted ISO. + + .PARAMETER VMName + Specifies the name of the virtual machine to which the DVD drive is to be added. + + .PARAMETER ControllerNumber + Specifies the number of the controller to which the DVD drive is to be added. + + .PARAMETER ControllerLocation + Specifies the number of the location on the controller at which the DVD drive is to be added. + + .PARAMETER Path + Specifies the full path to the virtual hard disk file or physical hard disk volume for the + added DVD drive. + + .PARAMETER Ensure + Specifies if the DVD Drive should exist or not. +#> +function Test-TargetResource +{ + [CmdletBinding()] + [OutputType([System.Boolean])] + param + ( + [parameter(Mandatory = $true)] + [System.String] + $VMName, + + [parameter(Mandatory = $true)] + [System.Uint32] + $ControllerLocation, + + [parameter(Mandatory = $true)] + [System.Uint32] + $ControllerNumber, + + [System.String] + $Path, + + [ValidateSet("Present","Absent")] + [System.String] + $Ensure = 'Present' + ) + + Write-Verbose -Message ( @( + "$($MyInvocation.MyCommand): " + $($LocalizedData.TestingVMDVDDriveMessage ` + -f $VMName,$ControllerNumber,$ControllerLocation) + ) -join '' ) + + $null = $PSBoundParameters.Remove('Path') + $null = $PSBoundParameters.Remove('Ensure') + + # Get the current status of the VM DVD Drive + $dvdDrive = Get-TargetResource @PSBoundParameters + + # Flag to signal whether settings are correct + [Boolean] $desiredConfigurationMatch = $true + + if ($Ensure -eq 'Present') + { + # The DVD Drive should exist + if ($dvdDrive.Ensure -eq 'Present') + { + # The DVD Drive already exists + if (-not [String]::IsNullOrWhiteSpace($Path) ` + -and ($Path -ne $dvdDrive.Path)) + { + # The current path assigned to the DVD drive is wrong. Change required. + Write-Verbose -Message ( @( + "$($MyInvocation.MyCommand): " + $($LocalizedData.VMDVDDriveExistsAndShouldPathMismatchMessage) ` + -f $VMName,$ControllerNumber,$ControllerLocation,$Path,$dvdDrive.Path ` + ) -join '' ) + + $desiredConfigurationMatch = $false + } + else + { + # The DVD drive exists and should. Change not required. + Write-Verbose -Message ( @( + "$($MyInvocation.MyCommand): " + $($LocalizedData.VMDVDDriveExistsAndShouldMessage) ` + -f $VMName,$ControllerNumber,$ControllerLocation,$Path ` + ) -join '' ) + } # if + } + else + { + # The DVD Drive does not exist but should. Change required. + Write-Verbose -Message ( @( + "$($MyInvocation.MyCommand): " + $($LocalizedData.VMDVDDriveDoesNotExistButShouldMessage) ` + -f $VMName,$ControllerNumber,$ControllerLocation ` + ) -join '' ) + + $desiredConfigurationMatch = $false + } # if + } + else + { + # The DVD Drive should not exist + if ($dvdDrive.Ensure -eq 'Present') + { + # The DVD Drive does exist, but should not. Change required. + Write-Verbose -Message ( @( + "$($MyInvocation.MyCommand): " + $($LocalizedData.VMDVDDriveDoesExistButShouldNotMessage) ` + -f $VMName,$ControllerNumber,$ControllerLocation ` + ) -join '' ) + + $desiredConfigurationMatch = $false + } + else + { + # The DVD Drive does not exist and should not. Change not required. + Write-Verbose -Message ( @( + "$($MyInvocation.MyCommand): " + $($LocalizedData.VMDVDDriveDoesNotExistAndShouldNotMessage) ` + -f $VMName,$ControllerNumber,$ControllerLocation ` + ) -join '' ) + } # if + } # if + + return $desiredConfigurationMatch +} # Test-TargetResource + +<# + .SYNOPSIS + Validates that the parameters passed are valid. If the parameter combination + is invalid then an exception will be thrown. The following items are validated: + - The VM exists. + - A disk mount point at the controller number/location exists. + - A hard disk is not already mounted at the controller number/location. + - The Path if required is valid. + + .PARAMETER VMName + Specifies the name of the virtual machine to which the DVD drive is to be added. + + .PARAMETER ControllerNumber + Specifies the number of the controller to which the DVD drive is to be added. + + .PARAMETER ControllerLocation + Specifies the number of the location on the controller at which the DVD drive is to be added. + + .PARAMETER Path + Specifies the full path to the virtual hard disk file or physical hard disk volume for the + added DVD drive. + + .PARAMETER Ensure + Specifies if the DVD Drive should exist or not. + + .OUTPUTS + Returns true if the parameters are valid, but will throw a specific error if not. +#> +function Test-ParameterValid +{ + [CmdletBinding()] + [OutputType([Boolean])] + param + ( + [parameter(Mandatory = $true)] + [System.String] + $VMName, + + [parameter(Mandatory = $true)] + [System.Uint32] + $ControllerLocation, + + [parameter(Mandatory = $true)] + [System.Uint32] + $ControllerNumber, + + [System.String] + $Path, + + [ValidateSet("Present","Absent")] + [System.String] + $Ensure = 'Present' + ) + + # Check if Hyper-V module is present for Hyper-V cmdlets + if(-not (Get-Module -ListAvailable -Name Hyper-V)) + { + New-InvalidArgumentError ` + -ErrorId 'RoleMissingError' ` + -ErrorMessage ($LocalizedData.RoleMissingError -f ` + 'Hyper-V') + } # if + + # Does the VM exist? + $null = Get-VM -Name $VMName + + # Does the controller exist? + if (-not (Get-VMScsiController -VMName $VMName -ControllerNumber $ControllerNumber) ` + -and -not (Get-VMIdeController -VMName $VMName -ControllerNumber $ControllerNumber)) + { + # No it does not + New-InvalidArgumentError ` + -ErrorId 'VMControllerDoesNotExistError' ` + -ErrorMessage ($LocalizedData.VMControllerDoesNotExistError -f ` + $VMName,$ControllerNumber) + } # if + + # Is a Hard Drive assigned to this controller location/number? + if (Get-VMHardDiskDrive ` + -VMName $VMName ` + -ControllerLocation $ControllerLocation ` + -ControllerNumber $ControllerNumber) + { + # Yes, so don't even try and touch this + New-InvalidArgumentError ` + -ErrorId 'ControllerConflictError' ` + -ErrorMessage ($LocalizedData.ControllerConflictError -f ` + $VMName,$ControllerNumber,$ControllerLocation) + } # if + + if ($Ensure -eq 'Present') + { + # If the path is not blank does it exist? + if (-not ([String]::IsNullOrWhiteSpace($Path))) + { + if (-not (Test-Path -Path $Path)) + { + # Path does not exist + New-InvalidArgumentError ` + -ErrorId 'PathDoesNotExistError' ` + -ErrorMessage ($LocalizedData.PathDoesNotExistError -f ` + $Path) + } # if + } # if + } # if + + return $true +} # Test-ParameterValid + +Export-ModuleMember -Function *-TargetResource diff --git a/deployment/dsc/azshcihost/xHyper-V/3.17.0.0/DSCResources/MSFT_xVMDvdDrive/MSFT_xVMDvdDrive.schema.mof b/deployment/dsc/azshcihost/xHyper-V/3.17.0.0/DSCResources/MSFT_xVMDvdDrive/MSFT_xVMDvdDrive.schema.mof new file mode 100644 index 0000000..04e3c4a --- /dev/null +++ b/deployment/dsc/azshcihost/xHyper-V/3.17.0.0/DSCResources/MSFT_xVMDvdDrive/MSFT_xVMDvdDrive.schema.mof @@ -0,0 +1,9 @@ +[ClassVersion("1.0.0.0"), FriendlyName("xVMDvdDrive")] +class MSFT_xVMDvdDrive : OMI_BaseResource +{ + [Key, Description("Specifies the name of the virtual machine to which the DVD drive is to be added.")] String VMName; + [Key, Description("Specifies the number of the controller to which the DVD drive is to be added.")] Uint32 ControllerNumber; + [Key, Description("Specifies the number of the location on the controller at which the DVD drive is to be added.")] Uint32 ControllerLocation; + [Write, Description("Specifies the full path to the virtual hard disk file or physical hard disk volume for the added DVD drive.")] String Path; + [Write, Description("Specifies if the DVD Drive should exist or not."), ValueMap{"Present","Absent"}, Values{"Present","Absent"}] String Ensure; +}; diff --git a/deployment/dsc/azshcihost/xHyper-V/3.17.0.0/DSCResources/MSFT_xVMDvdDrive/en-us/MSFT_xVMDvdDrive.strings.psd1 b/deployment/dsc/azshcihost/xHyper-V/3.17.0.0/DSCResources/MSFT_xVMDvdDrive/en-us/MSFT_xVMDvdDrive.strings.psd1 new file mode 100644 index 0000000..2c29d24 --- /dev/null +++ b/deployment/dsc/azshcihost/xHyper-V/3.17.0.0/DSCResources/MSFT_xVMDvdDrive/en-us/MSFT_xVMDvdDrive.strings.psd1 @@ -0,0 +1,20 @@ +ConvertFrom-StringData @' + GettingVMDVDDriveMessage = Getting VM DVD Drive for VM '{0}' controller number {1} location {2}. + + SettingVMDVDDriveMessage = Setting VM DVD Drive for VM '{0}' controller number {1} location {2}. + VMDVDDriveChangePathMessage = VM '{0}' DVD Drive on controller number {1} location {2} already exists, changing the Path to '{3}'. + VMDVDDriveAddMessage = Adding VM '{0}' DVD Drive on controller number {1} location {2} with path '{3}'. + VMDVDDriveRemoveMessage = Removing VM '{0}' DVD Drive on controller number {1} location {2}. + + TestingVMDVDDriveMessage = Testing VM DVD Drive for VM '{0}' controller number {1} location {2}. + VMDVDDriveExistsAndShouldPathMismatchMessage = VM '{0}' DVD Drive on controller number {1} location {2} exists and should but the desired path '{3}' does not match '{4}'. Change required. + VMDVDDriveExistsAndShouldMessage = VM '{0}' DVD Drive on controller number {1} location {2} exists and should and the path '{3}' matches. Change not required. + VMDVDDriveDoesNotExistButShouldMessage = VM '{0}' DVD Drive on controller number {1} location {2} does not exist but should. Change required. + VMDVDDriveDoesExistButShouldNotMessage = VM '{0}' DVD Drive on controller number {1} location {2} exists but should not. Change required. + VMDVDDriveDoesNotExistAndShouldNotMessage = VM '{0}' DVD Drive on controller number {1} location {2} does not exist and should not. Change not required. + + RoleMissingError = Please ensure that '{0}' role is installed with its PowerShell module. + VMControllerDoesNotExistError = The controller number {1} does not exist on VM '{0}'. + PathDoesNotExistError = The path '{0}' does not exist. + ControllerConflictError = The Controller number {1} location {2} already has a hard drive attached on VM '{0}'. +'@ diff --git a/deployment/dsc/azshcihost/xHyper-V/3.17.0.0/DSCResources/MSFT_xVMHardDiskDrive/MSFT_xVMHardDiskDrive.psm1 b/deployment/dsc/azshcihost/xHyper-V/3.17.0.0/DSCResources/MSFT_xVMHardDiskDrive/MSFT_xVMHardDiskDrive.psm1 new file mode 100644 index 0000000..b5db636 --- /dev/null +++ b/deployment/dsc/azshcihost/xHyper-V/3.17.0.0/DSCResources/MSFT_xVMHardDiskDrive/MSFT_xVMHardDiskDrive.psm1 @@ -0,0 +1,273 @@ +#region localizeddata +if (Test-Path "${PSScriptRoot}\${PSUICulture}") +{ + Import-LocalizedData ` + -BindingVariable LocalizedData ` + -Filename MSFT_xVMHardDiskDrive.strings.psd1 ` + -BaseDirectory "${PSScriptRoot}\${PSUICulture}" +} +else +{ + # fallback to en-US + Import-LocalizedData ` + -BindingVariable LocalizedData ` + -Filename MSFT_xVMHardDiskDrive.strings.psd1 ` + -BaseDirectory "${PSScriptRoot}\en-US" +} +#endregion + +# Import the common HyperV functions +Import-Module -Name ( Join-Path ` + -Path (Split-Path -Path $PSScriptRoot -Parent) ` + -ChildPath '\HyperVCommon\HyperVCommon.psm1' ) + +<# + .SYNOPSIS + Returns the current status of the VM hard disk drive. + .PARAMETER VMName + Specifies the name of the virtual machine whose hard disk drive status is to be fetched. + .PARAMETER Path + Specifies the full path of the VHD file linked to the hard disk drive. +#> +function Get-TargetResource +{ + [CmdletBinding()] + [OutputType([System.Collections.Hashtable])] + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $VMName, + + [Parameter(Mandatory = $true)] + [System.String] + $Path + ) + + Assert-Module -Name 'Hyper-V' + + $hardDiskDrive = Get-VMHardDiskDrive -VMName $VMName -ErrorAction Stop | + Where-Object -FilterScript { $_.Path -eq $Path } + + if ($null -eq $hardDiskDrive) + { + Write-Verbose -Message ($localizedData.DiskNotFound -f $Path, $VMName) + $ensure = 'Absent' + } + else + { + Write-Verbose -Message ($localizedData.DiskFound -f $Path, $VMName) + $ensure = 'Present' + } + + return @{ + VMName = $VMName + Path = $hardDiskDrive.Path + ControllerType = $hardDiskDrive.ControllerType + ControllerNumber = $hardDiskDrive.ControllerNumber + ControllerLocation = $hardDiskDrive.ControllerLocation + Ensure = $ensure + } +} + +<# + .SYNOPSIS + Tests the state of a VM hard disk drive. + .PARAMETER VMName + Specifies the name of the virtual machine whose hard disk drive is to be tested. + .PARAMETER Path + Specifies the full path of the VHD file to be tested. + .PARAMETER ControllerType + Specifies the type of controller to which the the hard disk drive is to be set (IDE/SCSI). + Default to SCSI. + .PARAMETER ControllerNumber + Specifies the number of the controller to which the hard disk drive is to be set. + If not specified, the controller number defaults to 0. + .PARAMETER ControllerLocation + Specifies the number of the location on the controller at which the hard disk drive is to be + set. If not specified, the controller location defaults to 0. + .PARAMETER Ensure + Specifies if the hard disk drive should exist or not. Defaults to Present. +#> +function Test-TargetResource +{ + [CmdletBinding()] + [OutputType([System.Boolean])] + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $VMName, + + [Parameter(Mandatory = $true)] + [System.String] + $Path, + + [Parameter()] + [ValidateSet('IDE', 'SCSI')] + [System.String] + $ControllerType = 'SCSI', + + [Parameter()] + [ValidateSet(0, 1, 2, 3)] + [System.UInt32] + $ControllerNumber, + + [Parameter()] + [ValidateRange(0, 63)] + [System.UInt32] + $ControllerLocation, + + [Parameter()] + [ValidateSet('Present', 'Absent')] + [System.String] + $Ensure = 'Present' + ) + + $resource = Get-TargetResource -VMName $VMName -Path $Path + + # Throw exception when the ControllerNumber or ControllerLocation are out of bounds for IDE + if ($ControllerType -eq 'IDE' -and ($ControllerNumber -gt 1 -or $ControllerLocation -gt 1)) + { + $errorMessage = $localizedData.IdeLocationError -f $ControllerNumber, $ControllerLocation + New-InvalidOperationError -ErrorId 'InvalidLocation' -ErrorMessage $errorMessage + } + + $isCompliant = $true + foreach ($key in $PSBoundParameters.Keys) + { + # Only check passed parameter values + if ($resource.ContainsKey($key)) + { + Write-Verbose -Message ($localizedData.ComparingParameter -f $key, + $PSBoundParameters[$key], + $resource[$key]) + $isCompliant = $isCompliant -and ($PSBoundParameters[$key] -eq $resource[$key]) + } + } + return $isCompliant +} + +<# + .SYNOPSIS + Tests the state of a VM hard disk drive. + .PARAMETER VMName + Specifies the name of the virtual machine whose hard disk drive is to be tested. + .PARAMETER Path + Specifies the full path of the VHD file to be tested. + .PARAMETER ControllerType + Specifies the type of controller to which the the hard disk drive is to be set (IDE/SCSI). + Default to SCSI. + .PARAMETER ControllerNumber + Specifies the number of the controller to which the hard disk drive is to be set. + If not specified, the controller number defaults to 0. + .PARAMETER ControllerLocation + Specifies the number of the location on the controller at which the hard disk drive is to be + set. If not specified, the controller location defaults to 0. + .PARAMETER Ensure + Specifies if the hard disk drive should exist or not. Defaults to Present. +#> +function Set-TargetResource +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $VMName, + + [Parameter(Mandatory = $true)] + [System.String] + $Path, + + [Parameter()] + [ValidateSet('IDE', 'SCSI')] + [System.String] + $ControllerType = 'SCSI', + + [Parameter()] + [ValidateSet(0, 1, 2, 3)] + [System.UInt32] + $ControllerNumber, + + [Parameter()] + [ValidateRange(0, 63)] + [System.UInt32] + $ControllerLocation, + + [Parameter()] + [ValidateSet('Present', 'Absent')] + [System.String] + $Ensure = 'Present' + ) + + Assert-Module -Name 'Hyper-V' + + $hardDiskDrive = Get-VMHardDiskDrive -VMName $VMName | + Where-Object -FilterScript { $_.Path -eq $Path } + + if ($Ensure -eq 'Present') + { + $null = $PSBoundParameters.Remove('Ensure') + + Write-Verbose -Message ($localizedData.CheckingDiskIsAttached) + if ($hardDiskDrive) + { + Write-Verbose -Message ($localizedData.DiskFound -f $Path, $VMName) + $null = $PSBoundParameters.Remove('VMName') + $null = $PSBoundParameters.Remove('Path') + # As the operation is a move, we must use ToController instead of Controller + if ($PSBoundParameters.ContainsKey('ControllerType')) + { + $null = $PSBoundParameters.Remove('ControllerType') + $null = $PSBoundParameters.Add('ToControllerType', $ControllerType) + } + if ($PSBoundParameters.ContainsKey('ControllerNumber')) + { + $null = $PSBoundParameters.Remove('ControllerNumber') + $null = $PSBoundParameters.Add('ToControllerNumber', $ControllerNumber) + } + if ($PSBoundParameters.ContainsKey('ControllerLocation')) + { + $null = $PSBoundParameters.Remove('ControllerLocation') + $null = $PSBoundParameters.Add('ToControllerLocation', $ControllerLocation) + } + $null = $hardDiskDrive | Set-VMHardDiskDrive @PSBoundParameters + } + else + { + Write-Verbose -Message ($localizedData.CheckingExistingDiskLocation) + $getVMHardDiskDriveParams = @{ + VMName = $VMName + ControllerType = $ControllerType + ControllerNumber = $ControllerNumber + ControllerLocation = $ControllerLocation + } + $existingHardDiskDrive = Get-VMHardDiskDrive @getVMHardDiskDriveParams + if ($null -ne $existingHardDiskDrive) + { + $errorMessage = $localizedData.DiskPresentError -f $ControllerNumber, ` + $ControllerLocation + New-InvalidOperationError -ErrorId 'ControllerNotEmpty' -ErrorMessage $errorMessage + } + + Write-Verbose -Message ($localizedData.AddingDisk -f $Path, $VMName) + $null = Add-VMHardDiskDrive @PSBoundParameters + } + } + else + { + # We must ensure that the disk is absent + if ($hardDiskDrive) + { + Write-Verbose -Message ($localizedData.RemovingDisk -f $Path, $VMName) + $null = $hardDiskDrive | Remove-VMHardDiskDrive + } + else + { + Write-Warning -Message ($localizedData.DiskNotFound -f $Path, $VMName) + } + } +} + +Export-ModuleMember -Function *-TargetResource diff --git a/deployment/dsc/azshcihost/xHyper-V/3.17.0.0/DSCResources/MSFT_xVMHardDiskDrive/MSFT_xVMHardDiskDrive.schema.mof b/deployment/dsc/azshcihost/xHyper-V/3.17.0.0/DSCResources/MSFT_xVMHardDiskDrive/MSFT_xVMHardDiskDrive.schema.mof new file mode 100644 index 0000000..3375c15 --- /dev/null +++ b/deployment/dsc/azshcihost/xHyper-V/3.17.0.0/DSCResources/MSFT_xVMHardDiskDrive/MSFT_xVMHardDiskDrive.schema.mof @@ -0,0 +1,10 @@ +[ClassVersion("1.0.0"), FriendlyName("xVMHardDiskDrive")] +class MSFT_xVMHardDiskDrive : OMI_BaseResource +{ + [Key, Description("Specifies the name of the virtual machine whose hard disk drive is to be manipulated.")] String VMName; + [Key, Description("Specifies the full path to the location of the VHD that represents the hard disk drive.")] String Path; + [Write, Description("Specifies the controller type - IDE/SCSI where the disk is attached. If not specified, it defaults to SCSI."), ValueMap{"IDE","SCSI"}, Values{"IDE","SCSI"}] String ControllerType; + [Write, Description("Specifies the number of the controller where the disk is attached. If not specified, it defaults to 0."), ValueMap{"0","1","2","3"}, Values{"0","1","2","3"}] Uint32 ControllerNumber; + [Write, Description("Specifies the number of the location on the controller where the disk is attached. If not specified, it defaults to 0.")] Uint32 ControllerLocation; + [Write, Description("Specifies if the hard disk drive must be present or absent. If not specified, it defaults to Present."), ValueMap{"Present","Absent"}, Values{"Present","Absent"}] String Ensure; +}; diff --git a/deployment/dsc/azshcihost/xHyper-V/3.17.0.0/DSCResources/MSFT_xVMHardDiskDrive/en-US/MSFT_xVMHardDiskDrive.strings.psd1 b/deployment/dsc/azshcihost/xHyper-V/3.17.0.0/DSCResources/MSFT_xVMHardDiskDrive/en-US/MSFT_xVMHardDiskDrive.strings.psd1 new file mode 100644 index 0000000..50a9aa4 --- /dev/null +++ b/deployment/dsc/azshcihost/xHyper-V/3.17.0.0/DSCResources/MSFT_xVMHardDiskDrive/en-US/MSFT_xVMHardDiskDrive.strings.psd1 @@ -0,0 +1,12 @@ +ConvertFrom-StringData @' + DiskFound = Found hard disk '{0}' attached to VM '{1}'. + DiskNotFound = Hard disk '{0}' missing from VM '{1}' + CheckingDiskIsAttached = Checking if the disk is already attached to the VM. + CheckingExistingDiskLocation = Checking if there is an existing disk in the specified location. + AddingDisk = Adding the disk '{0}' to VM '{1}'. + RemovingDisk = Removing disk '{0}' from VM '{1}'. + ComparingParameter = Comparing '{0}'; expected '{1}', actual '{2}'. + + DiskPresentError = There is already a disk present in controller '{0}', location '{1}'. + IdeLocationError = ControllerNumber '{0}' or ControllerLocation '{1}' are not valid for IDE controller. +'@ diff --git a/deployment/dsc/azshcihost/xHyper-V/3.17.0.0/DSCResources/MSFT_xVMHost/MSFT_xVMHost.psm1 b/deployment/dsc/azshcihost/xHyper-V/3.17.0.0/DSCResources/MSFT_xVMHost/MSFT_xVMHost.psm1 new file mode 100644 index 0000000..b4c0bb3 --- /dev/null +++ b/deployment/dsc/azshcihost/xHyper-V/3.17.0.0/DSCResources/MSFT_xVMHost/MSFT_xVMHost.psm1 @@ -0,0 +1,457 @@ +#region localizeddata +if (Test-Path "${PSScriptRoot}\${PSUICulture}") +{ + Import-LocalizedData -BindingVariable localizedData -Filename MSFT_xVMHost.psd1 ` + -BaseDirectory "${PSScriptRoot}\${PSUICulture}" +} +else +{ + # fallback to en-US + Import-LocalizedData -BindingVariable localizedData -Filename MSFT_xVMHost.psd1 ` + -BaseDirectory "${PSScriptRoot}\en-US" +} +#endregion + +# Import the common HyperV functions +Import-Module -Name ( Join-Path ` + -Path (Split-Path -Path $PSScriptRoot -Parent) ` + -ChildPath '\HyperVCommon\HyperVCommon.psm1' ) + +<# +.SYNOPSIS + Gets MSFT_xVMHost resource current state. + +.PARAMETER IsSingleInstance + Specifies the resource is a single instance, the value must be 'Yes'. +#> +function Get-TargetResource +{ + [CmdletBinding()] + [OutputType([System.Collections.Hashtable])] + param + ( + [Parameter(Mandatory = $true)] + [ValidateSet('Yes')] + [System.String] + $IsSingleInstance + ) + + Assert-Module -Name 'Hyper-V' + + Write-Verbose -Message $localizedData.QueryingVMHost + $vmHost = Get-VMHost + + # Convert the current TimeSpan into minutes + $convertFromTimeSpanParams = @{ + TimeSpan = $vmHost.ResourceMeteringSaveInterval + TimeSpanType = 'Minutes' + } + $resourceMeteringSaveInterval = ConvertFrom-TimeSpan @convertFromTimeSpanParams + + $configuration = @{ + IsSingleInstance = $IsSingleInstance + EnableEnhancedSessionMode = $vmHost.EnableEnhancedSessionMode + FibreChannelWwnn = $vmHost.FibreChannelWwnn + FibreChannelWwpnMaximum = $vmHost.FibreChannelWwpnMaximum + FibreChannelWwpnMinimum = $vmHost.FibreChannelWwpnMinimum + MacAddressMaximum = $vmHost.MacAddressMaximum + MacAddressMinimum = $vmHost.MacAddressMinimum + MaximumStorageMigrations = $vmHost.MaximumStorageMigrations + MaximumVirtualMachineMigrations = $vmHost.MaximumVirtualMachineMigrations + NumaSpanningEnabled = $vmHost.NumaSpanningEnabled + ResourceMeteringSaveIntervalMinute = $resourceMeteringSaveInterval + UseAnyNetworkForMigration = $vmHost.UseAnyNetworkForMigration + VirtualHardDiskPath = $vmHost.VirtualHardDiskPath + VirtualMachineMigrationAuthenticationType = $vmHost.VirtualMachineMigrationAuthenticationType + VirtualMachineMigrationPerformanceOption = $vmHost.VirtualMachineMigrationPerformanceOption + VirtualMachinePath = $vmHost.VirtualMachinePath + VirtualMachineMigrationEnabled = $vmHost.VirtualMachineMigrationEnabled + } + + return $configuration +} + +<# +.SYNOPSIS + Tests if MSFT_xVMHost resource state is in the desired state or not. + +.PARAMETER IsSingleInstance + Specifies the resource is a single instance, the value must be 'Yes'. + +.PARAMETER EnableEnhancedSessionMode + Indicates whether users can use enhanced mode when they connect to virtual machines on this + server by using Virtual Machine Connection. + +.PARAMETER FibreChannelWwnn + Specifies the default value of the World Wide Node Name on the Hyper-V host. + +.PARAMETER FibreChannelWwpnMaximum + Specifies the maximum value that can be used to generate World Wide Port Names on the Hyper-V + host. Use with the FibreChannelWwpnMinimum parameter to establish a range of WWPNs that the + specified Hyper-V host can assign to virtual Fibre Channel adapters. + +.PARAMETER FibreChannelWwpnMinimum + Specifies the minimum value that can be used to generate the World Wide Port Names on the + Hyper-V host. Use with the FibreChannelWwpnMaximum parameter to establish a range of WWPNs that + the specified Hyper-V host can assign to virtual Fibre Channel adapters. + +.PARAMETER MacAddressMaximum + Specifies the maximum MAC address using a valid hexadecimal value. Use with the + MacAddressMinimum parameter to establish a range of MAC addresses that the specified Hyper-V + host can assign to virtual machines configured to receive dynamic MAC addresses. + +.PARAMETER MacAddressMinimum + Specifies the minimum MAC address using a valid hexadecimal value. Use with the + MacAddressMaximum parameter to establish a range of MAC addresses that the specified Hyper-V + host can assign to virtual machines configured to receive dynamic MAC addresses. + +.PARAMETER MaximumStorageMigrations + Specifies the maximum number of storage migrations that can be performed at the same time on + the Hyper-V host. + +.PARAMETER MaximumVirtualMachineMigrations + Specifies the maximum number of live migrations that can be performed at the same time on the + Hyper-V host. + +.PARAMETER NumaSpanningEnabled + Specifies whether virtual machines on the Hyper-V host can use resources from more than one + NUMA node. + +.PARAMETER ResourceMeteringSaveIntervalMinute + Specifies how often the Hyper-V host saves the data that tracks resource usage. The range is a + minimum of 60 minutes to a maximum 1440 minutes (24 hours). + +.PARAMETER UseAnyNetworkForMigration + Specifies how networks are selected for incoming live migration traffic. If set to $True, any + available network on the host can be used for this traffic. If set to $False, incoming live + migration traffic is transmitted only on the networks specified in the MigrationNetworks + property of the host. + +.PARAMETER VirtualHardDiskPath + Specifies the default folder to store virtual hard disks on the Hyper-V host. + +.PARAMETER VirtualMachineMigrationAuthenticationType + Specifies the type of authentication to be used for live migrations. The acceptable values for + this parameter are 'Kerberos' and 'CredSSP'. + +.PARAMETER VirtualMachineMigrationPerformanceOption + Specifies the performance option to use for live migration. The acceptable values for this + parameter are 'TCPIP', 'Compression' and 'SMB'. + +.PARAMETER VirtualMachinePath + Specifies the default folder to store virtual machine configuration files on the Hyper-V host. + +.PARAMETER VirtualMachineMigrationEnabled + Indicates whether Live Migration should be enabled or disabled on the Hyper-V host. +#> +function Test-TargetResource +{ + [CmdletBinding()] + [OutputType([System.Boolean])] + param + ( + [Parameter(Mandatory = $true)] + [ValidateSet('Yes')] + [System.String] + $IsSingleInstance, + + [Parameter()] + [System.Boolean] + $EnableEnhancedSessionMode, + + [Parameter()] + [System.String] + $FibreChannelWwnn, + + [Parameter()] + [System.String] + $FibreChannelWwpnMaximum, + + [Parameter()] + [System.String] + $FibreChannelWwpnMinimum, + + [Parameter()] + [System.String] + $MacAddressMaximum, + + [Parameter()] + [System.String] + $MacAddressMinimum, + + [Parameter()] + [System.UInt32] + $MaximumStorageMigrations, + + [Parameter()] + [System.UInt32] + $MaximumVirtualMachineMigrations, + + [Parameter()] + [System.Boolean] + $NumaSpanningEnabled, + + [Parameter()] + [System.UInt32] + $ResourceMeteringSaveIntervalMinute, + + [Parameter()] + [System.Boolean] + $UseAnyNetworkForMigration, + + [Parameter()] + [System.String] + $VirtualHardDiskPath, + + [Parameter()] + [ValidateSet('Kerberos', 'CredSSP')] + [System.String] + $VirtualMachineMigrationAuthenticationType, + + [Parameter()] + [ValidateSet('TCPIP', 'Compression', 'SMB')] + [System.String] + $VirtualMachineMigrationPerformanceOption, + + [Parameter()] + [System.String] + $VirtualMachinePath, + + [Parameter()] + [System.Boolean] + $VirtualMachineMigrationEnabled + ) + + Assert-Module -Name 'Hyper-V' + + $targetResource = Get-TargetResource -IsSingleInstance $IsSingleInstance + $isTargetResourceCompliant = $true + + foreach ($parameter in $PSBoundParameters.GetEnumerator()) + { + if (($targetResource.ContainsKey($parameter.Key)) -and + ($parameter.Value -ne $targetResource[$parameter.Key])) + { + $isTargetResourceCompliant = $false + Write-Verbose -Message ($localizedData.PropertyMismatch -f $parameter.Key, + $parameter.Value, $targetResource[$parameter.Key]) + } + } + + if ($isTargetResourceCompliant) + { + Write-Verbose -Message $localizedData.VMHostInDesiredState + } + else + { + Write-Verbose -Message $localizedData.VMHostNotInDesiredState + } + + return $isTargetResourceCompliant +} #end function + +<# +.SYNOPSIS + Configures MSFT_xVMHost resource state. + +.PARAMETER IsSingleInstance + Specifies the resource is a single instance, the value must be 'Yes'. + +.PARAMETER EnableEnhancedSessionMode + Indicates whether users can use enhanced mode when they connect to virtual machines on this + server by using Virtual Machine Connection. + +.PARAMETER FibreChannelWwnn + Specifies the default value of the World Wide Node Name on the Hyper-V host. + +.PARAMETER FibreChannelWwpnMaximum + Specifies the maximum value that can be used to generate World Wide Port Names on the Hyper-V + host. Use with the FibreChannelWwpnMinimum parameter to establish a range of WWPNs that the + specified Hyper-V host can assign to virtual Fibre Channel adapters. + +.PARAMETER FibreChannelWwpnMinimum + Specifies the minimum value that can be used to generate the World Wide Port Names on the + Hyper-V host. Use with the FibreChannelWwpnMaximum parameter to establish a range of WWPNs that + the specified Hyper-V host can assign to virtual Fibre Channel adapters. + +.PARAMETER MacAddressMaximum + Specifies the maximum MAC address using a valid hexadecimal value. Use with the + MacAddressMinimum parameter to establish a range of MAC addresses that the specified Hyper-V + host can assign to virtual machines configured to receive dynamic MAC addresses. + +.PARAMETER MacAddressMinimum + Specifies the minimum MAC address using a valid hexadecimal value. Use with the + MacAddressMaximum parameter to establish a range of MAC addresses that the specified Hyper-V + host can assign to virtual machines configured to receive dynamic MAC addresses. + +.PARAMETER MaximumStorageMigrations + Specifies the maximum number of storage migrations that can be performed at the same time on + the Hyper-V host. + +.PARAMETER MaximumVirtualMachineMigrations + Specifies the maximum number of live migrations that can be performed at the same time on the + Hyper-V host. + +.PARAMETER NumaSpanningEnabled + Specifies whether virtual machines on the Hyper-V host can use resources from more than one + NUMA node. + +.PARAMETER ResourceMeteringSaveIntervalMinute + Specifies how often the Hyper-V host saves the data that tracks resource usage. The range is a + minimum of 60 minutes to a maximum 1440 minutes (24 hours). + +.PARAMETER UseAnyNetworkForMigration + Specifies how networks are selected for incoming live migration traffic. If set to $True, any + available network on the host can be used for this traffic. If set to $False, incoming live + migration traffic is transmitted only on the networks specified in the MigrationNetworks + property of the host. + +.PARAMETER VirtualHardDiskPath + Specifies the default folder to store virtual hard disks on the Hyper-V host. + +.PARAMETER VirtualMachineMigrationAuthenticationType + Specifies the type of authentication to be used for live migrations. The acceptable values for + this parameter are 'Kerberos' and 'CredSSP'. + +.PARAMETER VirtualMachineMigrationPerformanceOption + Specifies the performance option to use for live migration. The acceptable values for this + parameter are 'TCPIP', 'Compression' and 'SMB'. + +.PARAMETER VirtualMachinePath + Specifies the default folder to store virtual machine configuration files on the Hyper-V host. + +.PARAMETER VirtualMachineMigrationEnabled + Indicates whether Live Migration should be enabled or disabled on the Hyper-V host. +#> +function Set-TargetResource +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [ValidateSet('Yes')] + [System.String] + $IsSingleInstance, + + [Parameter()] + [System.Boolean] + $EnableEnhancedSessionMode, + + [Parameter()] + [System.String] + $FibreChannelWwnn, + + [Parameter()] + [System.String] + $FibreChannelWwpnMaximum, + + [Parameter()] + [System.String] + $FibreChannelWwpnMinimum, + + [Parameter()] + [System.String] + $MacAddressMaximum, + + [Parameter()] + [System.String] + $MacAddressMinimum, + + [Parameter()] + [System.UInt32] + $MaximumStorageMigrations, + + [Parameter()] + [System.UInt32] + $MaximumVirtualMachineMigrations, + + [Parameter()] + [System.Boolean] + $NumaSpanningEnabled, + + [Parameter()] + [System.UInt32] + $ResourceMeteringSaveIntervalMinute, + + [Parameter()] + [System.Boolean] + $UseAnyNetworkForMigration, + + [Parameter()] + [System.String] + $VirtualHardDiskPath, + + [Parameter()] + [ValidateSet('Kerberos', 'CredSSP')] + [System.String] + $VirtualMachineMigrationAuthenticationType, + + [Parameter()] + [ValidateSet('TCPIP', 'Compression', 'SMB')] + [System.String] + $VirtualMachineMigrationPerformanceOption, + + [Parameter()] + [System.String] + $VirtualMachinePath, + + [Parameter()] + [System.Boolean] + $VirtualMachineMigrationEnabled + ) + + Assert-Module -Name 'Hyper-V' + + $null = $PSBoundParameters.Remove('IsSingleInstance') + + if ($PSBoundParameters.ContainsKey('ResourceMeteringSaveIntervalMinute')) + { + # Need to convert the specified minutes into a TimeSpan object first + $convertToTimeSpanParams = @{ + TimeInterval = $PSBoundParameters['ResourceMeteringSaveIntervalMinute'] + TimeIntervalType = 'Minutes' + } + $resourceMeteringSaveInterval = ConvertTo-TimeSpan @convertToTimeSpanParams + + # Remove the existing UInt32 explicit type and add the TimeSpan type parameter + $null = $PSBoundParameters.Remove('ResourceMeteringSaveIntervalMinute') + $PSBoundParameters['ResourceMeteringSaveInterval'] = $resourceMeteringSaveInterval + } + + if ($PSBoundParameters.ContainsKey('VirtualMachineMigrationEnabled')) + { + $null = $PSBoundParameters.Remove('VirtualMachineMigrationEnabled') + + if ($VirtualMachineMigrationEnabled) + { + if ((Get-CimInstance -ClassName Win32_ComputerSystem).PartOfDomain) + { + Write-Verbose -Message $localizedData.EnableLiveMigration + Enable-VMMigration + } + else + { + New-InvalidOperationError -ErrorId InvalidState -ErrorMessage $localizedData.LiveMigrationDomainOnly + } + } + else + { + Write-Verbose -Message $localizedData.DisableLiveMigration + Disable-VMMigration + } + } + + $vmHostParams = $PSBoundParameters.GetEnumerator() | Where-Object -FilterScript { + $_.Key -notin ( + [System.Management.Automation.PSCmdlet]::CommonParameters + + [System.Management.Automation.PSCmdlet]::OptionalCommonParameters + ) + } + + if ($vmHostParams.Count -ne 0) + { + Write-Verbose -Message $localizedData.UpdatingVMHostProperties + Set-VMHost @PSBoundParameters + Write-Verbose -Message $localizedData.VMHostPropertiesUpdated + } +} #end function diff --git a/deployment/dsc/azshcihost/xHyper-V/3.17.0.0/DSCResources/MSFT_xVMHost/MSFT_xVMHost.schema.mof b/deployment/dsc/azshcihost/xHyper-V/3.17.0.0/DSCResources/MSFT_xVMHost/MSFT_xVMHost.schema.mof new file mode 100644 index 0000000..12be0e8 --- /dev/null +++ b/deployment/dsc/azshcihost/xHyper-V/3.17.0.0/DSCResources/MSFT_xVMHost/MSFT_xVMHost.schema.mof @@ -0,0 +1,21 @@ +[ClassVersion("1.0.0.0"), FriendlyName("xVMHost")] +class MSFT_xVMHost : OMI_BaseResource +{ + [Key, Description("Specifies the resource is a single instance, the value must be 'Yes'."), ValueMap{"Yes"}, Values{"Yes"}] String IsSingleInstance; + [Write, Description("Indicates whether users can use enhanced mode when they connect to virtual machines on this server by using Virtual Machine Connection.")] Boolean EnableEnhancedSessionMode; + [Write, Description("Specifies the default value of the World Wide Node Name on the Hyper-V host.")] String FibreChannelWwnn; + [Write, Description("Specifies the maximum value that can be used to generate World Wide Port Names on the Hyper-V host.")] String FibreChannelWwpnMaximum; + [Write, Description("Specifies the minimum value that can be used to generate the World Wide Port Names on the Hyper-V host.")] String FibreChannelWwpnMinimum; + [Write, Description("Specifies the maximum MAC address using a valid hexadecimal value.")] String MacAddressMaximum; + [Write, Description("Specifies the minimum MAC address using a valid hexadecimal value.")] String MacAddressMinimum; + [Write, Description("Specifies the maximum number of storage migrations that can be performed at the same time on the Hyper-V host.")] Uint32 MaximumStorageMigrations; + [Write, Description("Specifies the maximum number of live migrations that can be performed at the same time on the Hyper-V host.")] Uint32 MaximumVirtualMachineMigrations; + [Write, Description("Specifies whether virtual machines on the Hyper-V host can use resources from more than one NUMA node.")] Boolean NumaSpanningEnabled; + [Write, Description("Specifies how often the Hyper-V host saves the data that tracks resource usage. The range is a minimum of 60 minutes to a maximum of 1440 (24 hours).")] Uint32 ResourceMeteringSaveIntervalMinute; + [Write, Description("Specifies how networks are selected for incoming live migration traffic.")] Boolean UseAnyNetworkForMigration; + [Write, Description("Specifies the default folder to store virtual hard disks on the Hyper-V host.")] String VirtualHardDiskPath; + [Write, Description("Specifies the type of authentication to be used for live migrations. The acceptable values for this parameter are 'Kerberos' and 'CredSSP'."), ValueMap{"CredSSP","Kerberos"}, Values{"CredSSP","Kerberos"}] String VirtualMachineMigrationAuthenticationType; + [Write, Description("Specifies the performance option to use for live migration. The acceptable values for this parameter are 'TCPIP', 'Compression' and 'SMB'."), ValueMap{"TCPIP","Compression","SMB"}, Values{"TCPIP","Compression","SMB"}] String VirtualMachineMigrationPerformanceOption; + [Write, Description("Specifies the default folder to store virtual machine configuration files on the Hyper-V host.")] String VirtualMachinePath; + [Write, Description("Indicates whether Live Migration should be enabled or disabled on the Hyper-V host.")] Boolean VirtualMachineMigrationEnabled; +}; diff --git a/deployment/dsc/azshcihost/xHyper-V/3.17.0.0/DSCResources/MSFT_xVMHost/en-US/MSFT_xVMHost.psd1 b/deployment/dsc/azshcihost/xHyper-V/3.17.0.0/DSCResources/MSFT_xVMHost/en-US/MSFT_xVMHost.psd1 new file mode 100644 index 0000000..3e25077 --- /dev/null +++ b/deployment/dsc/azshcihost/xHyper-V/3.17.0.0/DSCResources/MSFT_xVMHost/en-US/MSFT_xVMHost.psd1 @@ -0,0 +1,11 @@ +ConvertFrom-StringData @' + QueryingVMHost = Querying VM host configuration. + PropertyMismatch = Property '{0}' mismatch; expected value '{1}', but was '{2}'. + VMHostInDesiredState = VM host in desired state. + VMHostNotInDesiredState = VM host not in desired state. + UpdatingVMHostProperties = Updating VM host properties. + VMHostPropertiesUpdated = VM host properties have been updated. + EnableLiveMigration = Enabling VM Live Migration. + DisableLiveMigration = Disabling VM Live Migration. + LiveMigrationDomainOnly = Enabling VM Live Migration requires the computer to be joined to an Active Directory domain. +'@ diff --git a/deployment/dsc/azshcihost/xHyper-V/3.17.0.0/DSCResources/MSFT_xVMHyperV/MSFT_xVMHyperV.psm1 b/deployment/dsc/azshcihost/xHyper-V/3.17.0.0/DSCResources/MSFT_xVMHyperV/MSFT_xVMHyperV.psm1 new file mode 100644 index 0000000..2b32f64 --- /dev/null +++ b/deployment/dsc/azshcihost/xHyper-V/3.17.0.0/DSCResources/MSFT_xVMHyperV/MSFT_xVMHyperV.psm1 @@ -0,0 +1,988 @@ +#region localizeddata +if (Test-Path "${PSScriptRoot}\${PSUICulture}") +{ + Import-LocalizedData -BindingVariable localizedData -Filename MSFT_xVMHyperV.psd1 ` + -BaseDirectory "${PSScriptRoot}\${PSUICulture}" +} +else +{ + # fallback to en-US + Import-LocalizedData -BindingVariable localizedData -Filename MSFT_xVMHyperV.psd1 ` + -BaseDirectory "${PSScriptRoot}\en-US" +} +#endregion + +# Import the common HyperV functions +Import-Module -Name ( Join-Path ` + -Path (Split-Path -Path $PSScriptRoot -Parent) ` + -ChildPath '\HyperVCommon\HyperVCommon.psm1' ) + +function Get-TargetResource +{ + [CmdletBinding()] + [OutputType([System.Collections.Hashtable])] + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $Name, + + [Parameter(Mandatory = $true)] + [System.String] + $VhdPath + ) + + Write-Verbose -Message ($localizedData.QueryingVM -f $Name) + + # Check if Hyper-V module is present for Hyper-V cmdlets + if (!(Get-Module -ListAvailable -Name Hyper-V)) + { + Throw ($localizedData.RoleMissingError -f 'Hyper-V') + } + + $vmobj = Get-VM -Name $Name -ErrorAction SilentlyContinue + + # Check if 1 or 0 VM with name = $name exist + if ($vmobj.count -gt 1) + { + Throw ($localizedData.MoreThanOneVMExistsError -f $Name) + } + + <# + Retrieve the Vhd hierarchy to ensure we enumerate snapshots/differencing disks + Fixes #28 + #> + if ($null -ne $vmobj) + { + $vhdChain = @(Get-VhdHierarchy -VhdPath ($vmObj.HardDrives[0].Path)) + } + + $vmSecureBootState = $false + if ($vmobj.Generation -eq 2) + { + # Retrieve secure boot status (can only be enabled on Generation 2 VMs) and convert to a boolean. + $vmSecureBootState = ($vmobj | Get-VMFirmware).SecureBoot -eq 'On' + } + + $guestServiceId = 'Microsoft:{0}\6C09BB55-D683-4DA0-8931-C9BF705F6480' -f $vmObj.Id + + $macAddress = @() + $switchName = @() + $ipAddress = @() + + foreach ($networkAdapter in $vmobj.NetworkAdapters) + { + $macAddress += $networkAdapter.MacAddress + + if (-Not ([string]::IsNullOrEmpty($networkAdapter.SwitchName))) + { + $switchName += $networkAdapter.SwitchName + } + + if ($networkAdapter.IPAddresses.Count -ge 1) + { + $ipAddress += $networkAdapter.IPAddresses + } + } + + @{ + Name = $Name + # Return the Vhd specified if it exists in the Vhd chain + VhdPath = if ($vhdChain -contains $VhdPath) { $VhdPath } else { $null } + SwitchName = $switchName + State = $vmobj.State + Path = $vmobj.Path + Generation = $vmobj.Generation + SecureBoot = $vmSecureBootState + StartupMemory = $vmobj.MemoryStartup + MinimumMemory = $vmobj.MemoryMinimum + MaximumMemory = $vmobj.MemoryMaximum + MACAddress = $macAddress + ProcessorCount = $vmobj.ProcessorCount + Ensure = if ($vmobj) { 'Present'} else { 'Absent' } + ID = $vmobj.Id + Status = $vmobj.Status + CPUUsage = $vmobj.CPUUsage + MemoryAssigned = $vmobj.MemoryAssigned + Uptime = $vmobj.Uptime + CreationTime = $vmobj.CreationTime + HasDynamicMemory = $vmobj.DynamicMemoryEnabled + NetworkAdapters = $ipAddress + EnableGuestService = ($vmobj | Get-VMIntegrationService | Where-Object -FilterScript {$_.Id -eq $guestServiceId}).Enabled + AutomaticCheckpointsEnabled = $vmobj.AutomaticCheckpointsEnabled + } +} + +function Set-TargetResource +{ + [CmdletBinding()] + param + ( + # Name of the VM + [Parameter(Mandatory = $true)] + [String] + $Name, + + # VHD associated with the VM + [Parameter(Mandatory = $true)] + [String] + $VhdPath, + + # Virtual switch associated with the VM + [Parameter()] + [String[]] + $SwitchName, + + # State of the VM + [Parameter()] + [ValidateSet('Running', 'Paused', 'Off')] + [String] + $State, + + # Folder where the VM data will be stored + [Parameter()] + [String] + $Path, + + # Virtual machine generation + [Parameter()] + [ValidateRange(1, 2)] + [UInt32] + $Generation = 1, + + # Startup RAM for the VM + [Parameter()] + [ValidateRange(32MB, 65536MB)] + [UInt64] + $StartupMemory, + + # Minimum RAM for the VM. This enables dynamic memory + [Parameter()] + [ValidateRange(32MB, 65536MB)] + [UInt64] + $MinimumMemory, + + # Maximum RAM for the VM. This enables dynamic memory + [Parameter()] + [ValidateRange(32MB, 1048576MB)] + [UInt64] + $MaximumMemory, + + # MAC address of the VM + [Parameter()] + [String[]] + $MACAddress, + + # Processor count for the VM + [Parameter()] + [UInt32] + $ProcessorCount, + + # Waits for VM to get valid IP address + [Parameter()] + [Boolean] + $WaitForIP, + + # If specified, shutdowns and restarts the VM as needed for property changes + [Parameter()] + [Boolean] + $RestartIfNeeded, + + # Should the VM be created or deleted + [Parameter()] + [ValidateSet('Present', 'Absent')] + [String] + $Ensure = 'Present', + + [Parameter()] + [System.String] + $Notes, + + # Enable secure boot for Generation 2 VMs + [Parameter()] + [Boolean] + $SecureBoot = $true, + + # Enable Guest Services + [Parameter()] + [Boolean] + $EnableGuestService = $false, + + # Enable AutomaticCheckpoints + [Parameter()] + [Boolean] + $AutomaticCheckpointsEnabled + ) + + # Check if Hyper-V module is present for Hyper-V cmdlets + if (!(Get-Module -ListAvailable -Name Hyper-V)) + { + Throw ($localizedData.RoleMissingError -f 'Hyper-V') + } + + # Check if AutomaticCheckpointsEnabled is set in configuration + if ($PSBoundParameters.ContainsKey('AutomaticCheckpointsEnabled')) + { + <# + Check if AutomaticCheckpoints are supported + If AutomaticCheckpoints are supported, parameter exists on Set-VM + #> + if (-Not (Get-Command -Name Set-VM -Module Hyper-V).Parameters.ContainsKey('AutomaticCheckpointsEnabled')) + { + Throw ($localizedData.AutomaticCheckpointsUnsupported) + } + } + + Write-Verbose -Message ($localizedData.CheckingVMExists -f $Name) + $vmObj = Get-VM -Name $Name -ErrorAction SilentlyContinue + + # VM already exists + if ($vmObj) + { + Write-Verbose -Message ($localizedData.VMExists -f $Name) + + # If VM shouldn't be there, stop it and remove it + if ($Ensure -eq 'Absent') + { + Write-Verbose -Message ($localizedData.VMPropertyShouldBe -f 'Ensure', $Ensure, 'Present') + Get-VM $Name | Stop-VM -Force -Passthru -WarningAction SilentlyContinue | Remove-VM -Force + Write-Verbose -Message ($localizedData.VMPropertySet -f 'Ensure', $Ensure) + } + + <# + If VM is present, check its state, startup memory, minimum memory, maximum memory,processor count, automatic checkpoint and mac address + One cannot set the VM's vhdpath, path, generation and switchName after creation + #> + else + { + # If state has been specified and the VM is not in right state, set it to right state + if ($State -and ($vmObj.State -ne $State)) + { + Write-Verbose -Message ($localizedData.VMPropertyShouldBe -f 'State', $State, $vmObj.State) + Set-VMState -Name $Name -State $State -WaitForIP $WaitForIP + Write-Verbose -Message ($localizedData.VMPropertySet -f 'State', $State) + } + + $changeProperty = @{} + # If the VM does not have the right startup memory + if ($PSBoundParameters.ContainsKey('StartupMemory') -and ($vmObj.MemoryStartup -ne $StartupMemory)) + { + Write-Verbose -Message ($localizedData.VMPropertyShouldBe -f 'MemoryStartup', $StartupMemory, $vmObj.MemoryStartup) + $changeProperty['MemoryStartup'] = $StartupMemory + } + elseif ($PSBoundParameters.ContainsKey('MinimumMemory') -and ($vmObj.MemoryStartup -lt $MinimumMemory)) + { + Write-Verbose -Message ($localizedData.AdjustingLessThanMemoryWarning -f 'StartupMemory', $vmObj.MemoryStartup, 'MinimumMemory', $MinimumMemory) + $changeProperty['MemoryStartup'] = $MinimumMemory + } + elseif ($PSBoundParameters.ContainsKey('MaximumMemory') -and ($vmObj.MemoryStartup -gt $MaximumMemory)) + { + Write-Verbose -Message ($localizedData.AdjustingGreaterThanMemoryWarning -f 'StartupMemory', $vmObj.MemoryStartup, 'MaximumMemory', $MaximumMemory) + $changeProperty['MemoryStartup'] = $MaximumMemory + } + + # If the VM does not have the right minimum or maximum memory, stop the VM, set the right memory, start the VM + if ($PSBoundParameters.ContainsKey('MinimumMemory') -or $PSBoundParameters.ContainsKey('MaximumMemory')) + { + $changeProperty['DynamicMemory'] = $true + $changeProperty['StaticMemory'] = $false + + if ($PSBoundParameters.ContainsKey('MinimumMemory') -and ($vmObj.Memoryminimum -ne $MinimumMemory)) + { + Write-Verbose -Message ($localizedData.VMPropertyShouldBe -f 'MinimumMemory', $MinimumMemory, $vmObj.MemoryMinimum) + $changeProperty['MemoryMinimum'] = $MinimumMemory + } + if ($PSBoundParameters.ContainsKey('MaximumMemory') -and ($vmObj.Memorymaximum -ne $MaximumMemory)) + { + Write-Verbose -Message ($localizedData.VMPropertyShouldBe -f 'MaximumMemory', $MaximumMemory, $vmObj.MemoryMaximum) + $changeProperty['MemoryMaximum'] = $MaximumMemory + } + } + + # If the VM does not have the right processor count, stop the VM, set the right memory, start the VM + if ($PSBoundParameters.ContainsKey('ProcessorCount') -and ($vmObj.ProcessorCount -ne $ProcessorCount)) + { + Write-Verbose -Message ($localizedData.VMPropertyShouldBe -f 'ProcessorCount', $ProcessorCount, $vmObj.ProcessorCount) + $changeProperty['ProcessorCount'] = $ProcessorCount + } + + # Stop the VM, set the right properties, start the VM only if there are properties to change + if ($changeProperty.Count -gt 0) + { + Set-VMProperty -Name $Name -VMCommand 'Set-VM' -ChangeProperty $changeProperty -WaitForIP $WaitForIP -RestartIfNeeded $RestartIfNeeded + Write-Verbose -Message ($localizedData.VMPropertiesUpdated -f $Name) + } + + <# + Special cases to disable dynamic memory: + - If startup, minimum and maximum memory are specified with equal values or + - If only startup memory is specified, but neither minimum nor maximum + #> + if ( ($PSBoundParameters.ContainsKey('StartupMemory') -and + ($StartupMemory -eq $MinimumMemory) -and + ($StartupMemory -eq $MaximumMemory) + ) -or + ( $PSBoundParameters.ContainsKey('StartupMemory') -and + (-not $PSBoundParameters.ContainsKey('MinimumMemory')) -and + (-not $PSBoundParameters.ContainsKey('MaximumMemory')) + ) + ) + { + # Refresh VM properties + $vmObj = Get-VM -Name $Name -ErrorAction SilentlyContinue + if ($vmObj.DynamicMemoryEnabled) + { + Write-Verbose -Message ($localizedData.VMPropertyShouldBe -f 'DynamicMemoryEnabled', $false, $vmObj.DynamicMemoryEnabled) + $setVMPropertyParams = @{ + VMName = $Name + VMCommand = 'Set-VM' + ChangeProperty = @{ + StaticMemory = $true + DynamicMemory = $false + } + WaitForIP = $WaitForIP + RestartIfNeeded = $RestartIfNeeded + } + Set-VMProperty @setVMPropertyParams + Write-Verbose -Message ($localizedData.VMPropertiesUpdated -f $Name) + } + } + + # Set VM network switches. This can be done while the VM is running. + for ($i = 0; $i -lt $SwitchName.Count; $i++) + { + $switch = $SwitchName[$i] + $nic = $vmObj.NetworkAdapters[$i] + if ($nic) + { + # We cannot change the MAC address whilst the VM is running.. This is changed later + if ($nic.SwitchName -ne $switch) + { + Write-Verbose -Message ($localizedData.VMPropertyShouldBe -f 'NIC', $switch, $nic.SwitchName) + $nic | Connect-VMNetworkAdapter -SwitchName $switch + Write-Verbose -Message ($localizedData.VMPropertySet -f 'NIC', $switch) + } + } + else + { + Write-Verbose -Message ($localizedData.VMPropertyShouldBe -f 'NIC', $switch, '<missing>') + if ($MACAddress -and (-not [System.String]::IsNullOrEmpty($MACAddress[$i]))) + { + Add-VMNetworkAdapter -VMName $Name -SwitchName $switch -StaticMacAddress $MACAddress[$i] + Write-Verbose -Message ($localizedData.VMPropertySet -f 'NIC', $switch) + } + else + { + Add-VMNetworkAdapter -VMName $Name -SwitchName $switch + Write-Verbose -Message ($localizedData.VMPropertySet -f 'NIC', $switch) + } + # Refresh the NICs after we've added one + $vmObj = Get-VM -Name $Name -ErrorAction SilentlyContinue + } + } + + # If the VM does not have the right MACAddress, stop the VM, set the right MACAddress, start the VM + for ($i = 0; $i -lt $MACAddress.Count; $i++) + { + $address = $MACAddress[$i] + $nic = $vmObj.NetworkAdapters[$i] + if ($nic.MacAddress -ne $address) + { + Write-Verbose -Message ($localizedData.VMPropertyShouldBe -f 'MACAddress', $address, $nic.MacAddress) + Set-VMMACAddress -Name $Name -NICIndex $i -MACAddress $address -WaitForIP $WaitForIP -RestartIfNeeded $RestartIfNeeded + } + } + + if ($Generation -eq 2) + { + # Retrive the current secure boot state + $vmSecureBoot = Test-VMSecureBoot -Name $Name + if ($SecureBoot -ne $vmSecureBoot) + { + Write-Verbose -Message ($localizedData.VMPropertyShouldBe -f 'SecureBoot', $SecureBoot, $vmSecureBoot) + + if (-not $SecureBoot) + { + $enableSecureBoot = 'On' + } + else + { + $enableSecureBoot = 'Off' + } + + # Cannot change the secure boot state whilst the VM is powered on. + $setVMPropertyParams = @{ + VMName = $Name + VMCommand = 'Set-VMFirmware' + ChangeProperty = @{ EnableSecureBoot = $enableSecureBoot } + RestartIfNeeded = $RestartIfNeeded + } + Set-VMProperty @setVMPropertyParams + Write-Verbose -Message ($localizedData.VMPropertySet -f 'SecureBoot', $SecureBoot) + } + } + + if ($Notes -ne $null) + { + # If the VM notes do not match the desire notes, update them. This can be done while the VM is running. + if ($vmObj.Notes -ne $Notes) + { + Set-Vm -Name $Name -Notes $Notes + } + } + + # If the VM doesn't have Guest Service Interface correctly configured, update it. + $guestServiceId = 'Microsoft:{0}\6C09BB55-D683-4DA0-8931-C9BF705F6480' -f $vmObj.Id + + $guestService = $vmObj | Get-VMIntegrationService | Where-Object -FilterScript {$_.Id -eq $guestServiceId} + if ($guestService.Enabled -eq $false -and $EnableGuestService) + { + Write-Verbose -Message ($localizedData.VMPropertyShouldBe -f 'EnableGuestService', $EnableGuestService, $guestService.Enabled) + $guestService | Enable-VMIntegrationService + Write-Verbose -Message ($localizedData.VMPropertySet -f 'EnableGuestService', $EnableGuestService) + } + elseif ($guestService.Enabled -and -not $EnableGuestService) + { + Write-Verbose -Message ($localizedData.VMPropertyShouldBe -f 'EnableGuestService', $EnableGuestService, $guestService.Enabled) + $guestService | Disable-VMIntegrationService + Write-Verbose -Message ($localizedData.VMPropertySet -f 'EnableGuestService', $EnableGuestService) + } + + # If AutomaticCheckpointsEnabled is set in configuration + if ($PSBoundParameters.ContainsKey('AutomaticCheckpointsEnabled')) + { + if ($vmObj.AutomaticCheckpointsEnabled -ne $AutomaticCheckpointsEnabled) + { + Set-VM -Name $Name -AutomaticCheckpointsEnabled $AutomaticCheckpointsEnabled + } + } + } + } + + # VM is not present, create one + else + { + Write-Verbose -Message ($localizedData.VMDoesNotExist -f $Name) + if ($Ensure -eq 'Present') + { + Write-Verbose -Message ($localizedData.CreatingVM -f $Name) + + $parameters = @{} + $parameters['Name'] = $Name + $parameters['VHDPath'] = $VhdPath + $parameters['Generation'] = $Generation + + # Optional parameters + if ($SwitchName) + { + $parameters['SwitchName'] = $SwitchName[0] + } + if ($Path) + { + $parameters['Path'] = $Path + } + $defaultStartupMemory = 512MB + if ($PSBoundParameters.ContainsKey('StartupMemory')) + { + $parameters['MemoryStartupBytes'] = $StartupMemory + } + elseif ($PSBoundParameters.ContainsKey('MinimumMemory') -and ($defaultStartupMemory -lt $MinimumMemory)) + { + $parameters['MemoryStartupBytes'] = $MinimumMemory + } + elseif ($PSBoundParameters.ContainsKey('MaximumMemory') -and ($defaultStartupMemory -gt $MaximumMemory)) + { + $parameters['MemoryStartupBytes'] = $MaximumMemory + } + $null = New-VM @parameters + + $parameters = @{} + $parameters['Name'] = $Name + $parameters['StaticMemory'] = $true + $parameters['DynamicMemory'] = $false + if ($PSBoundParameters.ContainsKey('MinimumMemory') -or $PSBoundParameters.ContainsKey('MaximumMemory')) + { + $parameters['DynamicMemory'] = $true + $parameters['StaticMemory'] = $false + if ($PSBoundParameters.ContainsKey('MinimumMemory')) + { + $parameters['MemoryMinimumBytes'] = $MinimumMemory + } + if ($PSBoundParameters.ContainsKey('MaximumMemory')) + { + $parameters['MemoryMaximumBytes'] = $MaximumMemory + } + } + + if ($Notes) + { + $parameters['Notes'] = $Notes + } + + if ($PSBoundParameters.ContainsKey('ProcessorCount')) + { + $parameters['ProcessorCount'] = $ProcessorCount + } + + # If AutomaticCheckpointsEnabled is set in configuration + if ($PSBoundParameters.ContainsKey('AutomaticCheckpointsEnabled')) + { + $parameters['AutomaticCheckpointsEnabled'] = $AutomaticCheckpointsEnabled + } + + $null = Set-VM @parameters + + # Special case: Disable dynamic memory if startup, minimum and maximum memory are equal + if ($PSBoundParameters.ContainsKey('StartupMemory') -and + ($StartupMemory -eq $MinimumMemory) -and + ($StartupMemory -eq $MaximumMemory)) + { + Set-VMMemory -VMName $Name -DynamicMemoryEnabled $false + } + + # There's always a NIC added with New-VM + if ($MACAddress) + { + Set-VMNetworkAdapter -VMName $Name -StaticMacAddress $MACAddress[0] + } + + # Add additional NICs + for ($i = 1; $i -lt $SwitchName.Count; $i++) + { + $addVMNetworkAdapterParams = @{ + VMName = $Name + SwitchName = $SwitchName[$i] + } + if ($MACAddress -and (-not [System.String]::IsNullOrEmpty($MACAddress[$i]))) + { + $addVMNetworkAdapterParams['StaticMacAddress'] = $MACAddress[$i] + } + Add-VMNetworkAdapter @addVMNetworkAdapterParams + Write-Verbose -Message ($localizedData.VMPropertySet -f 'NIC', $SwitchName[$i]) + } + + if ($Generation -eq 2) + { + <# + Secure boot is only applicable to Generation 2 VMs and it defaults to on. + Therefore, we only need to explicitly set it to off if specified. + #> + if ($SecureBoot -eq $false) + { + Set-VMFirmware -VMName $Name -EnableSecureBoot Off + } + } + + if ($EnableGuestService) + { + $guestServiceId = 'Microsoft:{0}\6C09BB55-D683-4DA0-8931-C9BF705F6480' -f (Get-VM -Name $Name).Id + Get-VMIntegrationService -VMName $Name | Where-Object -FilterScript {$_.Id -eq $guestServiceId} | Enable-VMIntegrationService + } + + Write-Verbose -Message ($localizedData.VMCreated -f $Name) + + if ($State) + { + Set-VMState -Name $Name -State $State -WaitForIP $WaitForIP + Write-Verbose -Message ($localizedData.VMPropertySet -f 'State', $State) + } + + } + } +} + +function Test-TargetResource +{ + [CmdletBinding()] + [OutputType([System.Boolean])] + param + ( + # Name of the VM + [Parameter(Mandatory = $true)] + [String] + $Name, + + # VHD associated with the VM + [Parameter(Mandatory = $true)] + [String] + $VhdPath, + + # Virtual switch associated with the VM + [Parameter()] + [String[]] + $SwitchName, + + # State of the VM + [Parameter()] + [ValidateSet('Running', 'Paused', 'Off')] + [String] + $State, + + # Folder where the VM data will be stored + [Parameter()] + [String] + $Path, + + # Virtual machine generation + [Parameter()] + [ValidateRange(1, 2)] + [UInt32] + $Generation = 1, + + # Startup RAM for the VM + [Parameter()] + [ValidateRange(32MB, 65536MB)] + [UInt64] + $StartupMemory, + + # Minimum RAM for the VM. This enables dynamic memory + [Parameter()] + [ValidateRange(32MB, 65536MB)] + [UInt64] + $MinimumMemory, + + # Maximum RAM for the VM. This enables dynamic memory + [Parameter()] + [ValidateRange(32MB, 1048576MB)] + [UInt64] + $MaximumMemory, + + # MAC address of the VM + [Parameter()] + [String[]] + $MACAddress, + + # Processor count for the VM + [Parameter()] + [UInt32] + $ProcessorCount, + + # Waits for VM to get valid IP address + [Parameter()] + [Boolean] + $WaitForIP, + + # If specified, shutdowns and restarts the VM as needed for property changes + [Parameter()] + [Boolean] + $RestartIfNeeded, + + # Should the VM be created or deleted + [Parameter()] + [ValidateSet('Present', 'Absent')] + [String] + $Ensure = 'Present', + + [Parameter()] + [System.String] + $Notes, + + # Enable secure boot for Generation 2 VMs + [Parameter()] + [Boolean] + $SecureBoot = $true, + + [Parameter()] + [Boolean] + $EnableGuestService = $false, + + # Enable AutomaticCheckpoints + [Parameter()] + [Boolean] + $AutomaticCheckpointsEnabled + ) + + # Check if Hyper-V module is present for Hyper-V cmdlets + if (!(Get-Module -ListAvailable -Name Hyper-V)) + { + Throw ($localizedData.RoleMissingError -f 'Hyper-V') + } + + # Check if 1 or 0 VM with name = $name exist + if ((Get-VM -Name $Name -ErrorAction SilentlyContinue).count -gt 1) + { + Throw ($localizedData.MoreThanOneVMExistsError -f $Name) + } + + # Check if AutomaticCheckpointsEnabled is set in configuration + if ($PSBoundParameters.ContainsKey('AutomaticCheckpointsEnabled')) + { + <# + Check if AutomaticCheckpoints are supported + If AutomaticCheckpoints are supported, parameter exists on Set-VM + #> + if (-Not (Get-Command -Name Set-VM -Module Hyper-V).Parameters.ContainsKey('AutomaticCheckpointsEnabled')) + { + Throw ($localizedData.AutomaticCheckpointsUnsupported) + } + } + + try + { + $vmObj = Get-VM -Name $Name -ErrorAction Stop + if ($Ensure -eq 'Present') + { + # Check if $VhdPath exist + if (!(Test-Path $VhdPath)) + { + Throw ($localizedData.VhdPathDoesNotExistError -f $VhdPath) + } + + # Check if Minimum memory is less than StartUpmemory + if ($PSBoundParameters.ContainsKey('StartupMemory') -and + $PSBoundParameters.ContainsKey('MinimumMemory') -and + ($MinimumMemory -gt $StartupMemory)) + { + Throw ($localizedData.MinMemGreaterThanStartupMemError -f $MinimumMemory, $StartupMemory) + } + + # Check if Minimum memory is greater than Maximummemory + if ($PSBoundParameters.ContainsKey('MaximumMemory') -and + $PSBoundParameters.ContainsKey('MinimumMemory') -and + ($MinimumMemory -gt $MaximumMemory)) + { + Throw ($localizedData.MinMemGreaterThanMaxMemError -f $MinimumMemory, $MaximumMemory) + } + + # Check if Startup memory is greater than Maximummemory + if ($PSBoundParameters.ContainsKey('MaximumMemory') -and + $PSBoundParameters.ContainsKey('StartupMemory') -and + ($StartupMemory -gt $MaximumMemory)) + { + Throw ($localizedData.StartUpMemGreaterThanMaxMemError -f $StartupMemory, $MaximumMemory) + } + + <# + VM Generation has no direct relation to the virtual hard disk format and cannot be changed + after the virtual machine has been created. Generation 2 VMs do not support .VHD files. + #> + if (($Generation -eq 2) -and ($VhdPath.Split('.')[-1] -eq 'vhd')) + { + Throw ($localizedData.VhdUnsupportedOnGen2VMError) + } + + # Check if $Path exist + if ($Path -and !(Test-Path -Path $Path)) + { + Throw ($localizedData.PathDoesNotExistError -f $Path) + } + + $vhdChain = @(Get-VhdHierarchy -VhdPath ($vmObj.HardDrives[0].Path)) + if ($vhdChain -notcontains $VhdPath) + { + Write-Verbose -Message ($localizedData.VMPropertyShouldBe -f 'VhdPath', $VhdPath, ($vhdChain -join ',')) + return $false + } + + if ($state -and ($vmObj.State -ne $State)) + { + return $false + } + + if ($PSBoundParameters.ContainsKey('StartupMemory') -and + ($vmObj.MemoryStartup -ne $StartupMemory)) + { + return $false + } + + if ($PSBoundParameters.ContainsKey('MaximumMemory') -and + ($vmObj.MemoryMaximum -ne $MaximumMemory)) + { + return $false + } + + if ($PSBoundParameters.ContainsKey('MinimumMemory') -and + ($vmObj.MemoryMinimum -ne $MinimumMemory)) + { + return $false + } + + # If startup memory but neither minimum nor maximum memory specified, dynamic memory should be disabled + if ($PSBoundParameters.ContainsKey('StartupMemory') -and + ( -not $PSBoundParameters.ContainsKey('MinimumMemory')) -and + ( -not $PSBoundParameters.ContainsKey('MaximumMemory')) -and + $vmobj.DynamicMemoryEnabled) + { + return $false + } + + # If startup, minimum and maximum memory are specified with the same values, dynamic memory should be disabled + if ($PSBoundParameters.ContainsKey('StartupMemory') -and + $PSBoundParameters.ContainsKey('MinimumMemory') -and + $PSBoundParameters.ContainsKey('MaximumMemory') -and + ($StartupMemory -eq $MinimumMemory) -and + ($StartupMemory -eq $MaximumMemory) -and + $vmobj.DynamicMemoryEnabled) + { + return $false + } + + if ($vmObj.HardDrives.Path -notcontains $VhdPath) + { + return $false + } + + for ($i = 0; $i -lt $SwitchName.Count; $i++) + { + if ($vmObj.NetworkAdapters[$i].SwitchName -ne $SwitchName[$i]) + { + Write-Verbose -Message ($localizedData.VMPropertyShouldBe -f 'SwitchName', $SwitchName[$i], $vmObj.NetworkAdapters[$i].SwitchName) + return $false + } + } + + for ($i = 0; $i -lt $MACAddress.Count; $i++) + { + if ($vmObj.NetworkAdapters[$i].MACAddress -ne $MACAddress[$i]) + { + Write-Verbose -Message ($localizedData.VMPropertyShouldBe -f 'MACAddress', $MACAddress[$i], $vmObj.NetworkAdapters[$i].MACAddress) + return $false + } + } + + # $Generation always exists, only check if parameter has been explicitly specified + if ($PSBoundParameters.ContainsKey('Generation') -and ($Generation -ne $vmObj.Generation)) + { + return $false + } + + if ($PSBoundParameters.ContainsKey('ProcessorCount') -and ($vmObj.ProcessorCount -ne $ProcessorCount)) + { + return $false + } + + if ($vmObj.Generation -eq 2) + { + $vmSecureBoot = Test-VMSecureBoot -Name $Name + if ($SecureBoot -ne $vmSecureBoot) + { + Write-Verbose -Message ($localizedData.VMPropertyShouldBe -f 'SecureBoot', $SecureBoot, $vmSecureBoot) + return $false + } + } + + $guestServiceId = 'Microsoft:{0}\6C09BB55-D683-4DA0-8931-C9BF705F6480' -f $vmObj.Id + $guestService = $vmObj | Get-VMIntegrationService | Where-Object -FilterScript {$_.Id -eq $guestServiceId} + if ($guestService.Enabled -ne $EnableGuestService) + { + Write-Verbose -Message ($localizedData.VMPropertyShouldBe -f 'EnableGuestService', $EnableGuestService, $guestService.Enabled) + return $false + } + + # If AutomaticCheckpointsEnabled is set in configuration + if ($PSBoundParameters.ContainsKey('AutomaticCheckpointsEnabled')) + { + if ($vmObj.AutomaticCheckpointsEnabled -ne $AutomaticCheckpointsEnabled) + { + Write-Verbose -Message ($localizedData.VMPropertyShouldBe -f 'AutomaticCheckpointsEnabled', $AutomaticCheckpointsEnabled, $vmObj.AutomaticCheckpointsEnabled) + return $false + } + } + + return $true + } + else + { + return $false + } + } + catch [System.Management.Automation.ActionPreferenceStopException] + { + ($Ensure -eq 'Absent') + } +} + +#region Helper function + +# Returns VM VHDs, including snapshots and differencing disks +function Get-VhdHierarchy +{ + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $VhdPath + ) + + $vmVhdPath = Get-VHD -Path $VhdPath + Write-Output -InputObject $vmVhdPath.Path + while (-not [System.String]::IsNullOrEmpty($vmVhdPath.ParentPath)) + { + $vmVhdPath.ParentPath + $vmVhdPath = (Get-VHD -Path $vmVhdPath.ParentPath) + } +} + +<# + The 'Set-VMProperty' method cannot be used as it cannot deal with piped + command in it's current implementation +#> +function Set-VMMACAddress +{ + param + ( + [Parameter(Mandatory = $true)] + [String] + $Name, + + [Parameter(Mandatory = $true)] + [String] + $MACAddress, + + [Parameter(Mandatory = $true)] + [Int] + $NICIndex, + + [Parameter()] + [Boolean] + $WaitForIP, + + [Parameter()] + [Boolean] + $RestartIfNeeded + ) + $vmObj = Get-VM -Name $Name + $originalState = $vmObj.state + if ($originalState -ne 'Off' -and $RestartIfNeeded) + { + Set-VMState -Name $Name -State Off + $vmObj.NetworkAdapters[$NICIndex] | Set-VMNetworkAdapter -StaticMacAddress $MACAddress + + # Can not move a off VM to paused, but only to running state + if ($originalState -eq 'Running') + { + Set-VMState -Name $Name -State Running -WaitForIP $WaitForIP + } + + # Cannot make a paused VM to go back to Paused state after turning Off + if ($originalState -eq 'Paused') + { + Write-Warning -Message ($localizedData.VMStateWillBeOffWarning -f $Name) + } + } + elseif ($originalState -eq 'Off') + { + $vmObj.NetworkAdapters[$NICIndex] | Set-VMNetworkAdapter -StaticMacAddress $MACAddress + Write-Verbose -Message ($localizedData.VMPropertySet -f 'MACAddress', $MACAddress) + } + else + { + Write-Error -Message ($localizedData.CannotUpdatePropertiesOnlineError -f $Name, $vmObj.State) + } +} + +function Test-VMSecureBoot +{ + param + ( + [Parameter(Mandatory = $true)] + [string] + $Name + ) + $vm = Get-VM -Name $Name + return (Get-VMFirmware -VM $vm).SecureBoot -eq 'On' +} + +#endregion + +Export-ModuleMember -Function *-TargetResource diff --git a/deployment/dsc/azshcihost/xHyper-V/3.17.0.0/DSCResources/MSFT_xVMHyperV/MSFT_xVMHyperV.schema.mof b/deployment/dsc/azshcihost/xHyper-V/3.17.0.0/DSCResources/MSFT_xVMHyperV/MSFT_xVMHyperV.schema.mof new file mode 100644 index 0000000..7a2541e --- /dev/null +++ b/deployment/dsc/azshcihost/xHyper-V/3.17.0.0/DSCResources/MSFT_xVMHyperV/MSFT_xVMHyperV.schema.mof @@ -0,0 +1,30 @@ +[ClassVersion("1.0.0"), FriendlyName("xVMHyperV")] +class MSFT_xVMHyperV : OMI_BaseResource +{ + [Key, Description("Name of the VM")] String Name; + [Required, Description("VHD associated with the VM")] String VhdPath; + [Write, Description("Virtual switch(es) associated with the VM")] String SwitchName[]; + [Write, Description("State of the VM."), ValueMap{"Running","Paused","Off"}, Values{"Running","Paused","Off"}] String State; + [Write, Description("Folder where the VM data will be stored")] String Path; + [Write, Description("Virtual machine generation")] Uint32 Generation; + [Write, Description("Startup RAM for the VM.")] Uint64 StartupMemory; + [Write, Description("Minimum RAM for the VM. This enables dynamic memory.")] Uint64 MinimumMemory; + [Write, Description("Maximum RAM for the VM. This enable dynamic memory.")] Uint64 MaximumMemory; + [Write, Description("MAC address(es) of the VM NICs.")] String MACAddress[]; + [Write, Description("Processor count for the VM")] Uint32 ProcessorCount; + [Write, Description("Waits for VM to get valid IP address.")] Boolean WaitForIP; + [Write, Description("If specified, shutdowns and restarts the VM as needed for property changes")] Boolean RestartIfNeeded; + [Write, Description("Should the VM be created or deleted"), ValueMap{"Present","Absent"}, Values{"Present","Absent"}] String Ensure; + [Write, Description("Notes about the VM.")] String Notes; + [Write, Description("Enable secure boot for Generation 2 VMs.")] Boolean SecureBoot; + [Write, Description("Enable Guest Service Interface for the VM.")] Boolean EnableGuestService; + [Write, Description("Enable AutomaticCheckpoints for the VM.")] Boolean AutomaticCheckpointsEnabled; + [Read, Description("VM unique ID")] String ID; + [Read, Description("Status of the VM")] String Status; + [Read, Description("CPU Usage of the VM")] Uint32 CPUUsage; + [Read, Description("Memory assigned to the VM")] Uint64 MemoryAssigned; + [Read, Description("Uptime of the VM")] String Uptime; + [Read, Description("Creation time of the VM")] DateTime CreationTime; + [Read, Description("Does VM has dynamic memory enabled")] Boolean HasDynamicMemory; + [Read, Description("Network adapters' IP addresses of the VM")] String NetworkAdapters[]; +}; diff --git a/deployment/dsc/azshcihost/xHyper-V/3.17.0.0/DSCResources/MSFT_xVMHyperV/en-US/MSFT_xVMHyperV.psd1 b/deployment/dsc/azshcihost/xHyper-V/3.17.0.0/DSCResources/MSFT_xVMHyperV/en-US/MSFT_xVMHyperV.psd1 new file mode 100644 index 0000000..67d0b3d --- /dev/null +++ b/deployment/dsc/azshcihost/xHyper-V/3.17.0.0/DSCResources/MSFT_xVMHyperV/en-US/MSFT_xVMHyperV.psd1 @@ -0,0 +1,28 @@ +ConvertFrom-StringData @' + RoleMissingError = Please ensure that '{0}' role is installed with its PowerShell module. + MoreThanOneVMExistsError = More than one VM with the name '{0}' exists. + PathDoesNotExistError = Path '{0}' does not exist. + VhdPathDoesNotExistError = Vhd '{0}' does not exist. + MinMemGreaterThanStartupMemError = MinimumMemory '{0}' should not be greater than StartupMemory '{1}' + MinMemGreaterThanMaxMemError = MinimumMemory '{0}' should not be greater than MaximumMemory '{1}' + StartUpMemGreaterThanMaxMemError = StartupMemory '{0}' should not be greater than MaximumMemory '{1}'. + VhdUnsupportedOnGen2VMError = Generation 2 virtual machines do not support the .VHD virtual disk extension. + CannotUpdatePropertiesOnlineError = Can not change properties for VM '{0}' in '{1}' state unless 'RestartIfNeeded' is set to true. + AutomaticCheckpointsUnsupported = AutomaticCheckpoints are not supported on this host. + + AdjustingGreaterThanMemoryWarning = VM {0} '{1}' is greater than {2} '{3}'. Adjusting {0} to be '{3}'. + AdjustingLessThanMemoryWarning = VM {0} '{1}' is less than {2} '{3}'. Adjusting {0} to be '{3}'. + VMStateWillBeOffWarning = VM '{0}' state will be 'OFF' and not 'Paused'. + + CheckingVMExists = Checking if VM '{0}' exists ... + VMExists = VM '{0}' exists. + VMDoesNotExist = VM '{0}' does not exist. + CreatingVM = Creating VM '{0}' ... + VMCreated = VM '{0}' created. + VMPropertyShouldBe = VM property '{0}' should be '{1}', actual '{2}'. + VMPropertySet = VM property '{0}' is '{1}'. + VMPropertiesUpdated = VM '{0}' properties have been updated. + WaitingForVMIPAddress = Waiting for IP Address for VM '{0}' ... + + QueryingVM = Querying VM '{0}'. +'@ diff --git a/deployment/dsc/azshcihost/xHyper-V/3.17.0.0/DSCResources/MSFT_xVMNetworkAdapter/MSFT_xVMNetworkAdapter.psm1 b/deployment/dsc/azshcihost/xHyper-V/3.17.0.0/DSCResources/MSFT_xVMNetworkAdapter/MSFT_xVMNetworkAdapter.psm1 new file mode 100644 index 0000000..65ec9b5 --- /dev/null +++ b/deployment/dsc/azshcihost/xHyper-V/3.17.0.0/DSCResources/MSFT_xVMNetworkAdapter/MSFT_xVMNetworkAdapter.psm1 @@ -0,0 +1,681 @@ +#region localizeddata +if (Test-Path "${PSScriptRoot}\${PSUICulture}") +{ + Import-LocalizedData -BindingVariable LocalizedData -filename MSFT_xVMNetworkAdapter.psd1 ` + -BaseDirectory "${PSScriptRoot}\${PSUICulture}" +} +else +{ + #fallback to en-US + Import-LocalizedData -BindingVariable LocalizedData -filename MSFT_xVMNetworkAdapter.psd1 ` + -BaseDirectory "${PSScriptRoot}\en-US" +} +#endregion + +<# +.SYNOPSIS + Gets MSFT_xVMNetworkAdapter resource current state. + +.PARAMETER Id + Specifies an unique identifier for the network adapter. + +.PARAMETER Name + Specifies a name for the network adapter that needs to be connected to a VM or management OS. + +.PARAMETER SwitchName + Specifies the name of the switch to which the new VM network adapter will be connected. + +.PARAMETER VMName + Specifies the name of the VM to which the network adapter will be connected. + Specify VMName as ManagementOS if you wish to connect the adapter to host OS. + +.PARAMETER IpAddress + Specifies the IpAddress information for the network adapter. +#> +Function Get-TargetResource +{ + [CmdletBinding()] + [OutputType([System.Collections.Hashtable])] + Param ( + [Parameter(Mandatory)] + [String] $Id, + + [Parameter(Mandatory)] + [String] $Name, + + [Parameter(Mandatory)] + [String] $SwitchName, + + [Parameter(Mandatory)] + [String] $VMName + ) + + $configuration = @{ + Id = $Id + Name = $Name + SwitchName = $SwitchName + VMName = $VMName + } + + $arguments = @{ + Name = $Name + } + + if ($VMName -ne 'ManagementOS') + { + $arguments.Add('VMName',$VMName) + } + else + { + $arguments.Add('ManagementOS', $true) + $arguments.Add('SwitchName', $SwitchName) + } + + Write-Verbose -Message $localizedData.GetVMNetAdapter + $netAdapter = Get-VMNetworkAdapter @arguments -ErrorAction SilentlyContinue + + if ($netAdapter) + { + Write-Verbose -Message $localizedData.FoundVMNetAdapter + if ($VMName -eq 'ManagementOS') + { + $configuration.Add('MacAddress', $netAdapter.MacAddress) + $configuration.Add('DynamicMacAddress', $false) + } + elseif ($netAdapter.VMName) + { + $configuration.Add('MacAddress', $netAdapter.MacAddress) + $configuration.Add('DynamicMacAddress', $netAdapter.DynamicMacAddressEnabled) + } + + $networkInfo = Get-NetworkInformation -VMName $VMName -Name $Name + if($networkInfo) + { + $item = New-CimInstance -ClassName MSFT_xNetworkSettings -Property $networkInfo -Namespace root/microsoft/windows/desiredstateconfiguration -ClientOnly + $configuration.Add('NetworkSetting', $item) + } + + $configuration.Add('Ensure','Present') + + Write-Verbose -Message $localizedData.GetVMNetAdapterVlan + $netAdapterVlan = Get-VMNetworkAdapterVlan -VMNetworkAdapter $netAdapter + if ($netAdapterVlan.OperationMode -ne 'Untagged') + { + $configuration.Add('VlanId', $netAdapterVlan.AccessVlanId) + } + } + else + { + Write-Verbose -Message $localizedData.NoVMNetAdapterFound + $configuration.Add('Ensure','Absent') + } + + return $configuration +} + +<# +.SYNOPSIS + Sets MSFT_xVMNetworkAdapter resource state. + +.PARAMETER Id + Specifies an unique identifier for the network adapter. + +.PARAMETER Name + Specifies a name for the network adapter that needs to be connected to a VM or management OS. + +.PARAMETER SwitchName + Specifies the name of the switch to which the new VM network adapter will be connected. + +.PARAMETER VMName + Specifies the name of the VM to which the network adapter will be connected. + Specify VMName as ManagementOS if you wish to connect the adapter to host OS. + +.PARAMETER MacAddress + Specifies the MAC address for the network adapter. This is not applicable if VMName + is set to ManagementOS. Use this parameter to specify a static MAC address. + +.PARAMETER IpAddress + Specifies the IpAddress information for the network adapter. + +.PARAMETER VlanId + Specifies the Vlan Id for the network adapter. + +.PARAMETER Ensure + Specifies if the network adapter should be Present or Absent. +#> +Function Set-TargetResource +{ + [CmdletBinding()] + Param ( + [Parameter(Mandatory)] + [String] $Id, + + [Parameter(Mandatory)] + [String] $Name, + + [Parameter(Mandatory)] + [String] $SwitchName, + + [Parameter(Mandatory)] + [String] $VMName, + + [Parameter()] + [String] $MacAddress, + + [Parameter()] + [Microsoft.Management.Infrastructure.CimInstance] + $NetworkSetting, + + [Parameter()] + [String] $VlanId, + + [Parameter()] + [ValidateSet('Present','Absent')] + [String] $Ensure='Present' + ) + + $arguments = @{ + Name = $Name + } + + if ($VMName -ne 'ManagementOS') + { + $arguments.Add('VMName',$VMName) + } + else + { + $arguments.Add('ManagementOS', $true) + $arguments.Add('SwitchName', $SwitchName) + } + + Write-Verbose -Message $localizedData.GetVMNetAdapter + $netAdapterExists = Get-VMNetworkAdapter @arguments -ErrorAction SilentlyContinue + + if ($Ensure -eq 'Present') + { + if ($netAdapterExists) + { + Write-Verbose -Message $localizedData.FoundVMNetAdapter + if (($VMName -ne 'ManagementOS')) + { + if ($MacAddress) + { + if ($netAdapterExists.DynamicMacAddressEnabled) + { + Write-Verbose -Message $localizedData.EnableStaticMacAddress + $updateMacAddress = $true + } + elseif ($MacAddress -ne $netAdapterExists.StaicMacAddress) + { + Write-Verbose -Message $localizedData.EnableStaticMacAddress + $updateMacAddress = $true + } + } + else + { + if (-not $netAdapterExists.DynamicMacAddressEnabled) + { + Write-Verbose -Message $localizedData.EnableDynamicMacAddress + $updateMacAddress = $true + } + } + + if ($netAdapterExists.SwitchName -ne $SwitchName) + { + Write-Verbose -Message $localizedData.PerformSwitchConnect + Connect-VMNetworkAdapter -VMNetworkAdapter $netAdapterExists -SwitchName $SwitchName -ErrorAction Stop -Verbose + } + + if (($updateMacAddress)) + { + Write-Verbose -Message $localizedData.PerformVMNetModify + + $setArguments = @{ } + $setArguments.Add('VMNetworkAdapter',$netAdapterExists) + if ($MacAddress) + { + $setArguments.Add('StaticMacAddress',$MacAddress) + } + else + { + $setArguments.Add('DynamicMacAddress', $true) + } + Set-VMNetworkAdapter @setArguments -ErrorAction Stop + } + } + } + else + { + if ($VMName -ne 'ManagementOS') + { + if (-not $MacAddress) + { + $arguments.Add('DynamicMacAddress',$true) + } + else + { + $arguments.Add('StaticMacAddress',$MacAddress) + } + $arguments.Add('SwitchName',$SwitchName) + } + Write-Verbose -Message $localizedData.AddVMNetAdapter + $netAdapterExists = Add-VMNetworkAdapter @arguments -Passthru -ErrorAction Stop + } + + if ($VmName -ne 'ManagementOS') + { + $networkInfo = Get-NetworkInformation -VMName $VMName -Name $Name + if (-not $NetworkSetting) + { + if($networkInfo) + { + Write-Verbose -Message $localizedData.EnableDhcp + Set-NetworkInformation -VMName $VMName -Name $Name -Dhcp + } + } + else + { + $parameters = @{} + if ($ipAddress = $NetworkSetting.CimInstanceProperties["IpAddress"].Value) + { + if (-not $ipAddress) + { + throw $localizedData.MissingIPAndSubnet + } + $parameters.Add('IPAddress', $ipAddress) + } + if ($subnet = $NetworkSetting.CimInstanceProperties["Subnet"].Value) + { + if (-not $subnet) + { + throw $localizedData.MissingIPAndSubnet + } + $parameters.Add('Subnet', $subnet) + } + if ($defaultGateway = $NetworkSetting.CimInstanceProperties["DefaultGateway"].Value) + { + $parameters.Add('DefaultGateway', $defaultGateway) + } + if ($dnsServer = $NetworkSetting.CimInstanceProperties["DnsServer"].Value) + { + $parameters.Add('DnsServer', $dnsServer) + } + + Set-NetworkInformation -VMName $VMName -Name $Name @parameters + } + + Write-Verbose -Message $localizedData.GetVMNetAdapterVlan + $netAdapterVlan = Get-VMNetworkAdapterVlan -VMNetworkAdapter $netAdapterExists + if ($netAdapterVlan) + { + if ($VlanId) + { + $setVlan = $true + } + else + { + Write-Verbose -Message $localizedData.RemovingVlanTag + Set-VMNetworkAdapterVlan -VMNetworkAdapter $netAdapterExists -Untagged + } + } + elseif ($VlanId) + { + $setVlan = $true + } + + if ($setVlan) + { + Write-Verbose -Message $localizedData.SettingVlan + Set-VMNetworkAdapterVlan -VMNetworkAdapter $netAdapterExists -Access -VlanId $VlanId + } + } + } + else + { + Write-Verbose -Message $localizedData.RemoveVMNetAdapter + Remove-VMNetworkAdapter @arguments -ErrorAction Stop + } +} + +<# +.SYNOPSIS + Tests if MSFT_xVMNetworkAdapter resource state is indeed desired state or not. + +.PARAMETER Id + Specifies an unique identifier for the network adapter. + +.PARAMETER Name + Specifies a name for the network adapter that needs to be connected to a VM or management OS. + +.PARAMETER SwitchName + Specifies the name of the switch to which the new VM network adapter will be connected. + +.PARAMETER VMName + Specifies the name of the VM to which the network adapter will be connected. + Specify VMName as ManagementOS if you wish to connect the adapter to host OS. + +.PARAMETER MacAddress + Specifies the MAC address for the network adapter. This is not applicable if VMName + is set to ManagementOS. Use this parameter to specify a static MAC address. + +.PARAMETER IpAddress + Specifies the IpAddress information for the network adapter. + +.PARAMETER VlanId + Specifies the Vlan Id for the network adapter. + +.PARAMETER Ensure + Specifies if the network adapter should be Present or Absent. +#> +Function Test-TargetResource +{ + [CmdletBinding()] + [OutputType([System.Boolean])] + Param ( + [Parameter(Mandatory)] + [String] $Id, + + [Parameter(Mandatory)] + [String] $Name, + + [Parameter(Mandatory)] + [String] $SwitchName, + + [Parameter(Mandatory)] + [String] $VMName, + + [Parameter()] + [String] $MacAddress, + + [Parameter()] + [Microsoft.Management.Infrastructure.CimInstance] + $NetworkSetting, + + [Parameter()] + [String] $VlanId, + + [Parameter()] + [ValidateSet('Present','Absent')] + [String] $Ensure='Present' + ) + + $arguments = @{ + Name = $Name + } + + if ($VMName -ne 'ManagementOS') + { + $arguments.Add('VMName',$VMName) + } + else + { + $arguments.Add('ManagementOS', $true) + $arguments.Add('SwitchName', $SwitchName) + } + + Write-Verbose -Message $localizedData.GetVMNetAdapter + $netAdapterExists = Get-VMNetworkAdapter @arguments -ErrorAction SilentlyContinue + + if ($Ensure -eq 'Present') + { + if ($netAdapterExists) + { + if ($VMName -ne 'ManagementOS') + { + if ($MacAddress) + { + if ($netAdapterExists.DynamicMacAddressEnabled) + { + Write-Verbose -Message $localizedData.EnableStaticMacAddress + return $false + } + elseif ($netAdapterExists.MacAddress -ne $MacAddress) + { + Write-Verbose -Message $localizedData.StaticAddressDoesNotMatch + return $false + } + } + else + { + if (-not $netAdapterExists.DynamicMacAddressEnabled) + { + Write-Verbose -Message $localizedData.EnableDynamicMacAddress + return $false + } + } + + $networkInfo = Get-NetworkInformation -VMName $VMName -Name $Name + if (-not $NetworkSetting) + { + if($networkInfo) + { + Write-Verbose -Message $localizedData.NotDhcp + return $false + } + } + else + { + if (-not $networkInfo) + { + Write-Verbose -Message $localizedData.Dhcp + return $false + } + else + { + $ipAddress = $NetworkSetting.CimInstanceProperties["IpAddress"].Value + $subnet = $NetworkSetting.CimInstanceProperties["Subnet"].Value + $defaultGateway = $NetworkSetting.CimInstanceProperties["DefaultGateway"].Value + $dnsServer = $NetworkSetting.CimInstanceProperties["DnsServer"].Value + + if (-not $IpAddress -or -not $subnet) + { + throw $localizedData.MissingIPAndSubnet + } + + if ($ipAddress -and -not $networkInfo.IPAddress.Split(',').Contains($ipAddress)) + { + Write-Verbose -Message $localizedData.IPAddressNotConfigured + return $false + } + + if ($defaultGateway -and -not $networkInfo.DefaultGateway.Split(',').Contains($defaultGateway)) + { + Write-Verbose -Message $localizedData.GatewayNotConfigured + return $false + } + + if ($dnsServer -and -not $networkInfo.DNSServer.Split(',').Contains($dnsServer)) + { + Write-Verbose -Message $localizedData.DNSServerNotConfigured + return $false + } + } + } + + Write-Verbose -Message $localizedData.GetVMNetAdapterVlan + $netAdapterVlan = Get-VMNetworkAdapterVlan -VMNetworkAdapter $netAdapterExists + if ($netAdapterVlan) + { + if ($netAdapterVlan.OperationMode -eq 'Untagged') + { + if ($VlanId) + { + Write-Verbose -Message $localizedData.VlanNotUntagged + return $false + } + } + else + { + if ($VlanId) + { + if ($netAdapterVlan.AccessVlanId -ne $VlanId) + { + Write-Verbose -Message $localizedData.VlanDoesNotMatch + return $false + } + } + else + { + Write-Verbose -Message $localizedData.VlanShouldntBeTagged + return $false + } + } + } + elseif ($VlanId) + { + Write-Verbose -Message $localizedData.VlanNotUntagged + return $false + } + + if ($netAdapterExists.SwitchName -ne $SwitchName) + { + Write-Verbose -Message $localizedData.SwitchIsDifferent + return $false + } + else + { + Write-Verbose -Message $localizedData.VMNetAdapterExistsNoActionNeeded + return $true + } + + } + else + { + Write-Verbose -Message $localizedData.VMNetAdapterExistsNoActionNeeded + return $true + } + } + else + { + Write-Verbose -Message $localizedData.VMNetAdapterDoesNotExistShouldAdd + return $false + } + } + else + { + if ($netAdapterExists) + { + Write-Verbose -Message $localizedData.VMNetAdapterExistsShouldRemove + return $false + } + else + { + Write-Verbose -Message $localizedData.VMNetAdapterDoesNotExistNoActionNeeded + return $true + } + } +} + +function Get-NetworkInformation +{ + [CmdletBinding()] + [OutputType([System.Collections.Hashtable])] + Param ( + [Parameter(Mandatory)] + [String] $VMName, + + [Parameter(Mandatory)] + [String] $Name + ) + + $vm = Get-WmiObject -Namespace 'root\virtualization\v2' -Class 'Msvm_ComputerSystem' | Where-Object { $_.ElementName -ieq "$VmName" } + $vmSettings = $vm.GetRelated('Msvm_VirtualSystemSettingData') | Where-Object { $_.VirtualSystemType -eq 'Microsoft:Hyper-V:System:Realized' } + $vmNetAdapter = $vmSettings.GetRelated('Msvm_SyntheticEthernetPortSettingData') | Where-Object { $_.ElementName -ieq "$Name" } + $networkSettings = $vmNetAdapter.GetRelated("Msvm_GuestNetworkAdapterConfiguration") + + if ($networkSettings.DHCPEnabled) + { + return $null + } + else + { + return @{ + IpAddress = $networkSettings.IPAddresses -join ',' + Subnet = $networkSettings.Subnets -join ',' + DefaultGateway = $networkSettings.DefaultGateways -join ',' + DnsServer = $networkSettings.DNSServers -join ',' + } + } + +} + +function Set-NetworkInformation +{ + [CmdletBinding()] + Param ( + [Parameter(Mandatory)] + [String] $VMName, + + [Parameter(Mandatory)] + [String] $Name, + + [Parameter(ParameterSetName='Dhcp')] + [switch] $Dhcp, + + [Parameter(Mandatory, ParameterSetName='Static')] + [String] $IPAddress, + + [Parameter(Mandatory, ParameterSetName='Static')] + [String] $Subnet, + + [Parameter(ParameterSetName='Static')] + [String] $DefaultGateway, + + [Parameter(ParameterSetName='Static')] + [String] $DnsServer + ) + + $vm = Get-WmiObject -Namespace 'root\virtualization\v2' -Class 'Msvm_ComputerSystem' | Where-Object { $_.ElementName -ieq "$VmName" } + $vmSettings = $vm.GetRelated('Msvm_VirtualSystemSettingData') | Where-Object { $_.VirtualSystemType -eq 'Microsoft:Hyper-V:System:Realized' } + $vmNetAdapter = $vmSettings.GetRelated('Msvm_SyntheticEthernetPortSettingData') | Where-Object { $_.ElementName -ieq $Name } + $networkSettings = $vmNetAdapter.GetRelated("Msvm_GuestNetworkAdapterConfiguration") | Select-Object -First 1 + + switch ($PSCmdlet.ParameterSetName) + { + 'Dhcp' + { + $networkSettings.DHCPEnabled = $true + $networkSettings.IPAddresses = @() + $networkSettings.Subnets = @() + $networkSettings.DefaultGateways = @() + $networkSettings.DNSServers = @() + } + 'Static' + { + $networkSettings.IPAddresses = $IPAddress + $networkSettings.Subnets = $Subnet + + if ($DefaultGateway) + { + $networkSettings.DefaultGateways = $DefaultGateway + } + if ($DnsServer) + { + $networkSettings.DNSServers = $DNSServer + } + $networkSettings.DHCPEnabled = $false + } + } + $networkSettings.ProtocolIFType = 4096 + + $service = Get-WmiObject -Class "Msvm_VirtualSystemManagementService" -Namespace "root\virtualization\v2" + $setIP = $service.SetGuestNetworkAdapterConfiguration($vm, $networkSettings.GetText(1)) + + if ($setIP.ReturnValue -eq 4096) + { + $job = [WMI]$setIP.job + + while ($job.JobState -eq 3 -or $job.JobState -eq 4) + { + Start-Sleep 1 + $job = [WMI]$setIP.job + } + + if($job.JobState -ne 7) + { + throw $job.GetError().Error + } + } +} + +Export-ModuleMember -Function *-TargetResource diff --git a/deployment/dsc/azshcihost/xHyper-V/3.17.0.0/DSCResources/MSFT_xVMNetworkAdapter/MSFT_xVMNetworkAdapter.schema.mof b/deployment/dsc/azshcihost/xHyper-V/3.17.0.0/DSCResources/MSFT_xVMNetworkAdapter/MSFT_xVMNetworkAdapter.schema.mof new file mode 100644 index 0000000..8929134 --- /dev/null +++ b/deployment/dsc/azshcihost/xHyper-V/3.17.0.0/DSCResources/MSFT_xVMNetworkAdapter/MSFT_xVMNetworkAdapter.schema.mof @@ -0,0 +1,23 @@ + +[ClassVersion("2.0.0.0")] +Class xNetworkSettings +{ + [Write] string IpAddress; + [Write] string Subnet; + [Write] string DefaultGateway; + [Write] string DnsServer; +}; + +[ClassVersion("2.0.0.0"), FriendlyName("xVMNetworkAdapter")] +class MSFT_xVMNetworkAdapter : OMI_BaseResource +{ + [Key] String Id; + [Required] String Name; + [Required] String SwitchName; + [Required] String VMName; + [Write] String MacAddress; + [Write, EmbeddedInstance("xNetworkSettings")] String NetworkSetting; + [Write] String VlanId; + [Write, ValueMap{"Present","Absent"}, Values{"Present","Absent"}] String Ensure; + [Read] Boolean DynamicMacAddress; +}; diff --git a/deployment/dsc/azshcihost/xHyper-V/3.17.0.0/DSCResources/MSFT_xVMNetworkAdapter/en-US/MSFT_xVMNetworkAdapter.psd1 b/deployment/dsc/azshcihost/xHyper-V/3.17.0.0/DSCResources/MSFT_xVMNetworkAdapter/en-US/MSFT_xVMNetworkAdapter.psd1 new file mode 100644 index 0000000..b5e1676 --- /dev/null +++ b/deployment/dsc/azshcihost/xHyper-V/3.17.0.0/DSCResources/MSFT_xVMNetworkAdapter/en-US/MSFT_xVMNetworkAdapter.psd1 @@ -0,0 +1,37 @@ +ConvertFrom-StringData @' + VMNameAndManagementTogether=VMName cannot be provided when ManagementOS is set to True. + MustProvideVMName=Must provide VMName parameter when ManagementOS is set to False. + GetVMNetAdapter=Getting VM Network Adapter information. + GetVMNetAdapterVlan=Getting VM Network Adapter VLAN information. + VlanShouldntBeTagged=VM Network Adapter should not have a Vlan tag. + VlanNotUntagged=Vlan is tagged. It will be removed. + VlanDoesNotMatch=VlanId does not match. + RemovingVlanTag=Removing Vlan tagging on Network Adapter. + SettingVlan=Setting VlanId on network adapter. + IpAddressIsNotSet=Ip Address is not set. + NotDhcp=Ethernet Adapter is not configured for Dhcp. + Dhcp=Ethernet Adapter is configured for Dhcp. + EnableDhcp=Enabling DHCP. + IPAddressNotConfigured=IPAddress is not configured. + GatewayNotConfigured=Gateway is not configured. + DNSServerNotConfigured=DNS Server not configured. + MissingIPAndSubnet=Missing IPAddress or Subnet. + FoundVMNetAdapter=Found VM Network Adapter. + NoVMNetAdapterFound=No VM Network Adapter found. + StaticMacAddressChosen=Static MAC Address has been specified. + StaticAddressDoesNotMatch=Staic MAC address on the VM Network Adapter does not match. + ModifyVMNetAdapter=VM Network Adapter exists with different configuration. This will be modified. + EnableDynamicMacAddress=VM Network Adapter exists but without Dynamic MAC address setting. + EnableStaticMacAddress=VM Network Adapter exists but without static MAC address setting. + PerformVMNetModify=Performing VM Network Adapter configuration changes. + CannotChangeHostAdapterMacAddress=VM Network adapter in configuration is a host adapter. Its configuration cannot be modified. + AddVMNetAdapter=Adding VM Network Adapter. + RemoveVMNetAdapter=Removing VM Network Adapter. + VMNetAdapterExistsNoActionNeeded=VM Network Adapter exists with requested configuration. No action needed. + VMNetAdapterDoesNotExistShouldAdd=VM Network Adapter does not exist. It will be added. + VMNetAdapterExistsShouldRemove=VM Network Adapter Exists. It will be removed. + VMNetAdapterDoesNotExistNoActionNeeded=VM Network adapter does not exist. No action needed. + StaticMacExists=StaicMacAddress configuration exists as desired. + SwitchIsDifferent=Net Adapter is not connected to the requested switch. + PerformSwitchConnect=Connecting VM Net adapter to the right switch. +'@ diff --git a/deployment/dsc/azshcihost/xHyper-V/3.17.0.0/DSCResources/MSFT_xVMProcessor/MSFT_xVMProcessor.psm1 b/deployment/dsc/azshcihost/xHyper-V/3.17.0.0/DSCResources/MSFT_xVMProcessor/MSFT_xVMProcessor.psm1 new file mode 100644 index 0000000..79cd09c --- /dev/null +++ b/deployment/dsc/azshcihost/xHyper-V/3.17.0.0/DSCResources/MSFT_xVMProcessor/MSFT_xVMProcessor.psm1 @@ -0,0 +1,443 @@ +#region localizeddata +if (Test-Path "${PSScriptRoot}\${PSUICulture}") +{ + Import-LocalizedData -BindingVariable localizedData -Filename MSFT_xVMProcessor.psd1 ` + -BaseDirectory "${PSScriptRoot}\${PSUICulture}" +} +else +{ + # fallback to en-US + Import-LocalizedData -BindingVariable localizedData -Filename MSFT_xVMProcessor.psd1 ` + -BaseDirectory "${PSScriptRoot}\en-US" +} +#endregion + +# Import the common HyperV functions +Import-Module -Name ( Join-Path ` + -Path (Split-Path -Path $PSScriptRoot -Parent) ` + -ChildPath '\HyperVCommon\HyperVCommon.psm1' ) + +<# +.SYNOPSIS + Gets MSFT_xVMProcessor resource current state. + +.PARAMETER VMName + Specifies the name of the virtual machine on which the processor is to be configured. +#> +function Get-TargetResource +{ + [CmdletBinding()] + [OutputType([System.Collections.Hashtable])] + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $VMName + ) + + Assert-Module -Name 'Hyper-V' + Write-Verbose -Message ($localizedData.QueryingVMProcessor -f $VMName) + $vmProcessor = Get-VMProcessor -VMName $VMName -ErrorAction Stop + $configuration = @{ + VMName = $VMName + EnableHostResourceProtection = $vmProcessor.EnableHostResourceProtection + ExposeVirtualizationExtensions = $vmProcessor.ExposeVirtualizationExtensions + HwThreadCountPerCore = $vmProcessor.HwThreadCountPerCore + Maximum = $vmProcessor.Maximum + MaximumCountPerNumaNode = $vmProcessor.MaximumCountPerNumaNode + MaximumCountPerNumaSocket = $vmProcessor.MaximumCountPerNumaSocket + RelativeWeight = $vmProcessor.RelativeWeight + Reserve = $vmProcessor.Reserve + ResourcePoolName = $vmProcessor.ResourcePoolName + CompatibilityForMigrationEnabled = $vmProcessor.CompatibilityForMigrationEnabled + CompatibilityForOlderOperatingSystemsEnabled = $vmProcessor.CompatibilityForOlderOperatingSystemsEnabled + RestartIfNeeded = $false + } + return $configuration +} + +<# +.SYNOPSIS + Tests if MSFT_xVMProcessor resource state is in the desired state or not. + +.PARAMETER VMName + Specifies the name of the virtual machine on which the processor is to be configured. + +.PARAMETER EnableHostResourceProtection + Specifies whether to enable host resource protection. + NOTE: Only supported on Windows 10 and Server 2016. + +.PARAMETER ExposeVirtualizationExtensions + Specifies whether nested virtualization is enabled. + NOTE: Only supported on Windows 10 and Server 2016. + +.PARAMETER HwThreadCountPerCore + Specifies the maximum thread core per processor core. + NOTE: Only supported on Windows 10 and Server 2016. + +.PARAMETER Maximum + Specifies the maximum percentage of resources available to the virtual machine + processor to be configured. Allowed values range from 0 to 100. + +.PARAMETER MaximumCountPerNumaNode + Specifies the maximum number of processors per NUMA node to be configured for + the virtual machine. + +.PARAMETER MaximumCountPerNumaSocket + Specifies the maximum number of sockets per NUMA node to be configured for + the virtual machine. + +.PARAMETER RelativeWeight + Specifies the priority for allocating the physical computer's processing + power to this virtual machine relative to others. Allowed values range + from 1 to 10000. + +.PARAMETER Reserve + Specifies the percentage of processor resources to be reserved for this + virtual machine. Allowed values range from 0 to 100. + +.PARAMETER ResourcePoolName + Specifies the name of the processor resource pool to be used. + +.PARAMETER CompatibilityForMigrationEnabled + Specifies whether the virtual processors features are to be limited + for compatibility when migrating the virtual machine to another host. + +.PARAMETER CompatibilityForOlderOperatingSystemsEnabled + Specifies whether the virtual processor’s features are to be limited + for compatibility with older operating systems. + +.PARAMETER RestartIfNeeded + If specified, shutdowns and restarts the VM if needed for property + changes. +#> +function Test-TargetResource +{ + [CmdletBinding()] + [OutputType([System.Boolean])] + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $VMName, + + [Parameter()] + [System.Boolean] + $EnableHostResourceProtection, + + [Parameter()] + [System.Boolean] + $ExposeVirtualizationExtensions, + + [Parameter()] + [System.UInt64] + $HwThreadCountPerCore, + + [Parameter()] + [ValidateRange(0,100)] + [System.UInt64] + $Maximum, + + [Parameter()] + [System.UInt32] + $MaximumCountPerNumaNode, + + [Parameter()] + [System.UInt32] + $MaximumCountPerNumaSocket, + + [Parameter()] + [ValidateRange(0,10000)] + [System.UInt32] + $RelativeWeight, + + [Parameter()] + [ValidateRange(0,100)] + [System.UInt64] + $Reserve, + + [Parameter()] + [System.String] + $ResourcePoolName, + + [Parameter()] + [System.Boolean] + $CompatibilityForMigrationEnabled, + + [Parameter()] + [System.Boolean] + $CompatibilityForOlderOperatingSystemsEnabled, + + [Parameter()] + [System.Boolean] + $RestartIfNeeded + ) + + Assert-Module -Name 'Hyper-V' + Assert-TargetResourceParameter @PSBoundParameters + + $targetResource = Get-TargetResource -VMName $VMName + $excludedTestParameters = @('RestartIfNeeded') + $isTargetResourceCompliant = $true + + foreach ($parameter in $PSBoundParameters.GetEnumerator()) + { + if (($targetResource.ContainsKey($parameter.Key)) -and + ($parameter.Key -notin $excludedTestParameters) -and + ($parameter.Value -ne $targetResource[$parameter.Key])) + { + $isTargetResourceCompliant = $false + Write-Verbose -Message ($localizedData.PropertyMismatch -f $parameter.Key, + $parameter.Value, $targetResource[$parameter.Key]) + } + } + + if ($isTargetResourceCompliant) + { + Write-Verbose -Message ($localizedData.VMProcessorInDesiredState -f $VMName) + } + else + { + Write-Verbose -Message ($localizedData.VMProcessorNotInDesiredState -f $VMName) + } + + return $isTargetResourceCompliant +} #end function + +<# +.SYNOPSIS + Configures MSFT_xVMProcessor resource state. + +.PARAMETER VMName + Specifies the name of the virtual machine on which the processor is to be configured. + +.PARAMETER EnableHostResourceProtection + Specifies whether to enable host resource protection. + NOTE: Only supported on Windows 10 and Server 2016. + +.PARAMETER ExposeVirtualizationExtensions + Specifies whether nested virtualization is enabled. + NOTE: Only supported on Windows 10 and Server 2016. + +.PARAMETER HwThreadCountPerCore + Specifies the maximum thread core per processor core + NOTE: Only supported on Windows 10 and Server 2016. + +.PARAMETER Maximum + Specifies the maximum percentage of resources available to the virtual machine + processor to be configured. Allowed values range from 0 to 100. + +.PARAMETER MaximumCountPerNumaNode + Specifies the maximum number of processors per NUMA node to be configured for + the virtual machine. + +.PARAMETER MaximumCountPerNumaSocket + Specifies the maximum number of sockets per NUMA node to be configured for + the virtual machine. + +.PARAMETER RelativeWeight + Specifies the priority for allocating the physical computer's processing + power to this virtual machine relative to others. Allowed values range + from 1 to 10000. + +.PARAMETER Reserve + Specifies the percentage of processor resources to be reserved for this + virtual machine. Allowed values range from 0 to 100. + +.PARAMETER ResourcePoolName + Specifies the name of the processor resource pool to be used. + +.PARAMETER CompatibilityForMigrationEnabled + Specifies whether the virtual processors features are to be limited + for compatibility when migrating the virtual machine to another host. + +.PARAMETER CompatibilityForOlderOperatingSystemsEnabled + Specifies whether the virtual processor’s features are to be limited + for compatibility with older operating systems. + +.PARAMETER RestartIfNeeded + If specified, shutdowns and restarts the VM if needed for property + changes. +#> +function Set-TargetResource +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $VMName, + + [Parameter()] + [System.Boolean] + $EnableHostResourceProtection, + + [Parameter()] + [System.Boolean] + $ExposeVirtualizationExtensions, + + [Parameter()] + [System.UInt64] + $HwThreadCountPerCore, + + [Parameter()] + [ValidateRange(0,100)] + [System.UInt64] + $Maximum, + + [Parameter()] + [System.UInt32] + $MaximumCountPerNumaNode, + + [Parameter()] + [System.UInt32] + $MaximumCountPerNumaSocket, + + [Parameter()] + [ValidateRange(0,10000)] + [System.UInt32] + $RelativeWeight, + + [Parameter()] + [ValidateRange(0,100)] + [System.UInt64] + $Reserve, + + [Parameter()] + [System.String] + $ResourcePoolName, + + [Parameter()] + [System.Boolean] + $CompatibilityForMigrationEnabled, + + [Parameter()] + [System.Boolean] + $CompatibilityForOlderOperatingSystemsEnabled, + + [Parameter()] + [System.Boolean] + $RestartIfNeeded + ) + + Assert-Module -Name 'Hyper-V' + Assert-TargetResourceParameter @PSBoundParameters + + # Parameters requiring shutdown. + $restartRequiredParameterNames = @( + 'ExposeVirtualizationExtensions', + 'CompatibilityForMigrationEnabled', + 'CompatibilityForOlderOperatingSystemsEnabled', + 'HwThreadCountPerCore', + 'MaximumCountPerNumaNode', + 'MaximumCountPerNumaSocket', + 'ResourcePoolName' + ) + $isRestartRequired = $false + $vmObject = Get-VM -Name $VMName + + # Only check for restart required parameters if VM is not off + if ($vmObject.State -ne 'Off') + { + foreach ($parameterName in $restartRequiredParameterNames) + { + if ($PSBoundParameters.ContainsKey($parameterName)) + { + if (-not $RestartIfNeeded) + { + $errorMessage = $localized.CannotUpdateVmOnlineError -f $parameterName + New-InvalidOperationError -ErrorId InvalidState -ErrorMessage $errorMessage + } + else + { + $isRestartRequired = $true + } + } + } #end foreach parameter + } + + $null = $PSBoundParameters.Remove('RestartIfNeeded') + $null = $PSBoundParameters.Remove('VMName') + + if (-not $isRestartRequired) + { + # No parameter specified that requires a restart, so disable the restart flag + Write-Verbose -Message ($localizedData.UpdatingVMProperties -f $VMName) + Set-VMProcessor -VMName $VMName @PSBoundParameters + Write-Verbose -Message ($localizedData.VMPropertiesUpdated -f $VMName) + } + else + { + # Restart is required and that requires turning VM off + $setVMPropertyParameters = @{ + VMName = $VMName + VMCommand = 'Set-VMProcessor' + ChangeProperty = $PSBoundParameters + RestartIfNeeded = $true + Verbose = $Verbose + } + Set-VMProperty @setVMPropertyParameters + } +} #end function + +<# +.SYNOPSIS + Ensures OS supports the supplied parameters. + +.PARAMETER EnableHostResourceProtection + Specifies whether to enable host resource protection. + NOTE: Only supported on Windows 10, Server 2016 and Nano. + +.PARAMETER ExposeVirtualizationExtensions + Specifies whether nested virtualization is enabled. + NOTE: Only supported on Windows 10, Server 2016 and Nano. + +.PARAMETER HwThreadCountPerCore + Specifies the maximum thread core per processor core + NOTE: Only supported on Windows 10, Server 2016 and Nano. + +.PARAMETER RemainingArgument + Catch all to enable splatting of remaining parameters. +#> +function Assert-TargetResourceParameter +{ + [CmdletBinding()] + param + ( + [Parameter()] + [System.Boolean] + $EnableHostResourceProtection, + + [Parameter()] + [System.Boolean] + $ExposeVirtualizationExtensions, + + [Parameter()] + [System.UInt64] + $HwThreadCountPerCore, + + [Parameter(ValueFromRemainingArguments)] + [System.Object[]] + $RemainingArguments + ) + + # Get-CimInstance returns build number as a string + $win32OperatingSystem = Get-CimInstance -ClassName Win32_OperatingSystem -Verbose:$false + $osBuildNumber = $win32OperatingSystem.BuildNumber -as [System.Int64] + $build14393RequiredParameterNames = @( + 'EnableHostResourceProtection', + 'ExposeVirtualizationExtensions', + 'HwThreadCountPerCore' + ) + + foreach ($parameterName in $build14393RequiredParameterNames) + { + if (($PSBoundParameters.ContainsKey($parameterName)) -and ($osBuildNumber -lt 14393)) + { + $errorMessage = $localizedData.UnsupportedSystemError -f $parameterName, 14393 + New-InvalidArgumentError -ErrorId SystemUnsupported -ErrorMessage $errorMessage + } + } +} #end function + +Export-ModuleMember -Function *-TargetResource diff --git a/deployment/dsc/azshcihost/xHyper-V/3.17.0.0/DSCResources/MSFT_xVMProcessor/MSFT_xVMProcessor.schema.mof b/deployment/dsc/azshcihost/xHyper-V/3.17.0.0/DSCResources/MSFT_xVMProcessor/MSFT_xVMProcessor.schema.mof new file mode 100644 index 0000000..0b3c174 --- /dev/null +++ b/deployment/dsc/azshcihost/xHyper-V/3.17.0.0/DSCResources/MSFT_xVMProcessor/MSFT_xVMProcessor.schema.mof @@ -0,0 +1,17 @@ +[ClassVersion("1.0.0.0"), FriendlyName("xVMProcessor")] +class MSFT_xVMProcessor : OMI_BaseResource +{ + [Key, Description("Specifies the name of the virtual machine on which the processor is to be configured.")] String VMName; + [Write, Description("Specifies whether to enable host resource protection.")] Boolean EnableHostResourceProtection; + [Write, Description("Specifies whether nested virtualization is enabled.")] Boolean ExposeVirtualizationExtensions; + [Write, Description("Specifies the maximum thread core per processor core.")] Uint64 HwThreadCountPerCore; + [Write, Description("Specifies the maximum percentage of resources available to the virtual machine processor to be configured. Allowed values range from 0 to 100.")] Uint64 Maximum; + [Write, Description("Specifies the maximum number of processors per NUMA node to be configured for the virtual machine.")] Uint32 MaximumCountPerNumaNode; + [Write, Description("Specifies the maximum number of sockets per NUMA node to be configured for the virtual machine.")] Uint32 MaximumCountPerNumaSocket; + [Write, Description("Specifies the priority for allocating the physical computer's processing power to this virtual machine relative to others. Allowed values range from 1 to 10000.")] Uint32 RelativeWeight; + [Write, Description("Specifies the percentage of processor resources to be reserved for this virtual machine. Allowed values range from 0 to 100.")] Uint64 Reserve; + [Write, Description("Specifies the name of the processor resource pool to be used.")] String ResourcePoolName; + [Write, Description("Specifies whether the virtual processors features are to be limited for compatibility when migrating the virtual machine to another host.")] Boolean CompatibilityForMigrationEnabled; + [Write, Description("Specifies whether the virtual processor’s features are to be limited for compatibility with older operating systems.")] Boolean CompatibilityForOlderOperatingSystemsEnabled; + [Write, Description("If specified, shutdowns and restarts the VM if needed for property changes.")] Boolean RestartIfNeeded; +}; diff --git a/deployment/dsc/azshcihost/xHyper-V/3.17.0.0/DSCResources/MSFT_xVMProcessor/en-US/MSFT_xVMProcessor.psd1 b/deployment/dsc/azshcihost/xHyper-V/3.17.0.0/DSCResources/MSFT_xVMProcessor/en-US/MSFT_xVMProcessor.psd1 new file mode 100644 index 0000000..fa6eab6 --- /dev/null +++ b/deployment/dsc/azshcihost/xHyper-V/3.17.0.0/DSCResources/MSFT_xVMProcessor/en-US/MSFT_xVMProcessor.psd1 @@ -0,0 +1,12 @@ +ConvertFrom-StringData @' + QueryingVMProcessor = Querying VM '{0}' processor(s). + PropertyMismatch = Property '{0}' mismatch; expected value '{1}', but was '{2}'. + VMProcessorInDesiredState = VM '{0}' processor(s) in desired state. + VMProcessorNotInDesiredState = VM '{0}' processor(s) not in desired state. + UpdatingVMProperties = Updating VM '{0}' properties. + VMPropertiesUpdated = VM '{0}' properties have been updated. + + VMNotFoundError = VM '{0}' was not found. + UnsupportedSystemError = Parameter '{0}' is not supported on operating system builds earlier than '{1}'. + CannotUpdateVmOnlineError = Cannot change online property '{0}' unless 'RestartIfNeeded' is set to true. +'@ diff --git a/deployment/dsc/azshcihost/xHyper-V/3.17.0.0/DSCResources/MSFT_xVMScsiController/MSFT_xVMScsiController.psm1 b/deployment/dsc/azshcihost/xHyper-V/3.17.0.0/DSCResources/MSFT_xVMScsiController/MSFT_xVMScsiController.psm1 new file mode 100644 index 0000000..6f7fd14 --- /dev/null +++ b/deployment/dsc/azshcihost/xHyper-V/3.17.0.0/DSCResources/MSFT_xVMScsiController/MSFT_xVMScsiController.psm1 @@ -0,0 +1,234 @@ +#region localizeddata +if (Test-Path "${PSScriptRoot}\${PSUICulture}") +{ + Import-LocalizedData ` + -BindingVariable LocalizedData ` + -Filename MSFT_xVMScsiController.strings.psd1 ` + -BaseDirectory "${PSScriptRoot}\${PSUICulture}" +} +else +{ + # fallback to en-US + Import-LocalizedData ` + -BindingVariable LocalizedData ` + -Filename MSFT_xVMScsiController.strings.psd1 ` + -BaseDirectory "${PSScriptRoot}\en-US" +} +#endregion + +# Import the common HyperV functions +Import-Module -Name ( Join-Path ` + -Path (Split-Path -Path $PSScriptRoot -Parent) ` + -ChildPath '\HyperVCommon\HyperVCommon.psm1' ) + +<# + .SYNOPSIS + Returns the current status of the VM SCSI controller. + + .PARAMETER VMName + Specifies the name of the virtual machine whose SCSI controller status is to be fetched. + + .PARAMETER ControllerNumber + Specifies the number of the controller to which the hard disk drive is to be set. + If not specified, the controller number defaults to 0. +#> +function Get-TargetResource +{ + [CmdletBinding()] + [OutputType([System.Collections.Hashtable])] + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $VMName, + + [Parameter(Mandatory = $true)] + [ValidateSet(0, 1, 2, 3)] + [System.UInt32] + $ControllerNumber + ) + + Assert-Module -Name 'Hyper-V' + + $controller = Get-VMScsiController -VMName $VMName -ControllerNumber $ControllerNumber + if ($null -eq $controller) + { + Write-Verbose -Message ($localizedData.ControllerNotFound -f $ControllerNumber, $VMName) + $ensure = 'Absent' + } + else + { + Write-Verbose -Message ($localizedData.ControllerFound -f $ControllerNumber, $VMName) + $ensure = 'Present' + } + + return @{ + VMName = $Controller.VMName + ControllerNumber = $Controller.ControllerNumber + RestartIfNeeded = $false + Ensure = $ensure + } +} + +<# + .SYNOPSIS + Tests the state of a VM SCSI controller. + + .PARAMETER VMName + Specifies the name of the virtual machine whose SCSI controller is to be tested. + + .PARAMETER ControllerNumber + Specifies the number of the controller to which the hard disk drive is to be set. + If not specified, the controller number defaults to 0. + + .PARAMETER RestartIfNeeded + Specifies if the VM should be restarted if needed for property changes. + + .PARAMETER Ensure + Specifies if the SCSI controller should exist or not. Default to Present. +#> +function Test-TargetResource +{ + [CmdletBinding()] + [OutputType([System.Boolean])] + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $VMName, + + [Parameter(Mandatory = $true)] + [ValidateSet(0, 1, 2, 3)] + [System.UInt32] + $ControllerNumber, + + [Parameter()] + [System.Boolean] + $RestartIfNeeded, + + [Parameter()] + [ValidateSet('Present', 'Absent')] + [System.String] + $Ensure = 'Present' + ) + + $null = $PSBoundParameters.Remove('RestartIfNeeded') + $resource = Get-TargetResource -VMName $VMName -ControllerNumber $ControllerNumber + + $isCompliant = $true + foreach ($key in $resource.Keys) + { + Write-Verbose -Message ($localizedData.ComparingParameter -f $key, + $PSBoundParameters[$key], + $resource[$key]) + $isCompliant = $isCompliant -and ($PSBoundParameters[$key] -eq $resource[$key]) + } + + return $isCompliant +} + +<# + .SYNOPSIS + Manipulates the state of a VM SCSI controller. + + .PARAMETER VMName + Specifies the name of the virtual machine whose SCSI controller is to be manipulated. + + .PARAMETER ControllerNumber + Specifies the number of the controller to which the hard disk drive is to be set. + If not specified, the controller number defaults to 0. + + .PARAMETER RestartIfNeeded + Specifies if the VM should be restarted if needed for property changes. + + .PARAMETER Ensure + Specifies if the SCSI controller should exist or not. Defaults to Present. +#> +function Set-TargetResource +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $VMName, + + [Parameter(Mandatory = $true)] + [ValidateSet(0, 1, 2, 3)] + [System.UInt32] + $ControllerNumber, + + [Parameter()] + [System.Boolean] + $RestartIfNeeded, + + [Parameter()] + [ValidateSet('Present', 'Absent')] + [System.String] + $Ensure = 'Present' + ) + + Assert-Module -Name 'Hyper-V' + + # Getting the state of the VM so we can restore it later + $existingVmState = (Get-VMHyperV -VMName $VMName).State + + if ((-not $RestartIfNeeded) -and ($existingVmState -ne 'Off')) + { + $errorMessage = $localizedData.CannotUpdateVmOnlineError -f $VMName + New-InvalidOperationError -ErrorId InvalidState -ErrorMessage $errorMessage + } + + [System.Int32] $scsiControllerCount = @(Get-VMScsiController -VMName $VMName).Count + if ($Ensure -eq 'Present') + { + if ($scsiControllerCount -lt $ControllerNumber) + { + <# + All intermediate controllers should be present on the system as we cannot create + a controller at a particular location. For example, we cannot explicitly create + controller #2 - it will only be controller #2 if controllers #0 and #1 are already + added/present in the VM. + #> + $errorMessage = $localizedData.CannotAddScsiControllerError -f $ControllerNumber + New-InvalidArgumentError -ErrorId InvalidController -ErrorMessage $errorMessage + } + + Set-VMState -Name $VMName -State 'Off' + Write-Verbose -Message ($localizedData.AddingController -f $scsiControllerCount) + Add-VMScsiController -VMName $VMName + } + else + { + if ($scsiControllerCount -ne ($ControllerNumber +1)) + { + <# + All intermediate controllers should be present on the system. Whilst we can remove + a controller at a particular location, all remaining controller numbers may be + reordered. For example, if we remove controller at position #1, then a controller + that was at position #2 will become controller number #1. + #> + $errorMessage = $localizedData.CannotRemoveScsiControllerError -f $ControllerNumber + New-InvalidArgumentError -ErrorId InvalidController -ErrorMessage $errorMessage + } + + Set-VMState -Name $VMName -State 'Off' + Write-Verbose -Message ($localizedData.CheckingExistingDisks -f $ControllerNumber) + $controller = Get-VMScsiController -VMName $VmName -ControllerNumber $ControllerNumber + + foreach ($drive in $controller.Drives) + { + $warningMessage = $localizedData.RemovingDiskWarning -f $drive.Path, $ControllerNumber + Write-Warning -Message $warningMessage + Remove-VMHardDiskDrive -VMHardDiskDrive $drive + } + + Write-Verbose -Message ($localizedData.RemovingController -f $ControllerNumber, $VMName) + Remove-VMScsiController -VMScsiController $controller + } + + # Restore the previous state + Set-VMState -Name $VMName -State $existingVmState +} + +Export-ModuleMember -Function *-TargetResource diff --git a/deployment/dsc/azshcihost/xHyper-V/3.17.0.0/DSCResources/MSFT_xVMScsiController/MSFT_xVMScsiController.schema.mof b/deployment/dsc/azshcihost/xHyper-V/3.17.0.0/DSCResources/MSFT_xVMScsiController/MSFT_xVMScsiController.schema.mof new file mode 100644 index 0000000..1eebb01 --- /dev/null +++ b/deployment/dsc/azshcihost/xHyper-V/3.17.0.0/DSCResources/MSFT_xVMScsiController/MSFT_xVMScsiController.schema.mof @@ -0,0 +1,8 @@ +[ClassVersion("1.0.0"), FriendlyName("xVMScsiController")] +class MSFT_xVMScsiController : OMI_BaseResource +{ + [Key, Description("Specifies the name of the virtual machine whose SCSI controller status is to be controlled")] String VMName; + [Key, Description("Specifies the number of the SCSI controller whose status is to be controlled. If not specified, it defaults to 0."), ValueMap{"0","1","2","3"}, Values{"0","1","2","3"}] Uint32 ControllerNumber; + [Write, Description("Specifies if the SCSI controller should exist or not. If not specified, it defaults to Present."), ValueMap{"Present","Absent"}, Values{"Present","Absent"}] String Ensure; + [Write, Description("Specifies if the VM should be restarted if needed for property changes. If not specified, it defaults to False.")] Boolean RestartIfNeeded; +}; diff --git a/deployment/dsc/azshcihost/xHyper-V/3.17.0.0/DSCResources/MSFT_xVMScsiController/en-US/MSFT_xVMScsiController.strings.psd1 b/deployment/dsc/azshcihost/xHyper-V/3.17.0.0/DSCResources/MSFT_xVMScsiController/en-US/MSFT_xVMScsiController.strings.psd1 new file mode 100644 index 0000000..1587659 --- /dev/null +++ b/deployment/dsc/azshcihost/xHyper-V/3.17.0.0/DSCResources/MSFT_xVMScsiController/en-US/MSFT_xVMScsiController.strings.psd1 @@ -0,0 +1,14 @@ +ConvertFrom-StringData @' + ControllerFound = Found controller '{0}' attached to VM '{1}'. + ControllerNotFound = Controller '{0}' missing from VM '{1}' + ComparingParameter = Comparing '{0}'; expected '{1}', actual '{2}'. + AddingController = Adding controller number '{0}'. + CheckingExistingDisks = Checking for existing disks on controller '{0}'. + RemovingController = Removing controller '{0}' from VM '{1}'. + + RemovingDiskWarning = Removing disk '{0}' from the controller '{1}'. + + CannotUpdateVmOnlineError = Cannot update a running VM unless 'RestartIfNeeded' is set to true. + CannotAddScsiControllerError = Cannot add controller number '{0}'. Ensure that all intermediate controllers are present on the system. + CannotRemoveScsiControllerError = Cannot remove controller number '{0}'. Ensure that you are removing the last controller to ensure that controller numbers are not reordered. +'@ diff --git a/deployment/dsc/azshcihost/xHyper-V/3.17.0.0/DSCResources/MSFT_xVMSwitch/MSFT_xVMSwitch.psm1 b/deployment/dsc/azshcihost/xHyper-V/3.17.0.0/DSCResources/MSFT_xVMSwitch/MSFT_xVMSwitch.psm1 new file mode 100644 index 0000000..397e464 --- /dev/null +++ b/deployment/dsc/azshcihost/xHyper-V/3.17.0.0/DSCResources/MSFT_xVMSwitch/MSFT_xVMSwitch.psm1 @@ -0,0 +1,677 @@ +#region localizeddata +if (Test-Path "${PSScriptRoot}\${PSUICulture}") +{ + Import-LocalizedData ` + -BindingVariable LocalizedData ` + -Filename MSFT_xVMSwitch.strings.psd1 ` + -BaseDirectory "${PSScriptRoot}\${PSUICulture}" +} +else +{ + #fallback to en-US + Import-LocalizedData ` + -BindingVariable LocalizedData ` + -Filename MSFT_xVMSwitch.strings.psd1 ` + -BaseDirectory "${PSScriptRoot}\en-US" +} +#endregion + +# Import the common HyperV functions +Import-Module -Name ( Join-Path ` + -Path (Split-Path -Path $PSScriptRoot -Parent) ` + -ChildPath '\HyperVCommon\HyperVCommon.psm1' ) + +<# +.SYNOPSIS + Gets MSFT_xVMSwitch resource current state. + +.PARAMETER Name + Name of the VM Switch. + +.PARAMETER Type + Type of switch. +#> +function Get-TargetResource +{ + [CmdletBinding()] + [OutputType([System.Collections.Hashtable])] + param + ( + [Parameter(Mandatory = $true)] + [String] + $Name, + + [Parameter(Mandatory = $true)] + [ValidateSet("External","Internal","Private")] + [String] + $Type + ) + + Write-Verbose -Message "Getting settings for VM Switch '$Name'" + + # Check if Hyper-V module is present for Hyper-V cmdlets + if (!(Get-Module -ListAvailable -Name Hyper-V)) + { + New-InvalidOperationError ` + -ErrorId 'HyperVNotInstalledError' ` + -ErrorMessage $LocalizedData.HyperVNotInstalledError + } + + $switch = Get-VMSwitch -Name $Name -SwitchType $Type -ErrorAction SilentlyContinue + + if ($null -ne $switch) + { + $ensure = 'Present' + if ($switch.SwitchType -eq 'External') + { + if ($switch.EmbeddedTeamingEnabled -ne $true) + { + $netAdapterName = (Get-NetAdapter -InterfaceDescription $switch.NetAdapterInterfaceDescription -ErrorAction SilentlyContinue).Name + $description = $switch.NetAdapterInterfaceDescription + + $loadBalancingAlgorithm = 'NA' + } + else + { + $netAdapterName = (Get-NetAdapter -InterfaceDescription $switch.NetAdapterInterfaceDescriptions).Name + $description = $switch.NetAdapterInterfaceDescriptions + + $loadBalancingAlgorithm = ($switch | Get-VMSwitchTeam).LoadBalancingAlgorithm.toString() + } + } + else + { + $netAdapterName = $null + $description = $null + } + } + else + { + $ensure = 'Absent' + } + + $returnValue = @{ + Name = $switch.Name + Type = $switch.SwitchType + NetAdapterName = [string[]]$netAdapterName + AllowManagementOS = $switch.AllowManagementOS + EnableEmbeddedTeaming = $switch.EmbeddedTeamingEnabled + LoadBalancingAlgorithm = $loadBalancingAlgorithm + Ensure = $ensure + Id = $switch.Id + NetAdapterInterfaceDescription = $description + } + + if ($null -ne $switch.BandwidthReservationMode) + { + $returnValue['BandwidthReservationMode'] = $switch.BandwidthReservationMode + } + else + { + $returnValue['BandwidthReservationMode'] = 'NA' + } + + return $returnValue +} + +<# +.SYNOPSIS + Configures MSFT_xVMSwitch resource state. + +.PARAMETER Name + Name of the VM Switch. + +.PARAMETER Type + Type of switch. + +.PARAMETER NetAdapterName + Network adapter name(s) for external switch type. + +.PARAMETER AllowManagementOS + Specify if the VM host has access to the physical NIC. + +.PARAMETER EnableEmbeddedTeaming + Should embedded NIC teaming be used (Windows Server 2016 only). + +.PARAMETER BandwidthReservationMode + Type of Bandwidth Reservation Mode to use for the switch. + +.PARAMETER LoadBalancingAlgorithm + The load balancing algorithm that this switch team use. + +.PARAMETER Id + Desired unique ID of the Hyper-V Switch (Windows Server 2016 only). + +.PARAMETER Ensure + Whether switch should be present or absent. +#> +function Set-TargetResource +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [String] + $Name, + + [Parameter(Mandatory = $true)] + [ValidateSet("External","Internal","Private")] + [String] + $Type, + + [Parameter()] + [ValidateNotNullOrEmpty()] + [String[]] + $NetAdapterName, + + [Parameter()] + [Boolean] + $AllowManagementOS = $false, + + [Parameter()] + [Boolean] + $EnableEmbeddedTeaming = $false, + + [Parameter()] + [ValidateSet("Default","Weight","Absolute","None","NA")] + [String] + $BandwidthReservationMode = "NA", + + [Parameter()] + [ValidateSet('Dynamic','HyperVPort')] + [String] + $LoadBalancingAlgorithm, + + [Parameter()] + [ValidateNotNullOrEmpty()] + [ValidateScript({$testGuid = New-Guid; if([guid]::TryParse($_,[ref]$testGuid)){return $true}else{Throw 'The VMSwitch Id must be in GUID format!'}})] + [String] + $Id, + + [Parameter()] + [ValidateSet("Present","Absent")] + [String] + $Ensure = "Present" + ) + + # Check if Hyper-V module is present for Hyper-V cmdlets + if (!(Get-Module -ListAvailable -Name Hyper-V)) + { + New-InvalidOperationError ` + -ErrorId 'HyperVNotInstalledError' ` + -ErrorMessage $LocalizedData.HyperVNotInstalledError + } + + # Check to see if the BandwidthReservationMode chosen is supported in the OS + elseif (($BandwidthReservationMode -ne "NA") -and ((Get-OSVersion) -lt [version]'6.2.0')) + { + New-InvalidArgumentError ` + -ErrorId 'BandwidthReservationModeError' ` + -ErrorMessage $LocalizedData.BandwidthReservationModeError + } + + if ($EnableEmbeddedTeaming -eq $true -and (Get-OSVersion).Major -lt 10) + { + New-InvalidArgumentError ` + -ErrorId 'SETServer2016Error' ` + -ErrorMessage $LocalizedData.SETServer2016Error + } + + if (($PSBoundParameters.ContainsKey('Id')) -and (Get-OSVersion).Major -lt 10) + { + New-InvalidArgumentError ` + -ErrorId 'VMSwitchIDServer2016Error' ` + -ErrorMessage $LocalizedData.VMSwitchIDServer2016Error + } + + if ($Ensure -eq 'Present') + { + $switch = (Get-VMSwitch -Name $Name -SwitchType $Type -ErrorAction SilentlyContinue) + + # If switch is present and it is external type, that means it doesn't have right properties (TEST code ensures that) + if ($switch -and ($switch.SwitchType -eq 'External')) + { + $removeReaddSwitch = $false + + Write-Verbose -Message ($LocalizedData.CheckingSwitchMessage -f $Name) + if ($switch.EmbeddedTeamingEnabled -eq $false -or $null -eq $switch.EmbeddedTeamingEnabled) + { + if ((Get-NetAdapter -Name $NetAdapterName).InterfaceDescription -ne $switch.NetAdapterInterfaceDescription) + { + Write-Verbose -Message ($LocalizedData.NetAdapterInterfaceIncorrectMessage -f $Name) + $removeReaddSwitch = $true + } + } + else + { + $adapters = (Get-NetAdapter -InterfaceDescription $switch.NetAdapterInterfaceDescriptions -ErrorAction SilentlyContinue).Name + if ($null -ne (Compare-Object -ReferenceObject $adapters -DifferenceObject $NetAdapterName)) + { + Write-Verbose -Message ($LocalizedData.SwitchIncorrectNetworkAdapters -f $Name) + $removeReaddSwitch = $true + } + } + + if (($BandwidthReservationMode -ne "NA") -and ($switch.BandwidthReservationMode -ne $BandwidthReservationMode)) + { + Write-Verbose -Message ($LocalizedData.BandwidthReservationModeIncorrect -f $Name) + $removeReaddSwitch = $true + } + + if ($null -ne $switch.EmbeddedTeamingEnabled -and + $switch.EmbeddedTeamingEnabled -ne $EnableEmbeddedTeaming) + { + Write-Verbose -Message ($LocalizedData.EnableEmbeddedTeamingIncorrect -f $Name) + $removeReaddSwitch = $true + } + + if ($null -ne $switch.EmbeddedTeamingEnabled -and + $switch.EmbeddedTeamingEnabled -ne $EnableEmbeddedTeaming) + { + Write-Verbose -Message ($LocalizedData.EnableEmbeddedTeamingIncorrect -f $Name) + $removeReaddSwitch = $true + } + + if ($PSBoundParameters.ContainsKey('Id') -and $switch.Id -ne $Id) + { + Write-Verbose -Message ($LocalizedData.IdIncorrect -f $Name) + $removeReaddSwitch = $true + } + + if ($removeReaddSwitch) + { + Write-Verbose -Message ($LocalizedData.RemoveAndReaddSwitchMessage -f $Name) + $switch | Remove-VMSwitch -Force + $parameters = @{} + $parameters["Name"] = $Name + $parameters["NetAdapterName"] = $NetAdapterName + + if ($BandwidthReservationMode -ne "NA") + { + $parameters["MinimumBandwidthMode"] = $BandwidthReservationMode + } + + if ($PSBoundParameters.ContainsKey("AllowManagementOS")) + { + $parameters["AllowManagementOS"] = $AllowManagementOS + } + + if ($PSBoundParameters.ContainsKey("EnableEmbeddedTeaming")) + { + $parameters["EnableEmbeddedTeaming"] = $EnableEmbeddedTeaming + } + + if ($PSBoundParameters.ContainsKey('Id')) + { + $parameters["Id"] = $Id.ToString() + } + + $null = New-VMSwitch @parameters + # Since the switch is recreated, the $switch variable is stale and needs to be reassigned + $switch = (Get-VMSwitch -Name $Name -SwitchType $Type -ErrorAction SilentlyContinue) + } + else + { + Write-Verbose -Message ($LocalizedData.SwitchCorrectNetAdapterAndBandwidthMode -f $Name, ($NetAdapterName -join ','), $BandwidthReservationMode) + } + + Write-Verbose -Message ($LocalizedData.CheckAllowManagementOS -f $Name) + if ($PSBoundParameters.ContainsKey("AllowManagementOS") -and ($switch.AllowManagementOS -ne $AllowManagementOS)) + { + Write-Verbose -Message ($LocalizedData.AllowManagementOSIncorrect -f $Name) + $switch | Set-VMSwitch -AllowManagementOS $AllowManagementOS + Write-Verbose -Message ($LocalizedData.AllowManagementOSUpdated -f $Name, $AllowManagementOS) + } + else + { + Write-Verbose -Message ($LocalizedData.AllowManagementOSCorrect -f $Name) + } + } + + # If the switch is not present, create one + else + { + Write-Verbose -Message ($LocalizedData.PresentNotCorrect -f $Name, $Ensure) + Write-Verbose -Message $LocalizedData.CreatingSwitch + $parameters = @{} + $parameters["Name"] = $Name + + if ($BandwidthReservationMode -ne "NA") + { + $parameters["MinimumBandwidthMode"] = $BandwidthReservationMode + } + + if ($NetAdapterName) + { + $parameters["NetAdapterName"] = $NetAdapterName + if ($PSBoundParameters.ContainsKey("AllowManagementOS")) + { + $parameters["AllowManagementOS"] = $AllowManagementOS + } + } + else + { + $parameters["SwitchType"] = $Type + } + + if ($PSBoundParameters.ContainsKey("EnableEmbeddedTeaming")) + { + $parameters["EnableEmbeddedTeaming"] = $EnableEmbeddedTeaming + } + + if ($PSBoundParameters.ContainsKey('Id')) + { + $parameters["Id"] = $Id + } + + $switch = New-VMSwitch @parameters + Write-Verbose -Message ($LocalizedData.PresentCorrect -f $Name, $Ensure) + } + + # Set the load balancing algorithm if it's a SET Switch and the parameter is specified + if($EnableEmbeddedTeaming -eq $true -and $PSBoundParameters.ContainsKey('LoadBalancingAlgorithm')) + { + Write-Verbose -Message ($LocalizedData.SetLoadBalancingAlgorithmMessage -f $Name, $LoadBalancingAlgorithm) + Set-VMSwitchTeam -Name $switch.Name -LoadBalancingAlgorithm $LoadBalancingAlgorithm -Verbose + } + } + # Ensure is set to "Absent", remove the switch + else + { + Get-VMSwitch $Name -ErrorAction SilentlyContinue | Remove-VMSwitch -Force + } +} + +<# +.SYNOPSIS + Tests if MSFT_xVMSwitch resource state is in the desired state or not. + +.PARAMETER Name + Name of the VM Switch. + +.PARAMETER Type + Type of switch. + +.PARAMETER NetAdapterName + Network adapter name(s) for external switch type. + +.PARAMETER AllowManagementOS + Specify if the VM host has access to the physical NIC. + +.PARAMETER EnableEmbeddedTeaming + Should embedded NIC teaming be used (Windows Server 2016 only). + +.PARAMETER BandwidthReservationMode + Type of Bandwidth Reservation Mode to use for the switch. + +.PARAMETER LoadBalancingAlgorithm + The load balancing algorithm that this switch team use. + +.PARAMETER Id + Desired unique ID of the Hyper-V Switch (Windows Server 2016 only). + +.PARAMETER Ensure + Whether switch should be present or absent. +#> +function Test-TargetResource +{ + [CmdletBinding()] + [OutputType([System.Boolean])] + param + ( + [Parameter(Mandatory = $true)] + [String] + $Name, + + [Parameter(Mandatory = $true)] + [ValidateSet("External","Internal","Private")] + [String] + $Type, + + [Parameter()] + [ValidateNotNullOrEmpty()] + [String[]] + $NetAdapterName, + + [Parameter()] + [Boolean] + $AllowManagementOS = $false, + + [Parameter()] + [Boolean] + $EnableEmbeddedTeaming = $false, + + [Parameter()] + [ValidateSet("Default","Weight","Absolute","None","NA")] + [String] + $BandwidthReservationMode = "NA", + + [Parameter()] + [ValidateSet('Dynamic','HyperVPort')] + [String] + $LoadBalancingAlgorithm, + + [Parameter()] + [ValidateNotNullOrEmpty()] + [ValidateScript({$testGuid = New-Guid; if([guid]::TryParse($_,[ref]$testGuid)){return $true}else{Throw 'The VMSwitch Id must be in GUID format!'}})] + [String] + $Id, + + [Parameter()] + [ValidateSet("Present","Absent")] + [String] + $Ensure = "Present" + ) + + # Check if Hyper-V module is present for Hyper-V cmdlets + if (!(Get-Module -ListAvailable -Name Hyper-V)) + { + New-InvalidOperationError ` + -ErrorId 'HyperVNotInstalledError' ` + -ErrorMessage $LocalizedData.HyperVNotInstalledError + } + + #region input validation + if ($Type -eq 'External' -and !($NetAdapterName)) + { + New-InvalidArgumentError ` + -ErrorId 'NetAdapterNameRequiredError' ` + -ErrorMessage $LocalizedData.NetAdapterNameRequiredError + } + + if ($Type -ne 'External' -and $NetAdapterName) + { + New-InvalidArgumentError ` + -ErrorId 'NetAdapterNameNotRequiredError' ` + -ErrorMessage $LocalizedData.NetAdapterNameNotRequiredError + } + + if (($BandwidthReservationMode -ne "NA") -and ((Get-OSVersion) -lt [version]'6.2.0')) + { + New-InvalidArgumentError ` + -ErrorId 'BandwidthReservationModeError' ` + -ErrorMessage $LocalizedData.BandwidthReservationModeError + } + + if ($EnableEmbeddedTeaming -eq $true -and (Get-OSVersion).Major -lt 10) + { + New-InvalidArgumentError ` + -ErrorId 'SETServer2016Error' ` + -ErrorMessage $LocalizedData.SETServer2016Error + } + + if (($PSBoundParameters.ContainsKey('Id')) -and (Get-OSVersion).Major -lt 10) + { + New-InvalidArgumentError ` + -ErrorId 'VMSwitchIDServer2016Error' ` + -ErrorMessage $LocalizedData.VMSwitchIDServer2016Error + } + #endregion + + try + { + # Check if switch exists + Write-Verbose -Message ($LocalizedData.PresentChecking -f $Name, $Ensure) + $switch = Get-VMSwitch -Name $Name -SwitchType $Type -ErrorAction Stop + + # If switch exists + if ($null -ne $switch) + { + Write-Verbose -Message ($LocalizedData.SwitchPresent -f $Name) + # If switch should be present, check the switch type + if ($Ensure -eq 'Present') + { + ## Only check the BandwidthReservationMode if specified + if ($PSBoundParameters.ContainsKey('BandwidthReservationMode')) + { + # If the BandwidthReservationMode is correct, or if $switch.BandwidthReservationMode is $null which means it isn't supported on the OS + Write-Verbose -Message ($LocalizedData.CheckingBandwidthReservationMode -f $Name) + if ($switch.BandwidthReservationMode -eq $BandwidthReservationMode -or $null -eq $switch.BandwidthReservationMode) + { + Write-Verbose -Message ($LocalizedData.BandwidthReservationModeCorrect -f $Name) + } + else + { + Write-Verbose -Message ($LocalizedData.BandwidthReservationModeIncorrect -f $Name) + return $false + } + } + + # If switch is the external type, check additional properties + if ($Type -eq 'External') + { + if ($EnableEmbeddedTeaming -eq $false) + { + Write-Verbose -Message ($LocalizedData.CheckingNetAdapterInterface -f $Name) + $adapter = $null + try + { + $adapter = Get-NetAdapter -Name $NetAdapterName -ErrorAction SilentlyContinue + } + catch + { + # There are scenarios where the SilentlyContinue error action is not honoured, + # so this block serves to handle those and the write-verbose message is here + # to ensure that script analyser doesn't see an empty catch block to throw an + # error + Write-Verbose -Message $LocalizedData.NetAdapterNotFound + } + + if ($adapter.InterfaceDescription -ne $switch.NetAdapterInterfaceDescription) + { + return $false + } + else + { + Write-Verbose -Message ($LocalizedData.NetAdapterInterfaceCorrect -f $Name) + } + } + else + { + Write-Verbose -Message ($LocalizedData.CheckingNetAdapterInterfaces -f $Name) + if ($null -ne $switch.NetAdapterInterfaceDescriptions) + { + $adapters = (Get-NetAdapter -InterfaceDescription $switch.NetAdapterInterfaceDescriptions -ErrorAction SilentlyContinue).Name + if ($null -ne (Compare-Object -ReferenceObject $adapters -DifferenceObject $NetAdapterName)) + { + Write-Verbose -Message ($LocalizedData.IncorrectNetAdapterInterfaces -f $Name) + return $false + } + else + { + Write-Verbose -Message ($LocalizedData.CorrectNetAdapterInterfaces -f $Name) + } + } + else + { + Write-Verbose -Message ($LocalizedData.IncorrectNetAdapterInterfaces -f $Name) + return $false + } + } + + if ($PSBoundParameters.ContainsKey("AllowManagementOS")) + { + Write-Verbose -Message ($LocalizedData.CheckAllowManagementOS -f $Name) + if (($switch.AllowManagementOS -ne $AllowManagementOS)) + { + return $false + } + else + { + Write-Verbose -Message ($LocalizedData.AllowManagementOSCorrect -f $Name) + } + } + + if($PSBoundParameters.ContainsKey('LoadBalancingAlgorithm')) + { + Write-Verbose -Message ($LocalizedData.CheckingLoadBalancingAlgorithm -f $Name) + $loadBalancingAlgorithm = ($switch | Get-VMSwitchTeam).LoadBalancingAlgorithm.toString() + if($loadBalancingAlgorithm -ne $LoadBalancingAlgorithm) + { + return $false + } + else + { + Write-Verbose -Message ($LocalizedData.LoadBalancingAlgorithmCorrect -f $Name) + } + } + } + + # Only check embedded teaming if specified + if ($PSBoundParameters.ContainsKey("EnableEmbeddedTeaming") -eq $true) + { + Write-Verbose -Message ($LocalizedData.CheckEnableEmbeddedTeaming -f $Name) + if ($switch.EmbeddedTeamingEnabled -eq $EnableEmbeddedTeaming -or $null -eq $switch.EmbeddedTeamingEnabled) + { + Write-Verbose -Message ($LocalizedData.EnableEmbeddedTeamingCorrect -f $Name) + } + else + { + Write-Verbose -Message ($LocalizedData.EnableEmbeddedTeamingIncorrect -f $Name) + return $false + } + } + + # Check if the Switch has the desired ID + if ($PSBoundParameters.ContainsKey("Id") -eq $true) + { + Write-Verbose -Message ($LocalizedData.CheckID -f $Name) + if ($switch.Id.Guid -eq $Id) + { + Write-Verbose -Message ($LocalizedData.IdCorrect -f $Name) + } + else + { + Write-Verbose -Message ($LocalizedData.IdIncorrect -f $Name) + return $false + } + } + + return $true + } + # If switch should be absent, but is there, return $false + else + { + return $false + } + } + } + + # If no switch was present + catch [System.Management.Automation.ActionPreferenceStopException] + { + Write-Verbose -Message ($LocalizedData.SwitchNotPresent -f $Name) + return ($Ensure -eq 'Absent') + } +} + +<# +.SYNOPSIS +Returns the OS version +#> +function Get-OSVersion +{ + [Environment]::OSVersion.Version +} + +Export-ModuleMember -Function *-TargetResource diff --git a/deployment/dsc/azshcihost/xHyper-V/3.17.0.0/DSCResources/MSFT_xVMSwitch/MSFT_xVMSwitch.schema.mof b/deployment/dsc/azshcihost/xHyper-V/3.17.0.0/DSCResources/MSFT_xVMSwitch/MSFT_xVMSwitch.schema.mof new file mode 100644 index 0000000..4f6df35 --- /dev/null +++ b/deployment/dsc/azshcihost/xHyper-V/3.17.0.0/DSCResources/MSFT_xVMSwitch/MSFT_xVMSwitch.schema.mof @@ -0,0 +1,14 @@ +[ClassVersion("1.0.0.0"), FriendlyName("xVMSwitch")] +class MSFT_xVMSwitch : OMI_BaseResource +{ + [Key, Description("Name of the VM Switch")] String Name; + [Key, Description("Type of switch"), ValueMap{"External","Internal","Private"}, Values{"External","Internal","Private"}] String Type; + [Write, Description("Network adapter name(s) for external switch type")] String NetAdapterName[]; + [Write, Description("Specify if the VM host has access to the physical NIC")] Boolean AllowManagementOS; + [Write, Description("Should embedded NIC teaming be used (Windows Server 2016 only)")] Boolean EnableEmbeddedTeaming; + [Write, Description("Type of Bandwidth Reservation Mode to use for the switch"), ValueMap{"Default","Weight","Absolute","None","NA"}, Values{"Default","Weight","Absolute","None","NA"}] String BandwidthReservationMode; + [Write, Description("Specifies the load balancing algorithm that this switch team uses"), ValueMap{"Dynamic","HyperVPort"}, Values{"Dynamic","HyperVPort"}] String LoadBalancingAlgorithm; + [Write, Description("Whether switch should be present or absent"), ValueMap{"Present","Absent"}, Values{"Present","Absent"}] String Ensure; + [Write, Description("Unique ID for the switch (Only settable on Windows Server 2016!)")] String Id; + [Read, Description("Description of the network interface")] String NetAdapterInterfaceDescription; +}; diff --git a/deployment/dsc/azshcihost/xHyper-V/3.17.0.0/DSCResources/MSFT_xVMSwitch/en-us/MSFT_xVMSwitch.strings.psd1 b/deployment/dsc/azshcihost/xHyper-V/3.17.0.0/DSCResources/MSFT_xVMSwitch/en-us/MSFT_xVMSwitch.strings.psd1 new file mode 100644 index 0000000..dacd8f9 --- /dev/null +++ b/deployment/dsc/azshcihost/xHyper-V/3.17.0.0/DSCResources/MSFT_xVMSwitch/en-us/MSFT_xVMSwitch.strings.psd1 @@ -0,0 +1,43 @@ +ConvertFrom-StringData @' + CheckingSwitchMessage = Checking switch '{0}' NetAdapterInterface and BandwidthReservationMode ... + NetAdapterInterfaceIncorrectMessage = The switch '{0}' NetAdapterInterface is incorrect ... + SwitchIncorrectNetworkAdapters = Switch '{0}' has an incorrect list of network adapters... + BandwidthReservationModeIncorrect = The switch '{0}' BandwidthReservationMode is incorrect ... + EnableEmbeddedTeamingIncorrect = The switch '{0}' EnableEmbeddedTeaming is incorrect ... + RemoveAndReaddSwitchMessage = Removing switch '{0}' and creating with the correct properties ... + SwitchCorrectNetAdapterAndBandwidthMode = Switch '{0}' set has right network adapter(s) ('{1}') and BandwidthReservationMode ('{2}') + CheckAllowManagementOS = Checking switch '{0}' AllowManagementOS ... + AllowManagementOSIncorrect = Switch '{0}' AllowManagementOS property is not correct + AllowManagementOSUpdated = Switch '{0}' AllowManagementOS property is set to '{1}' + AllowManagementOSCorrect = Switch '{0}' AllowManagementOS is correctly set + PresentNotCorrect = Switch '{0}' is not '{1}'. + CreatingSwitch = Creating Switch ...Checking + PresentCorrect = Switch '{0}' is now '{1}'. + PresentChecking = Checking if Switch '{0}' is '{1}' ... + SwitchPresent = Switch '{0}' is Present + SwitchNotPresent = Switch '{0}' is Absent + CheckingBandwidthReservationMode = Checking if Switch '{0}' has correct BandwidthReservationMode ... + BandwidthReservationModeCorrect = Switch '{0}' has correct BandwidthReservationMode or it does not apply to this OS + CheckingNetAdapterInterface = Checking if Switch '{0}' has correct NetAdapterInterface ... + NetAdapterNotFound = Network adapter not found + NetAdapterInterfaceCorrect = Switch '{0}' has correct NetAdapterInterface + CheckingNetAdapterInterfaces = Checking if Switch '{0}' has correct NetAdapterInterfaces ... + IncorrectNetAdapterInterfaces = Switch '{0}' has an incorrect list of network adapters + CorrectNetAdapterInterfaces = Switch '{0}' has a correct list of network adapters + CheckEnableEmbeddedTeaming = Checking if Switch '{0}' has correct EnableEmbeddedTeaming ... + EnableEmbeddedTeamingCorrect = Switch '{0}' has correct EnableEmbeddedTeaming or it does not apply to this OS + SetLoadBalancingAlgorithmMessage = Set Load Balancing Algorithm of Switch '{0}' to '{1}' + CheckingLoadBalancingAlgorithm = Checking if Switch '{0}' has correct LoadBalancingAlgorithm ... + LoadBalancingAlgorithmCorrect = Switch '{0}' has correct LoadBalancingAlgorithm + + SETServer2016Error = Embedded teaming is only supported on Windows Server 2016 + HyperVNotInstalledError = Please ensure that the Hyper-V role is installed with its PowerShell module + BandwidthReservationModeError = The BandwidthReservationMode cannot be set on a Hyper-V version lower than 2012 + NetAdapterNameRequiredError = For external switch type, NetAdapterName must be specified + NetAdapterNameNotRequiredError = For Internal or Private switch type, NetAdapterName should not be specified + + VMSwitchIDServer2016Error = User defined VMSwitch ID is only supported on Windows Server 2016 and onwards + CheckID = Checking if Switch '{0}' has the desired Id ... + IdCorrect = Switch '{0}' has the desired Id + IdIncorrect = Switch '{0}' has NOT the desired Id +'@ diff --git a/deployment/dsc/azshcihost/xHyper-V/3.17.0.0/DSCResources/MSFT_xVhdFileDirectory/MSFT_xVhdFileDirectory.psm1 b/deployment/dsc/azshcihost/xHyper-V/3.17.0.0/DSCResources/MSFT_xVhdFileDirectory/MSFT_xVhdFileDirectory.psm1 new file mode 100644 index 0000000..218d243 --- /dev/null +++ b/deployment/dsc/azshcihost/xHyper-V/3.17.0.0/DSCResources/MSFT_xVhdFileDirectory/MSFT_xVhdFileDirectory.psm1 @@ -0,0 +1,504 @@ + +<# +# Get the current configuration of the machine +# This function is called when you do Get-DscConfiguration after the configuration is set. +#> +function Get-TargetResource +{ + [CmdletBinding()] + [OutputType([System.Collections.Hashtable])] + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $VhdPath, + + [Parameter(Mandatory = $true)] + [Microsoft.Management.Infrastructure.CimInstance[]] + $FileDirectory, + + [Parameter()] + [ValidateSet('ModifiedDate','SHA-1','SHA-256','SHA-512')] + [System.String] + $CheckSum = 'ModifiedDate' + ) + + if ( -not (Test-path $VhdPath)) + { + $item = New-CimInstance -ClassName MSFT_FileDirectoryConfiguration -Property @{DestinationPath = $VhdPath; Ensure = "Absent"} -Namespace root/microsoft/windows/desiredstateconfiguration -ClientOnly + + Return @{ + VhdPath = $VhdPath + FileDirectory = $item + } + } + + # Mount VHD. + $mountVHD = EnsureVHDState -Mounted -vhdPath $vhdPath + + $itemsFound = foreach($Item in $FileDirectory) + { + $item = GetItemToCopy -item $item + $mountedDrive = $mountVHD | Get-Disk | Get-Partition | Where-Object -FilterScript {$_.Type -ne 'Recovery'} | Get-Volume + $letterDrive = (-join $mountedDrive.DriveLetter) + ":\" + + # show the drive letters. + Get-PSDrive | Write-Verbose + + $finalPath = Join-Path $letterDrive $item.DestinationPath + + Write-Verbose "Getting the current value at $finalPath ..." + + if (Test-Path $finalPath) + { + New-CimInstance -ClassName MSFT_FileDirectoryConfiguration -Property @{DestinationPath = $finalPath; Ensure = "Present"} -Namespace root/microsoft/windows/desiredstateconfiguration -ClientOnly + } + else + { + New-CimInstance -ClassName MSFT_FileDirectoryConfiguration -Property @{DestinationPath = $finalPath ; Ensure = "Absent"} -Namespace root/microsoft/windows/desiredstateconfiguration -ClientOnly + } + } + + # Dismount VHD. + EnsureVHDState -Dismounted -vhdPath $VhdPath + + # Return the result. + Return @{ + VhdPath = $VhdPath + FileDirectory = $itemsFound + } +} + + +# This is a resource method that gets called if the Test-TargetResource returns false. +function Set-TargetResource +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $VhdPath, + + [Parameter(Mandatory = $true)] + [Microsoft.Management.Infrastructure.CimInstance[]] + $FileDirectory, + + [Parameter()] + [ValidateSet('ModifiedDate','SHA-1','SHA-256','SHA-512')] + [System.String] + $CheckSum = 'ModifiedDate' + ) + + if (-not (Test-Path $VhdPath)) { throw "Specified destination path $VhdPath does not exist!"} + + # mount the VHD. + $mountedVHD = EnsureVHDState -Mounted -vhdPath $VhdPath + + try + { + # show the drive letters. + Get-PSDrive | Write-Verbose + + $mountedDrive = $mountedVHD | Get-Disk | Get-Partition | Where-Object -FilterScript {$_.Type -ne 'Recovery'} | Get-Volume + + foreach ($item in $FileDirectory) + { + $itemToCopy = GetItemToCopy -item $item + $letterDrive = (-join $mountedDrive.DriveLetter) + ":\" + $finalDestinationPath = $letterDrive + $finalDestinationPath = Join-Path $letterDrive $itemToCopy.DestinationPath + + # if the destination should be removed + if (-not($itemToCopy.Ensure)) + { + if (Test-Path $finalDestinationPath) + { + SetVHDFile -destinationPath $finalDestinationPath -ensure:$false -recurse:($itemToCopy.Recurse) + } + } + else + { + # Copy Scenario + if ($itemToCopy.SourcePath) + { + SetVHDFile -sourcePath $itemToCopy.SourcePath -destinationPath $finalDestinationPath -recurse:($itemToCopy.Recurse) -force:($itemToCopy.Force) + } + elseif ($itemToCopy.Content) + { + "Writing a content to a file" + + # if the type is not specified assume it is a file. + if (-not ($itemToCopy.Type)) + { + $itemToCopy.Type = 'File' + } + + # Create file/folder scenario + SetVHDFile -destinationPath $finalDestinationPath -type $itemToCopy.Type -force:($itemToCopy.Force) -content $itemToCopy.Content + } + + # Set Attribute scenario + if ($itemToCopy.Attributes) + { + SetVHDFile -destinationPath $finalDestinationPath -attribute $itemToCopy.Attributes -force:($itemToCopy.Force) + } + } + + } + } + finally + { + EnsureVHDState -Dismounted -vhdPath $VhdPath + } +} + +# This function returns if the current configuration of the machine is the same as the desired configration for this resource. +function Test-TargetResource +{ + [CmdletBinding()] + [OutputType([System.Boolean])] + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $VhdPath, + + [Parameter(Mandatory = $true)] + [Microsoft.Management.Infrastructure.CimInstance[]] + $FileDirectory, + + [Parameter()] + [ValidateSet('ModifiedDate','SHA-1','SHA-256','SHA-512')] + [System.String] + $CheckSum = 'ModifiedDate' + ) + + # If the VHD path does not exist throw an error and stop. + if ( -not (Test-Path $VhdPath)) + { + throw "VHD does not exist in the specified path $VhdPath" + } + + # mount the vhd. + $mountedVHD = EnsureVHDState -Mounted -vhdPath $VhdPath + + try + { + # Show the drive letters after mount + Get-PSDrive | Write-Verbose + + $mountedDrive = $mountedVHD | Get-Disk | Get-Partition | Where-Object -FilterScript {$_.Type -ne 'Recovery'} | Get-Volume + $letterDrive = (-join $mountedDrive.DriveLetter) + ":\" + Write-Verbose $letterDrive + + # return test result equal to true unless one of the tests in the loop below fails. + $result = $true + + foreach ($item in $FileDirectory) + { + $itemToCopy = GetItemToCopy -item $item + $destination = $itemToCopy.DestinationPath + Write-Verbose ("Testing the file with relative VHD destination $destination") + $destination = $itemToCopy.DestinationPath + $finalDestinationPath = $letterDrive + $finalDestinationPath = Join-Path $letterDrive $destination + + if (Test-Path $finalDestinationPath) + { + if( -not ($itemToCopy.Ensure)) + { + $result = $false + break; + } + else + { + $itemToCopyIsFile = Test-Path $itemToCopy.SourcePath -PathType Leaf + $destinationIsFolder = Test-Path $finalDestinationPath -PathType Container + + if ($itemToCopyIsFile -and $destinationIsFolder) + { + # Verify if the file exist inside the folder + $fileName = Split-Path $itemToCopy.SourcePath -Leaf + Write-Verbose "Checking if $fileName exist under $finalDestinationPath" + $fileExistInDestination = Test-Path (Join-Path $finalDestinationPath $fileName) + + # Report if the file exist on the destination folder. + Write-Verbose "File exist on the destination under $finalDestinationPath :- $fileExistInDestination" + $result = $fileExistInDestination + $result = $result -and -not(ItemHasChanged -sourcePath $itemToCopy.SourcePath -destinationPath (Join-Path $finalDestinationPath $fileName) -CheckSum $CheckSum) + } + + if (($itemToCopy.Type -eq "Directory") -and ($itemToCopy.Recurse)) + { + $result = $result -and -not(ItemHasChanged -sourcePath $itemToCopy.SourcePath -destinationPath $finalDestinationPath -CheckSum $CheckSum) + + if (-not ($result)) + { + break; + } + } + } + } + else + { + # If Ensure is specified as Present or if Ensure is not specified at all. + if(($itemToCopy.Ensure)) + { + $result = $false + break; + } + } + + # Check the attribute + if ($itemToCopy.Attributes) + { + $currentAttribute = @(Get-ItemProperty -Path $finalDestinationPath | ForEach-Object -MemberName Attributes) + $result = $currentAttribute.Contains($itemToCopy.Attributes) + } + } + } + finally + { + EnsureVHDState -Dismounted -vhdPath $VhdPath + } + + + Write-Verbose "Test returned $result" + return $result; +} + +# Assert the state of the VHD. +function EnsureVHDState +{ + [CmdletBinding(DefaultParametersetName="Mounted")] + param( + + [Parameter(ParameterSetName = "Mounted")] + [switch]$Mounted, + [Parameter(ParameterSetName = "Dismounted")] + [switch]$Dismounted, + [Parameter(Mandatory=$true)] + $vhdPath + ) + + if ( -not ( Get-Module -ListAvailable Hyper-v)) + { + throw "Hyper-v-Powershell Windows Feature is required to run this resource. Please install Hyper-v feature and try again" + } + if ($PSCmdlet.ParameterSetName -eq 'Mounted') + { + # Try mounting the VHD. + $mountedVHD = Mount-VHD -Path $vhdPath -Passthru -ErrorAction SilentlyContinue -ErrorVariable var + + # If mounting the VHD failed. Dismount the VHD and mount it again. + if ($var) + { + Write-Verbose "Mounting Failed. Attempting to dismount and mount it back" + Dismount-VHD $vhdPath + $mountedVHD = Mount-VHD -Path $vhdPath -Passthru -ErrorAction SilentlyContinue + + return $mountedVHD + } + else + { + return $mountedVHD + } + } + else + { + Dismount-VHD $vhdPath -ea SilentlyContinue + + } +} + +# Change the Cim Instance objects in to a hash table containing property value pair. +function GetItemToCopy +{ + param( + [Parameter()] + [Microsoft.Management.Infrastructure.CimInstance] $item + ) + + #Initialize Return Object + $returnValue = @{} + + #Define Default Values + + $DesiredProperties = [ordered]@{ + 'SourcePath' = $null + 'DestinationPath' = $null + 'Ensure' = 'Present' + 'Recurse' = 'True' + 'Force' = 'True' + 'Content' = $null + 'Attributes' = $null + 'Type' = 'Directory' + } + + [string[]]($DesiredProperties.Keys) | Foreach-Object -Process { + #Get Property Value + $thisItem = $item.CimInstanceProperties[$_].Value + + if (-not $thisItem -and $_ -in $DefaultValues.Keys) + { + #If unset and a default value is defined enter here + if ($_ -eq 'Type') + { + #Special behavior for the Type property based on SourcePath + #This relies on SourcePath preceeding Type in the list of keys (the reason for using OrderedDictionary) + if (Test-Path $returnValue.SourcePath -PathType Leaf ) + { + #If the sourcepath resolves to a file, set the default to File instad of Directory + $DefaultValues.Type = 'File' + } + } + $returnValue[$_] = $DefaultValues[$_] + } + else + { + #If value present or no default value enter here + $returnValue[$_] = $item.CimInstanceProperties[$_].Value + } + } + + #Relies on default values in the $DesiredProperties object being the $True equivalent values + $PropertyValuesToBoolean = @( + 'Force', + 'Recurse', + 'Ensure' + ) + + # Convert string values to boolean for ease of programming. + $PropertyValuesToBoolean | ForEach-Object -Process { + $returnValue[$_] = $returnValue[$_] -eq $DesiredProperties[$_] + } + + + $returnValue.Keys | ForEach-Object -Process { + Write-Verbose "$_ => $($returnValue[$_])" + } + + return $returnValue +} + + +# This is the main function that gets called after the file is mounted to perform copy, set or new operations on the mounted drive. +function SetVHDFile +{ + [CmdletBinding(DefaultParametersetName="Copy")] + param( + [Parameter(Mandatory=$true,ParameterSetName = "Copy")] + $sourcePath, + [Parameter()] + [switch]$recurse, + [Parameter()] + [switch]$force, + [Parameter(ParameterSetName = "New")] + $type, + [Parameter(ParameterSetName = "New")] + $content, + [Parameter(Mandatory=$true)] + $destinationPath, + [Parameter(Mandatory=$true,ParameterSetName = "Set")] + $attribute, + [Parameter(Mandatory=$true,ParameterSetName = "Delete")] + [switch]$ensure + ) + + Write-Verbose "Setting the VHD file $($PSCmdlet.ParameterSetName)" + if ($PSCmdlet.ParameterSetName -eq 'Copy') + { + New-Item -Path (Split-Path $destinationPath) -ItemType Directory -ErrorAction SilentlyContinue + Copy-Item -Path $sourcePath -Destination $destinationPath -Force:$force -Recurse:$recurse -ErrorAction SilentlyContinue + } + elseif ($PSCmdlet.ParameterSetName -eq 'New') + { + If ($type -eq 'Directory') + { + New-Item -Path $destinationPath -ItemType $type + } + else + { + New-Item -Path $destinationPath -ItemType $type + $content | Out-File $destinationPath + } + + } + elseif ($PSCmdlet.ParameterSetName -eq 'Set') + { + Write-Verbose "Attempting to change the attribute of the file $destinationPath to value $attribute" + Set-ItemProperty -Path $destinationPath -Name Attributes -Value $attribute + } + elseif (!($ensure)) + { + Remove-Item -Path $destinationPath -Force:$force -Recurse:$recurse + } +} + +# Detect if the item to be copied is modified version of the orginal. +function ItemHasChanged +{ + param( + [Parameter(Mandatory=$true)] + [ValidateScript({Test-Path $_})] + $sourcePath, + [Parameter(Mandatory=$true)] + [ValidateScript({Test-Path $_})] + $destinationPath, + [Parameter()] + [ValidateSet('ModifiedDate','SHA-1','SHA-256','SHA-512')] + $CheckSum = 'ModifiedDate' + ) + + $itemIsFolder = Test-Path $sourcePath -Type Container + $sourceItems = $null; + $destinationItems = $null; + + if ($itemIsFolder) + { + $sourceItems = Get-ChildItem "$sourcePath\*.*" -Recurse + $destinationItems = Get-ChildItem "$destinationPath\*.*" -Recurse + + } + else + { + $sourceItems = Get-ChildItem $sourcePath + $destinationItems = Get-ChildItem $destinationPath + + } + + if ( -not ($destinationItems)) + { + return $true; + } + + # Compute the difference using the algorithem specified. + $difference = $null + + switch ($CheckSum) + { + 'ModifiedDate' + { + $difference = Compare-Object -ReferenceObject $sourceItems -DifferenceObject $destinationItems -Property LastWriteTime + } + 'SHA-1' + { + $difference = Compare-Object -ReferenceObject ($sourceItems | Get-FileHash -Algorithm SHA1) -DifferenceObject ($destinationItems | Get-FileHash -Algorithm SHA1) -Property Hash + } + 'SHA-256' + { + $difference = Compare-Object -ReferenceObject ($sourceItems | Get-FileHash -Algorithm SHA256) -DifferenceObject ($destinationItems | Get-FileHash -Algorithm SHA256) -Property Hash + } + 'SHA-512' + { + $difference = Compare-Object -ReferenceObject ($sourceItems | Get-FileHash -Algorithm SHA512) -DifferenceObject ($destinationItems | Get-FileHash -Algorithm SHA512) -Property Hash + } + } + # If there are object difference between the item at the source and Items at the distenation. + return ($null -ne $difference) + +} + +Export-ModuleMember -Function *-TargetResource + diff --git a/deployment/dsc/azshcihost/xHyper-V/3.17.0.0/DSCResources/MSFT_xVhdFileDirectory/MSFT_xVhdFileDirectory.schema.mof b/deployment/dsc/azshcihost/xHyper-V/3.17.0.0/DSCResources/MSFT_xVhdFileDirectory/MSFT_xVhdFileDirectory.schema.mof new file mode 100644 index 0000000..b221b4b --- /dev/null +++ b/deployment/dsc/azshcihost/xHyper-V/3.17.0.0/DSCResources/MSFT_xVhdFileDirectory/MSFT_xVhdFileDirectory.schema.mof @@ -0,0 +1,24 @@ + +[ClassVersion("1.0.0.0")] +Class MSFT_xFileDirectory +{ + [Required] string DestinationPath; + [Write] string SourcePath; + [Write,ValueMap{"Present", "Absent"},Values{"Present", "Absent"}] string Ensure; + [Write,ValueMap{"File", "Directory"},Values{"File", "Directory"}] string Type; + [Write] boolean Recurse; + [Write] boolean Force ; + [write] string Content; + [Write,ValueMap{"ReadOnly", "Hidden", "System", "Archive"},Values{"ReadOnly", "Hidden", "System", "Archive"}] string Attributes[]; +}; + +[ClassVersion("1.0.0.0"), FriendlyName("xVhdFile")] +class MSFT_xVhdFileDirectory : OMI_BaseResource +{ + [Key, Description("Path to the VHD")] String VhdPath; + [Required, EmbeddedInstance("MSFT_xFileDirectory"), Description("The FileDirectory objects to copy to the VHD")] String FileDirectory[]; + [Write,ValueMap{"ModifiedDate","SHA-1","SHA-256","SHA-512"},Values{"ModifiedDate","SHA-1","SHA-256","SHA-512"}] string CheckSum; +}; + + + diff --git a/deployment/dsc/azshcihost/xHyper-V/3.17.0.0/Examples/Sample_xVHD_AdditionalPropertyVHD.ps1 b/deployment/dsc/azshcihost/xHyper-V/3.17.0.0/Examples/Sample_xVHD_AdditionalPropertyVHD.ps1 new file mode 100644 index 0000000..c552cbe --- /dev/null +++ b/deployment/dsc/azshcihost/xHyper-V/3.17.0.0/Examples/Sample_xVHD_AdditionalPropertyVHD.ps1 @@ -0,0 +1,46 @@ +configuration Sample_xVHD_AdditionalPropertyVHD +{ + param + ( + [Parameter(Mandatory = $true)] + [string] + $Name, + + [Parameter(Mandatory = $true)] + [string] + $Path, + + [Parameter(Mandatory = $true)] + [string] + $ParentPath, + + [Parameter(Mandatory = $true)] + [string] + $MaximumSizeBytes, + + [Parameter()] + [ValidateSet('Vhd', 'Vhdx')] + [string] + $Generation = 'Vhd', + + [Parameter()] + [ValidateSet('Present', 'Absent')] + [string] + $Ensure = 'Present' + ) + + Import-DscResource -ModuleName xHyper-V + + Node localhost + { + xVHD WrongVHD + { + Ensure = $Ensure + Name = $Name + Path = $Path + ParentPath = $ParentPath + MaximumSizeBytes = $MaximumSizeBytes + Generation = $Generation + } + } +} diff --git a/deployment/dsc/azshcihost/xHyper-V/3.17.0.0/Examples/Sample_xVHD_DiffVHD.ps1 b/deployment/dsc/azshcihost/xHyper-V/3.17.0.0/Examples/Sample_xVHD_DiffVHD.ps1 new file mode 100644 index 0000000..303544a --- /dev/null +++ b/deployment/dsc/azshcihost/xHyper-V/3.17.0.0/Examples/Sample_xVHD_DiffVHD.ps1 @@ -0,0 +1,64 @@ +configuration Sample_xVhd_DiffVhd +{ + param + ( + [Parameter()] + [string[]] + $NodeName = 'localhost', + + [Parameter(Mandatory = $true)] + [string] + $Name, + + [Parameter(Mandatory = $true)] + [string] + $Path, + + [Parameter(Mandatory = $true)] + [string] + $ParentPath, + + [Parameter()] + [ValidateSet('Vhd', 'Vhdx')] + [string] + $Generation = 'Vhd', + + [Parameter()] + [ValidateSet('Dynamic', 'Fixed', 'Differencing')] + [string]$Type = 'Differencing', + + [Parameter()] + [ValidateSet('Present', 'Absent')] + [string] + $Ensure = 'Present' + ) + + Import-DscResource -ModuleName xHyper-V + + Node $NodeName + { + # Install HyperV feature, if not installed - Server SKU only + WindowsFeature HyperV + { + Ensure = 'Present' + Name = 'Hyper-V' + } + + WindowsFeature HyperVPowerShell + { + Ensure = 'Present' + Name = 'Hyper-V-PowerShell' + } + + xVhd DiffVhd + { + Ensure = $Ensure + Name = $Name + Path = $Path + ParentPath = $ParentPath + Generation = $Generation + Type = $Type + DependsOn = '[WindowsFeature]HyperV', '[WindowsFeature]HyperVPowerShell' + } + } +} diff --git a/deployment/dsc/azshcihost/xHyper-V/3.17.0.0/Examples/Sample_xVHD_FixedVHD.ps1 b/deployment/dsc/azshcihost/xHyper-V/3.17.0.0/Examples/Sample_xVHD_FixedVHD.ps1 new file mode 100644 index 0000000..94777a1 --- /dev/null +++ b/deployment/dsc/azshcihost/xHyper-V/3.17.0.0/Examples/Sample_xVHD_FixedVHD.ps1 @@ -0,0 +1,60 @@ +configuration Sample_xVhd_FixedVhd +{ + param + ( + [Parameter()] + [string[]] + $NodeName = 'localhost', + + [Parameter(Mandatory = $true)] + [string] + $Name, + + [Parameter(Mandatory = $true)] + [string] + $Path, + + [Parameter()] + [ValidateSet('Vhd', 'Vhdx')] + [string] + $Generation = 'Vhd', + + [Parameter()] + [ValidateSet('Dynamic', 'Fixed', 'Differencing')] + [string] + $Type = 'Fixed', + + [Parameter()] + [ValidateSet('Present', 'Absent')] + [string] + $Ensure = 'Present' + ) + + Import-DscResource -ModuleName xHyper-V + + Node $NodeName + { + # Install HyperV feature, if not installed - Server SKU only + WindowsFeature HyperV + { + Ensure = 'Present' + Name = 'Hyper-V' + } + + WindowsFeature HyperVPowerShell + { + Ensure = 'Present' + Name = 'Hyper-V-PowerShell' + } + + xVhd DiffVhd + { + Ensure = $Ensure + Name = $Name + Path = $Path + Generation = $Generation + Type = $Type + DependsOn = '[WindowsFeature]HyperV', '[WindowsFeature]HyperVPowerShell' + } + } +} diff --git a/deployment/dsc/azshcihost/xHyper-V/3.17.0.0/Examples/Sample_xVHD_MissingPropertyVHD.ps1 b/deployment/dsc/azshcihost/xHyper-V/3.17.0.0/Examples/Sample_xVHD_MissingPropertyVHD.ps1 new file mode 100644 index 0000000..baa7037 --- /dev/null +++ b/deployment/dsc/azshcihost/xHyper-V/3.17.0.0/Examples/Sample_xVHD_MissingPropertyVHD.ps1 @@ -0,0 +1,36 @@ +configuration Sample_xVHD_MissingPropertyVHD +{ + param + ( + [Parameter(Mandatory = $true)] + [string] + $Name, + + [Parameter(Mandatory = $true)] + [string] + $Path, + + [Parameter()] + [ValidateSet('Vhd', 'Vhdx')] + [string] + $Generation = 'Vhd', + + [Parameter()] + [ValidateSet('Present', 'Absent')] + [string] + $Ensure = 'Present' + ) + + Import-DscResource -module xHyper-V + + Node localhost + { + xVHD WrongVHD + { + Ensure = $Ensure + Name = $Name + Path = $Path + Generation = $Generation + } + } +} diff --git a/deployment/dsc/azshcihost/xHyper-V/3.17.0.0/Examples/Sample_xVHD_NewVHD.ps1 b/deployment/dsc/azshcihost/xHyper-V/3.17.0.0/Examples/Sample_xVHD_NewVHD.ps1 new file mode 100644 index 0000000..f607afb --- /dev/null +++ b/deployment/dsc/azshcihost/xHyper-V/3.17.0.0/Examples/Sample_xVHD_NewVHD.ps1 @@ -0,0 +1,58 @@ +configuration Sample_xVHD_NewVhd +{ + param + ( + [Parameter()] + [string[]] + $NodeName = 'localhost', + + [Parameter(Mandatory = $true)] + [string] + $Name, + + [Parameter(Mandatory = $true)] + [string] + $Path, + + [Parameter(Mandatory = $true)] + [Uint64] + $MaximumSizeBytes, + + [Parameter()] + [ValidateSet('Vhd', 'Vhdx')] + [string]$Generation = 'Vhd', + + [Parameter()] + [ValidateSet('Present', 'Absent')] + [string] + $Ensure = 'Present' + ) + + Import-DscResource -ModuleName xHyper-V + + Node $NodeName + { + # Install HyperV feature, if not installed - Server SKU only + WindowsFeature HyperV + { + Ensure = 'Present' + Name = 'Hyper-V' + } + + WindowsFeature HyperVPowerShell + { + Ensure = 'Present' + Name = 'Hyper-V-PowerShell' + } + + xVhd NewVhd + { + Ensure = $Ensure + Name = $Name + Path = $Path + Generation = $Generation + MaximumSizeBytes = $MaximumSizeBytes + DependsOn = '[WindowsFeature]HyperV', '[WindowsFeature]HyperVPowerShell' + } + } +} diff --git a/deployment/dsc/azshcihost/xHyper-V/3.17.0.0/Examples/Sample_xVMHardDiskDrive_VMWith4AdditionalDisks.ps1 b/deployment/dsc/azshcihost/xHyper-V/3.17.0.0/Examples/Sample_xVMHardDiskDrive_VMWith4AdditionalDisks.ps1 new file mode 100644 index 0000000..7cf165b --- /dev/null +++ b/deployment/dsc/azshcihost/xHyper-V/3.17.0.0/Examples/Sample_xVMHardDiskDrive_VMWith4AdditionalDisks.ps1 @@ -0,0 +1,97 @@ +configuration Sample_xVMHardDiskDrive +{ + param + ( + [Parameter()] + [string[]] + $NodeName = 'localhost', + + [Parameter(Mandatory = $true)] + [string] + $VMName, + + [Parameter(Mandatory = $true)] + [string] + $VhdPath + ) + + Import-DscResource -ModuleName 'xHyper-V' + Import-DscResource -ModuleName 'PSDesiredStateConfiguration' + + Node $NodeName + { + $diskNameOS = "$VMName-OS.vhdx" + + # Install HyperV feature, if not installed - Server SKU only + WindowsFeature HyperV + { + Ensure = 'Present' + Name = 'Hyper-V' + } + + WindowsFeature HyperVPowerShell + { + Ensure = 'Present' + Name = 'Hyper-V-PowerShell' + } + + # Create the VHD for the OS + xVHD DiskOS + { + + Name = $diskNameOS + Path = $VhdPath + Generation = 'vhdx' + MaximumSizeBytes = 20GB + Ensure = 'Present' + DependsOn = '[WindowsFeature]HyperV' + } + + # Create the VM + xVMHyperV NewVM + { + Name = $VMName + VhdPath = Join-Path $VhdPath -ChildPath $diskNameOS + Generation = 1 + Ensure = 'Present' + DependsOn = '[xVHD]DiskOS' + } + + # Ensures a SCSI controller exists on the VM + xVMScsiController Controller + { + Ensure = 'Present' + VMName = $VMName + ControllerNumber = 0 + DependsOn = '[xVMHyperV]NewVM' + } + + foreach ($i in 0 .. 3) + { + $diskName = "$VMName-Disk-$i.vhdx" + + # Create the VHD + xVHD "Disk-$i" + { + + Name = $diskName + Path = $VhdPath + Generation = 'vhdx' + MaximumSizeBytes = 20GB + Ensure = 'Present' + DependsOn = '[WindowsFeature]HyperV' + } + + # Attach the VHD + xVMHardDiskDrive "ExtraDisk-$i" + { + VMName = $VMName + Path = Join-Path $VhdPath -ChildPath $diskName + ControllerType = 'SCSI' + ControllerLocation = $i + Ensure = 'Present' + DependsOn = '[xVMScsiController]Controller', "[xVHD]Disk-$i" + } + } + } +} diff --git a/deployment/dsc/azshcihost/xHyper-V/3.17.0.0/Examples/Sample_xVMHardDiskDrive_VMWithExtraDisk.ps1 b/deployment/dsc/azshcihost/xHyper-V/3.17.0.0/Examples/Sample_xVMHardDiskDrive_VMWithExtraDisk.ps1 new file mode 100644 index 0000000..0b01fbc --- /dev/null +++ b/deployment/dsc/azshcihost/xHyper-V/3.17.0.0/Examples/Sample_xVMHardDiskDrive_VMWithExtraDisk.ps1 @@ -0,0 +1,79 @@ +configuration Sample_xVMHardDiskDrive +{ + param + ( + [Parameter()] + [System.String[]] + $NodeName = 'localhost', + + [Parameter(Mandatory = $true)] + [System.String] + $VMName, + + [Parameter(Mandatory = $true)] + [System.String] + $VhdPath + ) + + Import-DscResource -ModuleName 'xHyper-V' + Import-DscResource -ModuleName 'PSDesiredStateConfiguration' + + Node $NodeName + { + # Install HyperV feature, if not installed - Server SKU only + $diskNameOS = "$VMName-DiskOS.vhdx" + $diskNameExtra1 = "$VMName-Disk1.vhdx" + + WindowsFeature HyperV + { + Ensure = 'Present' + Name = 'Hyper-V' + } + + WindowsFeature HyperVPowerShell + { + Ensure = 'Present' + Name = 'Hyper-V-PowerShell' + } + + xVHD DiskOS + { + Name = $diskNameOS + Path = $VhdPath + Generation = 'vhdx' + MaximumSizeBytes = 20GB + Ensure = 'Present' + DependsOn = '[WindowsFeature]HyperV' + } + + xVHD Disk1 + { + Name = $diskNameExtra1 + Path = $VhdPath + Generation = 'vhdx' + MaximumSizeBytes = 20GB + Ensure = 'Present' + DependsOn = '[WindowsFeature]HyperV' + } + + xVMHyperV NewVM + { + Ensure = 'Present' + Name = $VMName + VhdPath = Join-Path $VhdPath -ChildPath $diskNameOS + Generation = 1 + DependsOn = '[xVHD]DiskOS' + } + + xVMHardDiskDrive ExtraDisk + { + VMName = $VMName + Path = Join-Path $VhdPath -ChildPath $diskNameExtra1 + ControllerType = 'IDE' + ControllerNumber = 0 + ControllerLocation = 1 + Ensure = 'Present' + DependsOn = '[xVHD]Disk1' + } + } +} diff --git a/deployment/dsc/azshcihost/xHyper-V/3.17.0.0/Examples/Sample_xVMHost_Paths.ps1 b/deployment/dsc/azshcihost/xHyper-V/3.17.0.0/Examples/Sample_xVMHost_Paths.ps1 new file mode 100644 index 0000000..044bc55 --- /dev/null +++ b/deployment/dsc/azshcihost/xHyper-V/3.17.0.0/Examples/Sample_xVMHost_Paths.ps1 @@ -0,0 +1,22 @@ +Configuration HyperVHostPaths +{ + param + ( + [Parameter(Mandatory=$true, Position=0)] + [ValidateScript({Test-Path $_})] + $VirtualHardDiskPath, + + [Parameter(Mandatory=$true, Position=1)] + [ValidateScript({Test-Path $_})] + $VirtualMachinePath + ) + + Import-DscResource -moduleName xHyper-V + + xVMHost HyperVHostPaths + { + IsSingleInstance = 'Yes' + VirtualHardDiskPath = $VirtualHardDiskPath + VirtualMachinePath = $VirtualMachinePath + } +} diff --git a/deployment/dsc/azshcihost/xHyper-V/3.17.0.0/Examples/Sample_xVMHyperV_Complete.ps1 b/deployment/dsc/azshcihost/xHyper-V/3.17.0.0/Examples/Sample_xVMHyperV_Complete.ps1 new file mode 100644 index 0000000..6ec87db --- /dev/null +++ b/deployment/dsc/azshcihost/xHyper-V/3.17.0.0/Examples/Sample_xVMHyperV_Complete.ps1 @@ -0,0 +1,103 @@ +configuration Sample_xVMHyperV_Complete +{ + param + ( + [string[]]$NodeName = 'localhost', + + [Parameter(Mandatory)] + [string]$VMName, + + [Parameter(Mandatory)] + [uint64]$VhdSizeBytes, + + [Parameter(Mandatory)] + [Uint64]$StartupMemory, + + [Parameter(Mandatory)] + [Uint64]$MinimumMemory, + + [Parameter(Mandatory)] + [Uint64]$MaximumMemory, + + [Parameter(Mandatory)] + [String]$SwitchName, + + [Parameter(Mandatory)] + [String]$Path, + + [Parameter(Mandatory)] + [Uint32]$ProcessorCount, + + [ValidateSet('Off','Paused','Running')] + [String]$State = 'Off', + + [Switch]$WaitForIP, + + [bool]$AutomaticCheckpointsEnabled + ) + + Import-DscResource -module xHyper-V + + Node $NodeName + { + # Logic to handle both Client and Server OS + # Configuration needs to be compiled on target server + $Operatingsystem = Get-CimInstance -ClassName Win32_OperatingSystem + if ($Operatingsystem.ProductType -eq 1) + { + # Client OS, install Hyper-V as OptionalFeature + $HyperVDependency = '[WindowsOptionalFeature]HyperV' + WindowsOptionalFeature HyperV + { + Ensure = 'Enable' + Name = 'Microsoft-Hyper-V-All' + } + } + else { + # Server OS, install HyperV as WindowsFeature + $HyperVDependency = '[WindowsFeature]HyperV','[WindowsFeature]HyperVPowerShell' + WindowsFeature HyperV + { + Ensure = 'Present' + Name = 'Hyper-V' + } + WindowsFeature HyperVPowerShell + { + Ensure = 'Present' + Name = 'Hyper-V-PowerShell' + } + } + + # Create new VHD + xVhd NewVhd + { + Ensure = 'Present' + Name = "$VMName-OSDisk.vhdx" + Path = $Path + Generation = 'vhdx' + MaximumSizeBytes = $VhdSizeBytes + DependsOn = $HyperVDependency + } + + # Ensures a VM with all the properties + xVMHyperV NewVM + { + Ensure = 'Present' + Name = $VMName + VhdPath = (Join-Path -Path $Path -ChildPath "$VMName-OSDisk.vhdx") + SwitchName = $SwitchName + State = $State + Path = $Path + Generation = 2 + StartupMemory = $StartupMemory + MinimumMemory = $MinimumMemory + MaximumMemory = $MaximumMemory + ProcessorCount = $ProcessorCount + MACAddress = $MACAddress + RestartIfNeeded = $true + WaitForIP = $WaitForIP + AutomaticCheckpointsEnabled = $AutomaticCheckpointsEnabled + DependsOn = '[xVhd]NewVhd' + } + } +} diff --git a/deployment/dsc/azshcihost/xHyper-V/3.17.0.0/Examples/Sample_xVMHyperV_DynamicMemory.ps1 b/deployment/dsc/azshcihost/xHyper-V/3.17.0.0/Examples/Sample_xVMHyperV_DynamicMemory.ps1 new file mode 100644 index 0000000..4f260cd --- /dev/null +++ b/deployment/dsc/azshcihost/xHyper-V/3.17.0.0/Examples/Sample_xVMHyperV_DynamicMemory.ps1 @@ -0,0 +1,47 @@ +configuration Sample_xVMHyperV_DynamicMemory +{ + param + ( + [string[]]$NodeName = 'localhost', + + [Parameter(Mandatory)] + [string]$VMName, + + [Parameter(Mandatory)] + [string]$VhdPath, + + [Parameter(Mandatory)] + [Uint64]$StartupMemory, + + [Parameter(Mandatory)] + [Uint64]$MinimumMemory, + + [Parameter(Mandatory)] + [Uint64]$MaximumMemory + ) + + Import-DscResource -module xHyper-V + + Node $NodeName + { + # Install HyperV feature, if not installed - Server SKU only + WindowsFeature HyperV + { + Ensure = 'Present' + Name = 'Hyper-V' + } + + # Ensures a VM with dynamic memory + xVMHyperV NewVM + { + Ensure = 'Present' + Name = $VMName + VhdPath = $VhdPath + Generation = 2 + StartupMemory = $StartupMemory + MinimumMemory = $MinimumMemory + MaximumMemory = $MaximumMemory + DependsOn = '[WindowsFeature]HyperV' + } + } +} diff --git a/deployment/dsc/azshcihost/xHyper-V/3.17.0.0/Examples/Sample_xVMHyperV_MultipleNICs.ps1 b/deployment/dsc/azshcihost/xHyper-V/3.17.0.0/Examples/Sample_xVMHyperV_MultipleNICs.ps1 new file mode 100644 index 0000000..09adbae --- /dev/null +++ b/deployment/dsc/azshcihost/xHyper-V/3.17.0.0/Examples/Sample_xVMHyperV_MultipleNICs.ps1 @@ -0,0 +1,72 @@ +Configuration Sample_xVMHyperV_MultipleNICs +{ + param + ( + [string[]]$NodeName = 'localhost', + + [Parameter(Mandatory)] + [string]$VMName, + + [Parameter(Mandatory)] + [string]$VhdPath, + + [Parameter(Mandatory)] + [string[]]$SwitchName, + + [Parameter()] + [string[]]$MACAddress + ) + + Import-DscResource -module xHyper-V + + Node $NodeName + { + # Install HyperV features, if not installed - Server SKU only + WindowsFeature HyperV + { + Ensure = 'Present' + Name = 'Hyper-V' + } + + WindowsFeature HyperVPowerShell + { + Ensure = 'Present' + Name = 'Hyper-V-PowerShell' + } + + # Dynamically build the 'DependsOn' array for the 'xVMHyperV' feature + # based on the number of virtual switches specified + $xVMHyperVDependsOn = @('[WindowsFeature]HyperV','[WindowsFeature]HyperVPowerShell') + + # Create each virtual switch + foreach ($vmSwitch in $SwitchName) + { + # Remove spaces and hyphens from the identifier + $vmSwitchName = $vmSwitch -replace ' ','' -replace '-','' + # Add the virtual switch dependency + $xVMHyperVDependsOn += "[xVMHyperV]$vmSwitchName" + + xVMSwitch $vmSwitchName + { + Ensure = 'Present' + Name = $vmSwitch + Type = 'Internal' + DependsOn = '[WindowsFeature]HyperV','[WindowsFeature]HyperVPowerShell' + } + } + + # Ensures a VM with all the properties + xVMHyperV $VMName + { + Ensure = 'Present' + Name = $VMName + VhdPath = $VhdPath + SwitchName = $SwitchName + MACAddress = $MACAddress + # Use the dynamically created dependency list/array + DependsOn = $xVMHyperVDependsOn + } + } +} + +Sample_xVMHyperV_MultipleNICs -VMName 'MultiNICVM' -VhdPath 'C:\VMs\MultiNICVM.vhdx' -SwitchName 'Switch 1','Switch-2' diff --git a/deployment/dsc/azshcihost/xHyper-V/3.17.0.0/Examples/Sample_xVMHyperV_Simple.ps1 b/deployment/dsc/azshcihost/xHyper-V/3.17.0.0/Examples/Sample_xVMHyperV_Simple.ps1 new file mode 100644 index 0000000..a191a88 --- /dev/null +++ b/deployment/dsc/azshcihost/xHyper-V/3.17.0.0/Examples/Sample_xVMHyperV_Simple.ps1 @@ -0,0 +1,35 @@ +configuration Sample_xVMHyperV_Simple +{ + param + ( + [string[]]$NodeName = 'localhost', + + [Parameter(Mandatory)] + [string]$VMName, + + [Parameter(Mandatory)] + [string]$VhdPath + ) + + Import-DscResource -module xHyper-V + + Node $NodeName + { + # Install HyperV feature, if not installed - Server SKU only + WindowsFeature HyperV + { + Ensure = 'Present' + Name = 'Hyper-V' + } + + # Ensures a VM with default settings + xVMHyperV NewVM + { + Ensure = 'Present' + Name = $VMName + VhdPath = $VhdPath + Generation = 2 + DependsOn = '[WindowsFeature]HyperV' + } + } +} diff --git a/deployment/dsc/azshcihost/xHyper-V/3.17.0.0/Examples/Sample_xVMHyperV_SimpleWithDVDDrive.ps1 b/deployment/dsc/azshcihost/xHyper-V/3.17.0.0/Examples/Sample_xVMHyperV_SimpleWithDVDDrive.ps1 new file mode 100644 index 0000000..2299b79 --- /dev/null +++ b/deployment/dsc/azshcihost/xHyper-V/3.17.0.0/Examples/Sample_xVMHyperV_SimpleWithDVDDrive.ps1 @@ -0,0 +1,48 @@ +configuration Sample_xVMHyperV_SimpleWithDvdDrive +{ + param + ( + [string[]]$NodeName = 'localhost', + + [Parameter(Mandatory)] + [string]$VMName, + + [Parameter(Mandatory)] + [string]$VhdPath, + + [string]$ISOPath + ) + + Import-DscResource -module xHyper-V + + Node $NodeName + { + # Install HyperV feature, if not installed - Server SKU only + WindowsFeature HyperV + { + Ensure = 'Present' + Name = 'Hyper-V' + } + + # Ensures a VM with default settings + xVMHyperV NewVM + { + Ensure = 'Present' + Name = $VMName + VhdPath = $VhdPath + Generation = $VhdPath.Split('.')[-1] + DependsOn = '[WindowsFeature]HyperV' + } + + # Adds DVD Drive with ISO + xVMDvdDrive NewVMDvdDriveISO + { + Ensure = 'Present' + Name = $VMName + ControllerNumber = 0 + ControllerLocation = 0 + Path = $ISOPath + DependsOn = '[xVMHyperV]NewVM' + } + } +} diff --git a/deployment/dsc/azshcihost/xHyper-V/3.17.0.0/Examples/Sample_xVMHyperV_SimpleWithNestedVirtualization.ps1 b/deployment/dsc/azshcihost/xHyper-V/3.17.0.0/Examples/Sample_xVMHyperV_SimpleWithNestedVirtualization.ps1 new file mode 100644 index 0000000..6837639 --- /dev/null +++ b/deployment/dsc/azshcihost/xHyper-V/3.17.0.0/Examples/Sample_xVMHyperV_SimpleWithNestedVirtualization.ps1 @@ -0,0 +1,54 @@ +configuration Sample_xVMHyperV_SimpleWithNestedVirtualization +{ + param + ( + [Parameter()] + [string[]] + $NodeName = 'localhost', + + [Parameter(Mandatory = $true)] + [string] + $VMName, + + [Parameter(Mandatory = $true)] + [string] + $VhdPath, + + [Parameter(Mandatory = $true)] + [Uint64] + $Memory + ) + + Import-DscResource -module xHyper-V + + Node $NodeName + { + # Install HyperV feature, if not installed - Server SKU only + WindowsFeature HyperV + { + Ensure = 'Present' + Name = 'Hyper-V' + } + + # Ensures a VM with default settings + xVMHyperV NewVM + { + Ensure = 'Present' + Name = $VMName + VhdPath = $VhdPath + Generation = 2 + StartupMemory = $Memory + MinimumMemory = $Memory + MaximumMemory = $Memory + DependsOn = '[WindowsFeature]HyperV' + } + + # Set the VM options + xVMProcessor NestedVirtualization + { + VMName = $VMName + ExposeVirtualizationExtensions = $true + DependsOn = '[xVMHyperV]NewVM' + } + } +} diff --git a/deployment/dsc/azshcihost/xHyper-V/3.17.0.0/Examples/Sample_xVMNetworkAdapter_ManagementOS.ps1 b/deployment/dsc/azshcihost/xHyper-V/3.17.0.0/Examples/Sample_xVMNetworkAdapter_ManagementOS.ps1 new file mode 100644 index 0000000..7be0ae5 --- /dev/null +++ b/deployment/dsc/azshcihost/xHyper-V/3.17.0.0/Examples/Sample_xVMNetworkAdapter_ManagementOS.ps1 @@ -0,0 +1,13 @@ +Configuration HostOSAdapter +{ + Import-DscResource -ModuleName xHyper-V -Name xVMNetworkAdapter + Import-DscResource -ModuleName PSDesiredStateConfiguration + + xVMNetworkAdapter HostOSAdapter { + Id = 'Management-NIC' + Name = 'Management-NIC' + SwitchName = 'SETSwitch' + VMName = 'ManagementOS' + Ensure = 'Present' + } +} diff --git a/deployment/dsc/azshcihost/xHyper-V/3.17.0.0/Examples/Sample_xVMNetworkAdapter_MultipleManagementOS.ps1 b/deployment/dsc/azshcihost/xHyper-V/3.17.0.0/Examples/Sample_xVMNetworkAdapter_MultipleManagementOS.ps1 new file mode 100644 index 0000000..a0cd100 --- /dev/null +++ b/deployment/dsc/azshcihost/xHyper-V/3.17.0.0/Examples/Sample_xVMNetworkAdapter_MultipleManagementOS.ps1 @@ -0,0 +1,21 @@ +Configuration HostOSAdapter +{ + Import-DscResource -ModuleName xHyper-V -Name xVMNetworkAdapter + Import-DscResource -ModuleName PSDesiredStateConfiguration + + xVMNetworkAdapter ManagementAdapter { + Id = 'Management-NIC' + Name = 'Management-NIC' + SwitchName = 'SETSwitch' + VMName = 'ManagementOS' + Ensure = 'Present' + } + + xVMNetworkAdapter ClusterAdapter { + Id = 'Cluster-NIC' + Name = 'Cluster-NIC' + SwitchName = 'SETSwitch' + VMName = 'ManagementOS' + Ensure = 'Present' + } +} diff --git a/deployment/dsc/azshcihost/xHyper-V/3.17.0.0/Examples/Sample_xVMNetworkAdapter_MultipleVM.ps1 b/deployment/dsc/azshcihost/xHyper-V/3.17.0.0/Examples/Sample_xVMNetworkAdapter_MultipleVM.ps1 new file mode 100644 index 0000000..56d0a55 --- /dev/null +++ b/deployment/dsc/azshcihost/xHyper-V/3.17.0.0/Examples/Sample_xVMNetworkAdapter_MultipleVM.ps1 @@ -0,0 +1,29 @@ +Configuration VMAdapter +{ + Import-DscResource -ModuleName xHyper-V -Name xVMNetworkAdapter + Import-DscResource -ModuleName PSDesiredStateConfiguration + + xVMNetworkAdapter MyVM01NIC { + Id = 'MyVM01-NIC' + Name = 'MyVM01-NIC' + SwitchName = 'SETSwitch' + VMName = 'MyVM01' + Ensure = 'Present' + } + + xVMNetworkAdapter MyVM02NIC { + Id = 'MyVM02-NIC' + Name = 'NetAdapter' + SwitchName = 'SETSwitch' + VMName = 'MyVM02' + Ensure = 'Present' + } + + xVMNetworkAdapter MyVM03NIC { + Id = 'MyVM03-NIC' + Name = 'NetAdapter' + SwitchName = 'SETSwitch' + VMName = 'MyVM03' + Ensure = 'Present' + } +} diff --git a/deployment/dsc/azshcihost/xHyper-V/3.17.0.0/Examples/Sample_xVMNetworkAdapter_MultipleVMMACAddress.ps1 b/deployment/dsc/azshcihost/xHyper-V/3.17.0.0/Examples/Sample_xVMNetworkAdapter_MultipleVMMACAddress.ps1 new file mode 100644 index 0000000..80f0ead --- /dev/null +++ b/deployment/dsc/azshcihost/xHyper-V/3.17.0.0/Examples/Sample_xVMNetworkAdapter_MultipleVMMACAddress.ps1 @@ -0,0 +1,23 @@ +Configuration VMAdapter +{ + Import-DscResource -ModuleName xHyper-V -Name xVMNetworkAdapter + Import-DscResource -ModuleName PSDesiredStateConfiguration + + xVMNetworkAdapter MyVM01NIC { + Id = 'MyVM01-NIC' + Name = 'MyVM01-NIC' + SwitchName = 'SETSwitch' + MacAddress = '001523be0c' + VMName = 'MyVM01' + Ensure = 'Present' + } + + xVMNetworkAdapter MyVM02NIC { + Id = 'MyVM02-NIC' + Name = 'MyVM02-NIC' + SwitchName = 'SETSwitch' + MacAddress = '001523be0d' + VMName = 'MyVM02' + Ensure = 'Present' + } +} diff --git a/deployment/dsc/azshcihost/xHyper-V/3.17.0.0/Examples/Sample_xVMNetworkAdapter_VMStaticNetworkSettings.ps1 b/deployment/dsc/azshcihost/xHyper-V/3.17.0.0/Examples/Sample_xVMNetworkAdapter_VMStaticNetworkSettings.ps1 new file mode 100644 index 0000000..2d192bc --- /dev/null +++ b/deployment/dsc/azshcihost/xHyper-V/3.17.0.0/Examples/Sample_xVMNetworkAdapter_VMStaticNetworkSettings.ps1 @@ -0,0 +1,20 @@ +Configuration VMAdapter +{ + Import-DscResource -ModuleName xHyper-V -Name xVMNetworkAdapter + Import-DscResource -ModuleName PSDesiredStateConfiguration + + xVMNetworkAdapter MyVM01NIC { + Id = 'MyVM01-NIC' + Name = 'MyVM01-NIC' + SwitchName = 'SETSwitch' + MacAddress = '001523be0c' + VMName = 'MyVM01' + NetworkSetting = xNetworkSettings { + IpAddress = "192.168.0.100" + Subnet = "255.255.255.255" + DefaultGateway = "192.168.0.1" + DnsServer = "192.168.0.1" + } + Ensure = 'Present' + } +} diff --git a/deployment/dsc/azshcihost/xHyper-V/3.17.0.0/Examples/Sample_xVMNetworkAdapter_VMVlanTagging.ps1 b/deployment/dsc/azshcihost/xHyper-V/3.17.0.0/Examples/Sample_xVMNetworkAdapter_VMVlanTagging.ps1 new file mode 100644 index 0000000..0c17245 --- /dev/null +++ b/deployment/dsc/azshcihost/xHyper-V/3.17.0.0/Examples/Sample_xVMNetworkAdapter_VMVlanTagging.ps1 @@ -0,0 +1,15 @@ +Configuration VMAdapter +{ + Import-DscResource -ModuleName xHyper-V -Name xVMNetworkAdapter + Import-DscResource -ModuleName PSDesiredStateConfiguration + + xVMNetworkAdapter MyVM01NIC { + Id = 'MyVM01-NIC' + Name = 'MyVM01-NIC' + SwitchName = 'SETSwitch' + MacAddress = '001523be0c' + VMName = 'MyVM01' + VlanId = '1' + Ensure = 'Present' + } +} diff --git a/deployment/dsc/azshcihost/xHyper-V/3.17.0.0/Examples/Sample_xVMScsiController_AddControllers.ps1 b/deployment/dsc/azshcihost/xHyper-V/3.17.0.0/Examples/Sample_xVMScsiController_AddControllers.ps1 new file mode 100644 index 0000000..9ef5a72 --- /dev/null +++ b/deployment/dsc/azshcihost/xHyper-V/3.17.0.0/Examples/Sample_xVMScsiController_AddControllers.ps1 @@ -0,0 +1,68 @@ +configuration Sample_xVMScsiController +{ + param + ( + [Parameter()] + [System.String[]] + $NodeName = 'localhost', + + [Parameter(Mandatory = $true)] + [System.String] + $VMName, + + [Parameter(Mandatory = $true)] + [System.String] + $VhdPath + ) + + Import-DscResource -ModuleName 'xHyper-V' + Import-DscResource -ModuleName 'PSDesiredStateConfiguration' + + Node $NodeName + { + $diskNameOS = "$VMName-OS.vhdx" + + # Install HyperV feature, if not installed - Server SKU only + WindowsFeature HyperV + { + Ensure = 'Present' + Name = 'Hyper-V' + } + + # Create the VHD for the OS + xVHD DiskOS + { + Ensure = 'Present' + Name = $diskNameOS + Path = $VhdPath + Generation = 'vhdx' + MaximumSizeBytes = 20GB + DependsOn = '[WindowsFeature]HyperV' + } + + # Create the VM + xVMHyperV NewVM + { + Ensure = 'Present' + Name = $VMName + VhdPath = Join-Path -Path $VhdPath -ChildPath $diskNameOS + Generation = 2 + DependsOn = '[xVHD]DiskOS' + } + + # Add and additional SCSI controller + xVMScsiController Controller + { + Ensure = 'Present' + VMName = $VMName + ControllerNumber = 1 + DependsOn = '[xVMHyperV]NewVM' + } + + } +} + +$mofPath = "C:\temp\Sample_xVMScsiController" + +Sample_xVMScsiController -VMName "test1" -VhdPath "C:\temp\Tests" -OutputPath $mofPath +Start-DscConfiguration -Path $mofPath -Verbose -Wait -Force diff --git a/deployment/dsc/azshcihost/xHyper-V/3.17.0.0/Examples/Sample_xVMSwitch_External.ps1 b/deployment/dsc/azshcihost/xHyper-V/3.17.0.0/Examples/Sample_xVMSwitch_External.ps1 new file mode 100644 index 0000000..48e2e94 --- /dev/null +++ b/deployment/dsc/azshcihost/xHyper-V/3.17.0.0/Examples/Sample_xVMSwitch_External.ps1 @@ -0,0 +1,35 @@ +configuration Sample_xVMSwitch_External +{ + param + ( + [string[]]$NodeName = 'localhost', + + [Parameter(Mandatory)] + [string]$SwitchName, + + [Parameter(Mandatory)] + [string]$NetAdapterName + ) + + Import-DscResource -module xHyper-V + + Node $NodeName + { + # Install HyperV feature, if not installed - Server SKU only + WindowsFeature HyperV + { + Ensure = 'Present' + Name = 'Hyper-V' + } + + # Ensures a VM with default settings + xVMSwitch ExternalSwitch + { + Ensure = 'Present' + Name = $SwitchName + Type = 'External' + NetAdapterName = $NetAdapterName + DependsOn = '[WindowsFeature]HyperV' + } + } +} diff --git a/deployment/dsc/azshcihost/xHyper-V/3.17.0.0/Examples/Sample_xVMSwitch_ExternalSET.ps1 b/deployment/dsc/azshcihost/xHyper-V/3.17.0.0/Examples/Sample_xVMSwitch_ExternalSET.ps1 new file mode 100644 index 0000000..24c888d --- /dev/null +++ b/deployment/dsc/azshcihost/xHyper-V/3.17.0.0/Examples/Sample_xVMSwitch_ExternalSET.ps1 @@ -0,0 +1,47 @@ +Configuration Sample_xVMSwitch_External +{ + param + ( + [Parameter()] + [string[]] + $NodeName = 'localhost', + + [Parameter(Mandatory = $true)] + [string] + $SwitchName, + + [Parameter(Mandatory = $true)] + [string[]] + $NetAdapterNames + ) + + Import-DscResource -module xHyper-V + + Node $NodeName + { + # Install HyperV feature, if not installed - Server SKU only + WindowsFeature HyperV + { + Ensure = 'Present' + Name = 'Hyper-V' + } + + WindowsFeature HyperVTools + { + Ensure = 'Present' + Name = 'RSAT-Hyper-V-Tools' + DependsOn = '[WindowsFeature]HyperV' + } + + # Ensures a VM with default settings + xVMSwitch ExternalSwitch + { + Ensure = 'Present' + Name = $SwitchName + Type = 'External' + NetAdapterName = $NetAdapterNames + EnableEmbeddedTeaming = $true + DependsOn = '[WindowsFeature]HyperVTools' + } + } +} diff --git a/deployment/dsc/azshcihost/xHyper-V/3.17.0.0/Examples/Sample_xVMSwitch_ExternalSET_LBMode.ps1 b/deployment/dsc/azshcihost/xHyper-V/3.17.0.0/Examples/Sample_xVMSwitch_ExternalSET_LBMode.ps1 new file mode 100644 index 0000000..102ea56 --- /dev/null +++ b/deployment/dsc/azshcihost/xHyper-V/3.17.0.0/Examples/Sample_xVMSwitch_ExternalSET_LBMode.ps1 @@ -0,0 +1,48 @@ +Configuration Sample_xVMSwitch_External +{ + param + ( + [Parameter()] + [string[]] + $NodeName = 'localhost', + + [Parameter(Mandatory = $true)] + [string] + $SwitchName, + + [Parameter(Mandatory = $true)] + [string[]] + $NetAdapterNames + ) + + Import-DscResource -module xHyper-V + + Node $NodeName + { + # Install HyperV feature, if not installed - Server SKU only + WindowsFeature HyperV + { + Ensure = 'Present' + Name = 'Hyper-V' + } + + WindowsFeature HyperVTools + { + Ensure = 'Present' + Name = 'RSAT-Hyper-V-Tools' + DependsOn = '[WindowsFeature]HyperV' + } + + # Ensures a VM with Load Balancing Algorithm "Hyper-V Port" + xVMSwitch ExternalSwitch + { + Ensure = 'Present' + Name = $SwitchName + Type = 'External' + NetAdapterName = $NetAdapterNames + EnableEmbeddedTeaming = $true + LoadBalancingAlgorithm = 'HyperVPort' + DependsOn = '[WindowsFeature]HyperVTools' + } + } +} diff --git a/deployment/dsc/azshcihost/xHyper-V/3.17.0.0/Examples/Sample_xVMSwitch_Internal.ps1 b/deployment/dsc/azshcihost/xHyper-V/3.17.0.0/Examples/Sample_xVMSwitch_Internal.ps1 new file mode 100644 index 0000000..cc1c189 --- /dev/null +++ b/deployment/dsc/azshcihost/xHyper-V/3.17.0.0/Examples/Sample_xVMSwitch_Internal.ps1 @@ -0,0 +1,31 @@ +configuration Sample_xVMSwitch_Internal +{ + param + ( + [string[]]$NodeName = 'localhost', + + [Parameter(Mandatory)] + [string]$SwitchName + ) + + Import-DscResource -module xHyper-V + + Node $NodeName + { + # Install HyperV feature, if not installed - Server SKU only + WindowsFeature HyperV + { + Ensure = 'Present' + Name = 'Hyper-V' + } + + # Ensures a VM with default settings + xVMSwitch InternalSwitch + { + Ensure = 'Present' + Name = $SwitchName + Type = 'Internal' + DependsOn = '[WindowsFeature]HyperV' + } + } +} diff --git a/deployment/dsc/azshcihost/xHyper-V/3.17.0.0/Examples/Sample_xVhdFileExamples.ps1 b/deployment/dsc/azshcihost/xHyper-V/3.17.0.0/Examples/Sample_xVhdFileExamples.ps1 new file mode 100644 index 0000000..93052c3 --- /dev/null +++ b/deployment/dsc/azshcihost/xHyper-V/3.17.0.0/Examples/Sample_xVhdFileExamples.ps1 @@ -0,0 +1,171 @@ +# sample values used for testing. +$sampleVhdPath = "C:\test_vhds\RenameComputer.Vhd" + +# sample text file that you want to copy in the VHD. +$sampletxt = "C:\sample.txt" + +# This path is the relative path to mounted drive letter. +$sampleVhdDestinationPath = "xvhdFileExample\CopiedFile" + +# A local folder that you want to copy in to the VHD. +$samplefolder = "c:\SampleFolder" + +Configuration xVhdD_CopyFileOrFolder +{ + + Param( + [Parameter(Mandatory=$true, Position=0)] + [validatescript({Test-Path $_})] + $vhdPath, + [Parameter(Mandatory=$true)] + [validatescript({Test-Path $_})] + $itemToCopy, + [Parameter(Mandatory=$true)] + $relativeDestinationPath + ) + + Import-DscResource -moduleName xHyper-V + + xVhdFile FileCopy + { + VhdPath = $vhdPath + FileDirectory = MSFT_xFileDirectory { + SourcePath = $itemToCopy + DestinationPath = $relativeDestinationPath + } + + } + +} + +# Copy File/Folder example +xVhdD_CopyFileOrFolder -vhdPath $sampleVhdPath -itemToCopy $sampletxt -relativeDestinationPath $sampleVhdDestinationPath +Start-DscConfiguration -ComputerName localhost -Path $pwd\xVhdD_CopyFileOrFolder\ -Wait -Verbose + +xVhdD_CopyFileOrFolder -vhdPath $sampleVhdPath -itemToCopy $samplefolder -relativeDestinationPath $sampleVhdDestinationPath +Start-DscConfiguration -ComputerName localhost -Path $pwd\xVhdD_CopyFileOrFolder\ -Wait -Verbose + +Configuration RemoveFileOrFolderFromVHD +{ + param( + [Parameter(Mandatory=$true, Position=0)] + [validatescript({Test-Path $_})] + $vhdPath, + [Parameter(Mandatory=$true)] + $relativeDestinationPath, + $Ensure = 'Absent' + ) + Import-DscResource -moduleName xHyper-V + xVhdFile RemoveFile + { + VhdPath = $vhdPath + FileDirectory = MSFT_xFileDirectory { + DestinationPath = $relativeDestinationPath + Ensure = $Ensure + } + + } + +} + +RemoveFileOrFolderFromVHD -vhdPath $sampleVhdPath -relativeDestinationPath $sampleVhdDestinationPath +Start-DscConfiguration -ComputerName localhost -Path $pwd\RemoveFileOrFolderFromVHD\ -Wait -Verbose + +Configuration ChangeAttribute +{ + param( + [Parameter(Mandatory=$true, Position=0)] + [validatescript({Test-Path $_})] + $vhdPath, + [Parameter(Mandatory=$true)] + $relativeDestinationPath, + [ValidateSet ("Archive", "Hidden", "ReadOnly", "System" )] $attribute + ) + + Import-DscResource -moduleName xHyper-V + xVhdFile Change-Attribute + { + VhdPath = $vhdPath + FileDirectory = MSFT_xFileDirectory { + DestinationPath = $relativeDestinationPath + Attributes = $attribute + } + + } +} + +ChangeAttribute -vhdPath $sampleVhdPath -relativeDestinationPath $sampleVhdDestinationPath -attribute 'ReadOnly' +Start-DscConfiguration -ComputerName localhost -Path $pwd\RemoveFileOrFolderFromVHD\ -Wait -Verbose + +# End to end sample for x-Hyper-v +Configuration Sample_EndToEndXHyperV_RunningVM +{ + + param + ( + [Parameter(Mandatory)] + $vhdPath, + [Parameter(Mandatory)] + $name, + [Parameter(Mandatory)] + [validatescript({Test-Path $_})] + $unattendedFilePathToCopy + ) + + Import-DscResource -module xHyper-V + + # Create a switch to be used by the VM + xVMSwitch switch + { + Name = "Test-Switch" + Ensure = "Present" + Type = "Internal" + } + + # Create new VHD file. + xVHD NewVHD1 + { + + Ensure = "Present" + Name = $name + Path = (Split-Path $vhdPath) + Generation = "vhd" + ParentPath = $vhdPath + + } + + # Customize VHD by copying a folders/files to the VHD before a VM can be created + # Example below shows copying unattended.xml before a VM can be created + xVhdFile CopyUnattendxml + { + VhdPath = $vhdPath + FileDirectory = MSFT_xFileDirectory { + SourcePath = $unattendedFilePathToCopy + DestinationPath = "unattended.xml" + } + + } + + # create the testVM out of the vhd. + xVMHyperV testvm + { + Name = "$($name)_vm" + SwitchName = "Test-Switch" + VhdPath = Join-path (Split-Path $vhdPath) "$name.vhd" + ProcessorCount = 2 + MaximumMemory = 1GB + MinimumMemory = 512MB + RestartIfNeeded = "TRUE" + DependsOn = "[xVHD]NewVHD1","[xVMSwitch]switch","[xVhdFile]CopyUnattendxml" + State = "Running" + + } + +} + +# Create a mof file. +Sample_EndToEndXHyperV_RunningVM -vhdPath $sampleVhdPath -name TestMachine -unattendedFilePathToCopy C:\temp\unattended.xml + +# Run the configuration on localhost. +Start-DscConfiguration -Path $pwd\Sample_EndToEndXHyperV_RunningVM -ComputerName localhost -Verbose -Wait + diff --git a/deployment/dsc/azshcihost/xHyper-V/3.17.0.0/LICENSE b/deployment/dsc/azshcihost/xHyper-V/3.17.0.0/LICENSE new file mode 100644 index 0000000..567fd6a --- /dev/null +++ b/deployment/dsc/azshcihost/xHyper-V/3.17.0.0/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2015 Microsoft Corporation. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. \ No newline at end of file diff --git a/deployment/dsc/azshcihost/xHyper-V/3.17.0.0/Misc/VHDResourceGenerator.ps1 b/deployment/dsc/azshcihost/xHyper-V/3.17.0.0/Misc/VHDResourceGenerator.ps1 new file mode 100644 index 0000000..1a00958 --- /dev/null +++ b/deployment/dsc/azshcihost/xHyper-V/3.17.0.0/Misc/VHDResourceGenerator.ps1 @@ -0,0 +1,13 @@ + $name = New-DscResourceProperty -Name Name -Type String -Attribute Key -Description "Name of the VHD File" + $path = New-DscResourceProperty -Name Path -Type String -Attribute Key -Description "Folder where the VHD will be created" + $parentPath = New-DscResourceProperty -Name ParentPath -Type String -Attribute Write -Description "Parent VHD file path, for differencing disk" + $generation = New-DscResourceProperty -Name Generation -Type String -Attribute Write -ValidateSet "Vhd","Vhdx" -Description "Virtual disk format - Vhd or Vhdx" + $ensure = New-DscResourceProperty -Name Ensure -Type String -Attribute Write -ValidateSet "Present","Absent" -Description "Should the VHD be created or deleted" + $MaximumSizeBytes = New-DscResourceProperty -Name MaximumSizeBytes -Type Uint32 -Attribute Write -Description "Maximum size of Vhd to be created" + + $id = New-DscResourceProperty -Name ID -Type String -Attribute Read -Description "Virtual Disk Identifier" + $type = New-DscResourceProperty -Name Type -Type String -Attribute Read -Description "Type of Vhd - Dynamic, Fixed, Differencing" + $FileSizeBytes = New-DscResourceProperty -Name FileSizeBytes -Type Uint32 -Attribute Read -Description "Current size of the VHD" + $IsAttached = New-DscResourceProperty -Name IsAttached -Type Boolean -Attribute Read -Description "Is the VHD attached to a VM or not" + + New-DscResource -Name MSFT_xVHD -Properties $name,$path,$parentPath,$generation,$ensure,$id,$type,$MaximumSizeBytes,$FileSizeBytes,$IsAttached -Path . -ClassVersion 1.0.0 -FriendlyName xVHD diff --git a/deployment/dsc/azshcihost/xHyper-V/3.17.0.0/Misc/VMHardDiskDriveGenerator.ps1 b/deployment/dsc/azshcihost/xHyper-V/3.17.0.0/Misc/VMHardDiskDriveGenerator.ps1 new file mode 100644 index 0000000..37f2a6b --- /dev/null +++ b/deployment/dsc/azshcihost/xHyper-V/3.17.0.0/Misc/VMHardDiskDriveGenerator.ps1 @@ -0,0 +1,8 @@ +New-xDscResource -Name MSFT_xVMHardDiskDrive -Path . -ClassVersion 1.0.0 -FriendlyName xVMHardDiskDrive -Property $( + New-xDscResourceProperty -Name VMName -Type String -Attribute Key -Description "Specifies the name of the virtual machine whose hard disk drive is to be manipulated" + New-xDscResourceProperty -Name Path -Type String -Attribute Key -Description "Specifies the full path to the location of the VHD that represents the hard disk drive" + New-xDscResourceProperty -Name ControllerType -Type String -Attribute Write -ValidateSet "IDE","SCSI" -Description "Specifies the controller type - IDE/SCSI where the disk is attached" + New-xDscResourceProperty -Name ControllerNumber -Type Uint32 -Attribute Write -ValidateSet 0,1,2,3 -Description "Specifies the number of the controller where the disk is attached" + New-xDscResourceProperty -Name ControllerLocation -Type Uint32 -Attribute Write -Description "Specifies the number of the location on the controller where the disk is attached" + New-xDscResourceProperty -Name Ensure -Type String -Attribute Write -ValidateSet "Present","Absent" -Description "Specifies if the hard disk drive must be present or absent" +) diff --git a/deployment/dsc/azshcihost/xHyper-V/3.17.0.0/Misc/VMResourceGenerator.ps1 b/deployment/dsc/azshcihost/xHyper-V/3.17.0.0/Misc/VMResourceGenerator.ps1 new file mode 100644 index 0000000..41d2451 --- /dev/null +++ b/deployment/dsc/azshcihost/xHyper-V/3.17.0.0/Misc/VMResourceGenerator.ps1 @@ -0,0 +1,26 @@ + $name = New-xDscResourceProperty -Name Name -Type String -Attribute Key -Description "Name of the VM" + $vhdPath = New-xDscResourceProperty -Name VhdPath -Type String -Attribute Required -Description "VHD associated with the VM" + $switchName = New-xDscResourceProperty -Name SwitchName -Type String -Attribute Write -Description "Virtual switch associated with the VM" + $path = New-xDscResourceProperty -Name Path -Type String -Attribute Write -Description "Folder where the VM data will be stored" + $generation = New-xDscResourceProperty -Name Generation -Type String -Attribute Write -ValidateSet "Vhd","Vhdx" -Description "Associated Virtual disk format - Vhd or Vhdx" + $ensure = New-xDscResourceProperty -Name Ensure -Type String -Attribute Write -ValidateSet "Present","Absent" -Description "Should the VM be created or deleted" + $startupMem = New-xDscResourceProperty -Name StartupMemory -Type Uint64 -Attribute Write -Description "Startup RAM for the VM." + $minMem = New-xDscResourceProperty -Name MinimumMemory -Type Uint64 -Attribute Write -Description "Minimum RAM for the VM. This enables dynamic memory." + $maxMem = New-xDscResourceProperty -Name MaximumMemory -Type Uint64 -Attribute Write -Description "Maximum RAM for the VM. This enable dynamic memory." + $macAddress = New-xDscResourceProperty -Name MACAddress -Type String -Attribute Write -Description "MAC address of the VM." + $waitForIP = New-xDscResourceProperty -Name WaitForIP -Type Boolean -Attribute Write -Description "Waits for VM to get valid IP address." + $state = New-xDscResourceProperty -Name State -Type String -Attribute Write -ValidateSet "Running","Paused","Off" -Description "State of the VM." + $notes = New-xDscResourceProperty -Name Notes -Type String -Attribute Write -Description "Notes about the VM." + $procCount = New-xDscResourceProperty -Name ProcessorCount -Type Uint32 -Attribute Write -Description "Processor count for the VM" + $restartIfNeeded = New-xDscResourceProperty -Name RestartIfNeeded -Type Boolean -Attribute Write -Description "If specified, shutsdown and restarts the VM if needed for resource change" + + $id = New-xDscResourceProperty -Name ID -Type String -Attribute Read -Description "VM unique ID" + $status = New-xDscResourceProperty -Name Status -Type String -Attribute Read -Description "Status of the VM" + $CPUUsage = New-xDscResourceProperty -Name CPUUsage -Type Uint32 -Attribute Read -Description "CPU Usage of the VM" + $memAssigned = New-xDscResourceProperty -Name MemoryAssigned -Type Uint64 -Attribute Read -Description "Memory assigned to the VM" + $uptime = New-xDscResourceProperty -Name Uptime -Type String -Attribute Read -Description "Uptime of the VM" + $creationTime = New-xDscResourceProperty -Name CreationTime -Type DateTime -Attribute Read -Description "Creation time of the VM" + $hasDynamicMemory = New-xDscResourceProperty -Name HasDynamicMemory -Type Boolean -Attribute Read -Description "Does VM has dynamic memory enabled" + $networkAdapters = New-xDscResourceProperty -Name NetworkAdapters -Type String[] -Attribute Read -Description "Network adapters of the VM" + + New-xDscResource -Name MSFT_xVMHyperV -Property @($name,$vhdPath,$switchName,$state,$path,$generation,$startupMem,$minMem,$maxMem,$macAddress,$procCount,$waitForIP,$restartIfNeeded,$ensure, $notes,$id,$status,$CPUUsage,$memAssigned,$uptime,$creationTime,$hasDynamicMemory,$networkAdapters) -Path . -ClassVersion 1.0.0 -FriendlyName xVMHyperV diff --git a/deployment/dsc/azshcihost/xHyper-V/3.17.0.0/Misc/VMScsiControllerGenerator.ps1 b/deployment/dsc/azshcihost/xHyper-V/3.17.0.0/Misc/VMScsiControllerGenerator.ps1 new file mode 100644 index 0000000..4dd414a --- /dev/null +++ b/deployment/dsc/azshcihost/xHyper-V/3.17.0.0/Misc/VMScsiControllerGenerator.ps1 @@ -0,0 +1,5 @@ +New-xDscResource -Name MSFT_xVMScsiController -Path . -ClassVersion 1.0.0 -FriendlyName xVMScsiController -Property $( + New-xDscResourceProperty -Name VMName -Type String -Attribute Key -Description "Specifies the name of the virtual machine whose SCSI controller status is to be controlled" + New-xDscResourceProperty -Name ControllerNumber -Type Uint32 -Attribute Key -ValidateSet 0,1,2,3 -Description "Specifies the number of the SCSI controller whose status is to be controlled" + New-xDscResourceProperty -Name Ensure -Type String -Attribute Write -ValidateSet "Present","Absent" -Description "Specifies if the SCSI controller should exist or not" +) diff --git a/deployment/dsc/azshcihost/xHyper-V/3.17.0.0/Misc/VMSwitchGenerator.ps1 b/deployment/dsc/azshcihost/xHyper-V/3.17.0.0/Misc/VMSwitchGenerator.ps1 new file mode 100644 index 0000000..4d27ea9 --- /dev/null +++ b/deployment/dsc/azshcihost/xHyper-V/3.17.0.0/Misc/VMSwitchGenerator.ps1 @@ -0,0 +1,8 @@ + $name = New-DscResourceProperty -Name Name -Type String -Attribute Key -Description "Name of the VM Switch" + $type = New-DscResourceProperty -Name Type -Type String -Attribute Key -ValidateSet "Internal","Private" -Description "Type of switch" + $netAdapter = New-DscResourceProperty -Name NetAdapterName -Type String -Attribute Write -Description "Network adapter name for external switch type" + $allowManagementOS = New-DscResourceProperty -Name AllowManagementOS -Type Boolean -Attribute Write -Description "Specify is the VM host has access to the physical NIC" + $ensure = New-DscResourceProperty -Name Ensure -Type String -Attribute Write -ValidateSet "Present","Absent" -Description "Whether switch should be present or absent" + $id = New-DscResourceProperty -Name Id -Type String -Attribute Read -Description "Unique ID for the switch" + $netDescription = New-DscResourceProperty -Name NetAdapterInterfaceDescription -Type String -Attribute Read -Description "Description of the network interface" + New-DscResource -Name MSFT_xVMSwitch -Path . -Properties $name,$type,$netAdapter,$allowManagementOS,$ensure,$id,$netDescription -ClassVersion 1.0.0.0 -FriendlyName xVMSwitch diff --git a/deployment/dsc/azshcihost/xHyper-V/3.17.0.0/README.md b/deployment/dsc/azshcihost/xHyper-V/3.17.0.0/README.md new file mode 100644 index 0000000..ce8f76f --- /dev/null +++ b/deployment/dsc/azshcihost/xHyper-V/3.17.0.0/README.md @@ -0,0 +1,523 @@ +# xHyper-V + +The **xHyper-V** module contains DSC resources for deployment and configuration of + Hyper-V hosts, virtual machines and related resources. + +This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). +For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) + or contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any + additional questions or comments. + +## Branches + +### master + +[![Build status](https://ci.appveyor.com/api/projects/status/tsdbv0hgrxvmbo5y/branch/master?svg=true)](https://ci.appveyor.com/project/PowerShell/xhyper-v/branch/master) +[![codecov](https://codecov.io/gh/PowerShell/xHyper-V/branch/master/graph/badge.svg)](https://codecov.io/gh/PowerShell/xHyper-V/branch/master) + +This is the branch containing the latest release - no contributions should be +made directly to this branch. + +### dev + +[![Build status](https://ci.appveyor.com/api/projects/status/tsdbv0hgrxvmbo5y/branch/dev?svg=true)](https://ci.appveyor.com/project/PowerShell/xhyper-v/branch/dev) +[![codecov](https://codecov.io/gh/PowerShell/xHyper-V/branch/dev/graph/badge.svg)](https://codecov.io/gh/PowerShell/xHyper-V/branch/dev) + +This is the development branch to which contributions should be proposed by +contributors as pull requests. This development branch will periodically be +merged to the master branch, and be released to [PowerShell Gallery](https://www.powershellgallery.com/). + +## Contributing + +Please check out common DSC Resources [contributing guidelines](https://github.com/PowerShell/DscResource.Kit/blob/master/CONTRIBUTING.md). + +## Change log + +A full list of changes in each version can be found in the [change log](CHANGELOG.md). + +## Resources + +* [**xVHD**](#xvhd) manages VHDs in a Hyper-V host. +* [**xVhdFile**](#xvhdfile) manages files or directories in a VHD. + You can use it to copy files/folders to the VHD, remove files/folders from a VHD, + and change attributes of a file in a VHD (e.g. change a file attribute to + 'ReadOnly' or 'Hidden'). + This resource is particularly useful when bootstrapping DSC Configurations + into a VM. +* [**xVMDvdDrive**](#xvmdvddrive) manages DVD drives attached to a Hyper-V + virtual machine. +* [**xVMHardDiskDrive**](#xvmharddiskdrive) manages VHD(X)s attached to a Hyper-V virtual machine. +* [**xVMHost**](#xvmhost) manages Hyper-V host settings. +* [**xVMHyperV**](#xvmhyperv) manages VMs in a Hyper-V host. +* [**xVMNetworkAdapter**](#xvmnetworkadapter) manages VMNetadapters attached to + a Hyper-V virtual machine or the management OS. +* [**xVMProcessor**](#xvmprocessor) manages Hyper-V virtual machine processor options. +* [**xVMScsiController**](#xvmscsicontroller) manages the SCSI controllers attached to a Hyper-V virtual machine. +* [**xVMSwitch**](#xvmswitch) manages virtual switches in a Hyper-V host. + +### xVHD + +Manages VHDs in a Hyper-V host. + +#### Requirements for xVHD + +* The Hyper-V Role has to be installed on the machine. +* The Hyper-V PowerShell module has to be installed on the machine. + +#### Parameters for xVHD + +* **`[String]` Name** _(Key)_: The desired VHD file name. +* **`[String]` Path** _(Key)_: The desired Path where the VHD will be created. +* **`[String]` ParentPath** _(Write)_: Parent VHD file path, for differencing disk. +* **`[Uint64]` MaximumSizeBytes** _(Write)_: Maximum size of VHD to be created. +* **`[String]` Generation** _(Write)_: Virtual disk format. + The default value is Vhd. { *Vhd* | Vhdx }. +* **`[String]` Type** _(Write)_: Virtual disk type. + The default value is Dynamic. { *Dynamic* | Fixed | Differencing }. +* **`[String]` Ensure** _(Write)_: Ensures that the VHD is Present or Absent. + The default value is Present. { *Present* | Absent }. + +#### Read-Only Properties from Get-TargetResource for xVHD + +* **`[String]` ID** _(Read)_: Virtual Disk Identifier. +* **`[String]` Type** _(Read)_: Type of Vhd - Dynamic, Fixed, Differencing. +* **`[Uint64]` FileSizeBytes** _(Read)_: Current size of the VHD. +* **`[Boolean]` IsAttached** _(Read)_: Is the VHD attached to a VM or not. + +#### Examples xVHD + +* [Create a new VHD](/Examples/Sample_xVHD_NewVHD.ps1) +* [Create a new Fixed VHD](/Examples/Sample_xVHD_FixedVHD.ps1) +* [Create a differencing VHD](/Examples/Sample_xVHD_DiffVHD.ps1) + +### xVhdFile + +Manages files or directories in a VHD. + You can use it to copy files/folders to the VHD, remove files/folders from a VHD, + and change attributes of a file in a VHD (e.g. change a file attribute to + 'ReadOnly' or 'Hidden'). + This resource is particularly useful when bootstrapping DSC Configurations + into a VM. + +#### Requirements for xVhdFile + +* The Hyper-V Role has to be installed on the machine. +* The Hyper-V PowerShell module has to be installed on the machine. + +#### Parameters for xVhdFile + +* **`[String]` VhdPath** _(Key)_: Path to the VHD. +* **`[MSFT_xFileDirectory[]]` FileDirectory** _(Required)_: The FileDirectory objects + to copy to the VHD (as used in the "File" resource). + Please see the Examples section for more details. +* **`[String]` CheckSum** _(Write)_: Indicates the checksum type to use when determining + whether two files are the same. The default value is ModifiedDate. + { *ModifiedDate* | SHA-1 | SHA-256 | SHA-512 }. + +##### MSFT_xFileDirectory Class + +* **`[String]` DestinationPath** _(Required)_: Indicates the location where you want + to ensure the state for a file or directory. +* **`[String]` SourcePath** _(Write)_: Indicates the path from which to copy the + file or folder resource. +* **`[String]` Ensure** _(Write)_: Indicates if the file or directory exists. + Set this property to "Absent" to ensure that the file or directory does not exist. + Set it to "Present" to ensure that the file or directory does exist. + { Present | Absent }. +* **`[String]` Type** _(Write)_: Indicates if the resource being configured is a + directory or a file. Set this property to "Directory" to indicate that the resource + is a directory. Set it to "File" to indicate that the resource is a file. + { File | Directory }. +* **`[Boolean]` Recurse** _(Write)_: Indicates if subdirectories are included. + Set this property to $true to indicate that you want subdirectories to be included. +* **`[Boolean]` Force** _(Write)_: Certain file operations (such as overwriting a + file or deleting a directory that is not empty) will result in an error. Using the + Force property overrides such errors. +* **`[String]` Content** _(Write)_: Specifies the contents of a file, such as a + particular string. +* **`[String[]]` Attributes** _(Write)_: Specifies the desired state of the attributes + for the targeted file or directory. { ReadOnly | Hidden | System | Archive }. + +#### Read-Only Properties from Get-TargetResource for xVhdFile + +None + +#### Examples xVhdFile + +* [Multiple examples](/Examples/Sample_xVhdFileExamples.ps1) + +### xVMDvdDrive + +Manages DVD drives attached to a Hyper-V virtual machine. + +#### Requirements for xVMDvdDrive + +* The Hyper-V Role has to be installed on the machine. +* The Hyper-V PowerShell module has to be installed on the machine. + +#### Parameters for xVMDvdDrive + +* **`[String]` VMName** _(Key)_: Specifies the name of the virtual machine + to which the DVD drive is to be added. +* **`[Uint32]` ControllerNumber** _(Key)_: Specifies the number of the controller + to which the DVD drive is to be added. +* **`[Uint32]` ControllerLocation** _(Key)_: Specifies the number of the location + on the controller at which the DVD drive is to be added. +* **`[String]` Path** _(Write)_: Specifies the full path to the virtual hard disk + file or physical hard disk volume for the added DVD drive. +* **`[String]` Ensure** _(Write)_: Specifies if the DVD Drive should exist or not. + The default value is Present. { *Present* | Absent }. + +#### Read-Only Properties from Get-TargetResource for xVMDvdDrive + +None + +#### Examples xVMDvdDrive + +* [Create a VM, given a VHDX and add a DVD Drives](/Examples/Sample_xVMHyperV_SimpleWithDVDDrive.ps1) + +### xVMHardDiskDrive + +Manages VHD(X)s attached to a Hyper-V virtual machine. +When ControllerNumber or ControllerLocation is not provided, the same logic as + Set-VMHardDiskDrive cmdlet is used. + +#### Requirements for xVMHardDiskDrive + +* The Hyper-V Role has to be installed on the machine. +* The Hyper-V PowerShell module has to be installed on the machine. + +#### Parameters for xVMHardDiskDrive + +* **`[String]` VMName** _(Key)_: Specifies the name of the virtual machine + whose hard disk drive is to be manipulated. +* **`[String]` VhdPath** _(Key)_: Specifies the full path of the VHD file to be + manipulated. +* **`[String]` ControllerType** _(Write)_: Specifies the type of controller to which + the hard disk drive is to be set. The default value is SCSI. { *SCSI* | IDE }. +* **`[Uint32]` ControllerNumber** _(Write)_: Specifies the number of the controller + to which the hard disk drive is to be set. + For IDE: { 0, 1 }, for SCSI: { 0 | 1 | 2 | 3 }. + Defaults to 0. +* **`[Uint32]` ControllerLocation** _(Write)_: Specifies the number of the location + on the controller at which the hard disk drive is to be set. + For IDE: { 0 | 1 }, for SCSI: { 0 .. 63 }. + Defaults to 0. +* **`[String]` Ensure** _(Write)_: Specifies if the hard disk drive should exist or + not. The default value is Present. { *Present* | Absent }. + +#### Read-Only Properties from Get-TargetResource for xVMHardDiskDrive + +None + +#### Examples xVMHardDiskDrive + +* [Create a VM, with an OS drive and an additional data drive](/Examples/Sample_xVMHardDiskDrive_VMWithExtraDisk.ps1) +* [Create a VM, with an OS drive and 4 data drives](/Examples/Sample_xVMHardDiskDrive_VMWith4AdditionalDisks.ps1) + +### xVMHost + +Manages Hyper-V host settings. + +#### Requirements for xVMHost + +* The Hyper-V Role has to be installed on the machine. +* The Hyper-V PowerShell module has to be installed on the machine. + +#### Parameters for xVMHost + +* **`[String]` IsSingleInstance** _(Key)_: Specifies the resource is a single instance, + the value must be 'Yes'. { *Yes* }. +* **`[Boolean]` EnableEnhancedSessionMode** _(Write)_: Indicates whether users + can use enhanced mode when they connect to virtual machines on this server + by using Virtual Machine Connection. +* **`[String]` FibreChannelWwnn** _(Write)_: Specifies the default value of + the World Wide Node Name on the Hyper-V host. +* **`[String]` FibreChannelWwpnMaximum** _(Write)_: Specifies the maximum value + that can be used to generate World Wide Port Names on the Hyper-V host. + Use with the FibreChannelWwpnMinimum parameter to establish a range of WWPNs + that the specified Hyper-V host can assign to virtual Fibre Channel adapters. +* **`[String]` FibreChannelWwpnMinimum** _(Write)_: Specifies the minimum value + that can be used to generate the World Wide Port Names on the Hyper-V host. + Use with the FibreChannelWwpnMaximum parameter to establish a range of WWPNs + that the specified Hyper-V host can assign to virtual Fibre Channel adapters. +* **`[String]` MacAddressMaximum** _(Write)_: Specifies the maximum MAC address + using a valid hexadecimal value. Use with the MacAddressMinimum parameter + to establish a range of MAC addresses that the specified Hyper-V host can assign + to virtual machines configured to receive dynamic MAC addresses. +* **`[String]` MacAddressMinimum** _(Write)_: Specifies the minimum MAC address + using a valid hexadecimal value. Use with the MacAddressMaximum parameter to + establish a range of MAC addresses that the specified Hyper-V host can assign + to virtual machines configured to receive dynamic MAC addresses. +* **`[Uint32]` MaximumStorageMigrations** _(Write)_: Specifies the maximum number + of storage migrations that can be performed at the same time on the Hyper-V host. +* **`[Uint32]` MaximumVirtualMachineMigrations** _(Write)_: Specifies the maximum + number of live migrations that can be performed at the same time + on the Hyper-V host. +* **`[Boolean]` NumaSpanningEnabled** _(Write)_: Specifies whether virtual machines + on the Hyper-V host can use resources from more than one NUMA node. +* **`[Uint32]` ResourceMeteringSaveIntervalMinute** _(Write)_: Specifies how often + the Hyper-V host saves the data that tracks resource usage. The range is a minimum + of 60 minutes to a maximum 1440 minutes (24 hours). +* **`[Boolean]` UseAnyNetworkForMigration** _(Write)_: Specifies how networks are + selected for incoming live migration traffic. If set to $True, any available network + on the host can be used for this traffic. If set to $False, incoming live migration + traffic is transmitted only on the networks specified in the MigrationNetworks + property of the host. +* **`[String]` VirtualHardDiskPath** _(Write)_: Specifies the default folder to + store virtual hard disks on the Hyper-V host. +* **`[String]` VirtualMachineMigrationAuthenticationType** _(Write)_: Specifies the + type of authentication to be used for live migrations. { Kerberos | CredSSP }. +* **`[String]` VirtualMachineMigrationPerformanceOption** _(Write)_: Specifies the + performance option to use for live migration. { TCPIP | Compression | SMB }. +* **`[String]` VirtualMachinePath** _(Write)_: Specifies the default folder + to store virtual machine configuration files on the Hyper-V host. +* **`[Boolean]` VirtualMachineMigrationEnabled** _(Write)_: Indicates whether Live + Migration should be enabled or disabled on the Hyper-V host. + +#### Read-Only Properties from Get-TargetResource for xVMHost + +None + +#### Examples xVMHost + +* [Change VM Host paths](/Examples/Sample_xVMHost_Paths.ps1) + +### xVMHyperV + +Manages VMs in a Hyper-V host. + +The following properties **cannot** be changed after VM creation: + +* VhdPath +* Path +* Generation + +#### Requirements for xVMHyperV + +* The Hyper-V Role has to be installed on the machine. +* The Hyper-V PowerShell module has to be installed on the machine. + +#### Parameters for xVMHyperV + +* **`[String]` Name** _(Key)_: The desired VM name. +* **`[String]` VhdPath** _(Required)_: The desired VHD associated with the VM. +* **`[String[]]` SwitchName** _(Write)_: Virtual switch(es) associated with the VM. + Multiple NICs can now be assigned. +* **`[String]` State** _(Write)_: State of the VM: { Running | Paused | Off }. +* **`[String]` Path** _(Write)_: Folder where the VM data will be stored. +* **`[Uint32]` Generation** _(Write)_: Virtual machine generation. + Generation 2 virtual machines __only__ support VHDX files. + The default value is 1. { *1* | 2 }. +* **`[Boolean]` SecureBoot** _(Write)_: Enables or disables secure boot + __only on generation 2 virtual machines__. + The default value is $true. +* **`[Uint64]` StartupMemory** _(Write)_: Startup RAM for the VM. + If neither MinimumMemory nor MaximumMemory is specified, dynamic memory will be disabled. +* **`[Uint64]` MinimumMemory** _(Write)_: Minimum RAM for the VM. + Setting this property enables dynamic memory. Exception: + If MinimumMemory, MaximumMemory and StartupMemory is equal, dynamic memory will be disabled. +* **`[Uint64]` MaximumMemory** _(Write)_: Maximum RAM for the VM. + Setting this property enables dynamic memory. Exception: + If MinimumMemory, MaximumMemory and StartupMemory is equal, dynamic memory will be disabled. +* **`[String[]]` MACAddress** _(Write)_: MAC address(es) of the VM. + Multiple MAC addresses can now be assigned. +* **`[Uint32]` ProcessorCount** _(Write)_: Processor count for the VM. +* **`[Boolean]` WaitForIP** _(Write)_: If specified, waits for the VM to get + valid IP address. +* **`[Boolean]` RestartIfNeeded** _(Write)_: If specified, will shutdown and + restart the VM as needed for property changes. +* **`[String]` Ensure** _(Write)_: Ensures that the VM is Present or Absent. + The default value is Present. { *Present* | Absent }. +* **`[String]` Notes** _(Write)_: Notes about the VM. +* **`[Boolean]` EnableGuestService** _(Write)_: Enable Guest Service Interface + for the VM. The default value is $false. + +#### Read-Only Properties from Get-TargetResource for xVMHyperV + +* **`[String]` ID** _(Read)_: VM unique ID. +* **`[String]` Status** _(Read)_: Status of the VM. +* **`[Uint32]` CPUUsage** _(Read)_: CPU Usage of the VM. +* **`[Uint64]` MemoryAssigned** _(Read)_: Memory assigned to the VM. +* **`[String]` Uptime** _(Read)_: Uptime of the VM. +* **`[DateTime]` CreationTime** _(Read)_: Creation time of the VM. +* **`[Boolean]` HasDynamicMemory** _(Read)_: Does VM has dynamic memory enabled. +* **`[String[]]` NetworkAdapters** _(Read)_: Network adapters' IP addresses of + the VM". + +#### Examples xVMHyperV + +* [Create a VM (Simple)](/Examples/Sample_xVMHyperV_Simple.ps1) +* [Create a VM with dynamic memory](/Examples/Sample_xVMHyperV_DynamicMemory.ps1) +* [Create a VM (Complete)](/Examples/Sample_xVMHyperV_Complete.ps1) +* [Create a VM with multiple NICs attached to multiple switches](/Examples/Sample_xVMHyperV_MultipleNICs.ps1) + +### xVMNetworkAdapter + +Manages VMNetadapters attached to a Hyper-V virtual machine or the management OS. + +#### Requirements for xVMNetworkAdapter + +* The Hyper-V Role has to be installed on the machine. +* The Hyper-V PowerShell module has to be installed on the machine. + +#### Parameters for xVMNetworkAdapter + +* **`[String]` Id** _(Key)_: Unique string for identifying the resource instance. +* **`[String]` Name** _(Required)_: Name of the network adapter as it appears either + in the management OS or attached to a VM. +* **`[String]` SwitchName** _(Required)_: Virtual Switch name to connect to. +* **`[String]` VMName** _(Required)_: Name of the VM to attach to. + If you want to attach new VM Network adapter to the management OS, + set this property to 'Management OS'. +* **`[xNetworkSettings]` NetworkSetting** _(Write)_: Network Settings of the network adapter. + If this parameter is not supplied, DHCP will be used. +* **`[String]` MacAddress** _(Write)_: Use this to specify a Static MAC Address. + If this parameter is not specified, dynamic MAC Address will be set. +* **`[String]` VlanId** _(Write)_: Use this to specify a Vlan id on the +* Network Adapter. +* **`[String]` Ensure** _(Write)_: Ensures that the VM Network Adapter is + Present or Absent. The default value is Present. { *Present* | Absent }. + +##### xNetworkSettings Class + +* **`[String]` IpAddress** _(Write)_: IpAddress to give the network adapter. + Only used if not Dhcp. Required if not Dhcp. +* **`[String]` Subnet** _(Write)_: Subnet to give the network adapter. + Only used if not Dhcp. Required if not Dhcp. +* **`[String]` DefaultGateway** _(Write)_: DefaultGateway to give the network adapter. + Only used if not Dhcp. +* **`[String]` DnsServer** _(Write)_: DNSServer to give the network adapter. + Only used if not Dhcp. + +#### Read-Only Properties from Get-TargetResource for xVMNetworkAdapter + +* **`[Boolean]` DynamicMacAddress** _(Read)_: Does the VMNetworkAdapter use a + Dynamic MAC Address. + +#### Examples xVMNetworkAdapter + +* [Add a new VM Network adapter in the management OS](/Examples/Sample_xVMNetworkAdapter_ManagementOS.ps1) +* [Add multiple VM Network adapters to a VM](/Examples/Sample_xVMNetworkAdapter_MultipleVM.ps1) +* [Add a couple of VM Network adapters in the management OS](/Examples/Sample_xVMNetworkAdapter_MultipleManagementOS.ps1) +* [Add multiple VM Network adapters to a VM using status MAC addresses](/Examples/Sample_xVMNetworkAdapter_MultipleVMMACAddress.ps1) +* [Add VM Network adapters to a VM with a Vlan tag](/Examples/Sample_xVMNetworkAdapter_VMVlanTagging.ps1) +* [Add VM Network adapters to a VM with a static IpAddress](/Examples/Sample_xVMNetworkAdapter_VMStaticNetworkSettings.ps1) + +### xVMProcessor + +Manages Hyper-V virtual machine processor options. + +#### Requirements for xVMProcessor + +* The Hyper-V Role has to be installed on the machine. +* The Hyper-V PowerShell module has to be installed on the machine. + +#### Parameters for xVMProcessor + +* **`[String]` VMName** _(Key)_: Specifies the name of the virtual machine + on which the processor is to be configured. +* **`[Boolean]` EnableHostResourceProtection** _(Write)_: Specifies whether to + enable host resource protection. NOTE: Only supported on Windows 10 and Server 2016. +* **`[Boolean]` ExposeVirtualizationExtensions** _(Write)_: Specifies whether + nested virtualization is enabled. NOTE: Only supported on + Windows 10 and Server 2016. +* **`[Uint64]` HwThreadCountPerCore** _(Write)_: Specifies the maximum thread core + per processor core. NOTE: Only supported on Windows 10 and Server 2016. +* **`[Uint64]` Maximum** _(Write)_: Specifies the maximum percentage of resources + available to the virtual machine processor to be configured. + Allowed values range from 0 to 100. +* **`[Uint32]` MaximumCountPerNumaNode** _(Write)_: Specifies the maximum number + of processors per NUMA node to be configured for the virtual machine. +* **`[Uint32]` MaximumCountPerNumaSocket** _(Write)_: Specifies the maximum number + of sockets per NUMA node to be configured for the virtual machine. +* **`[Unit32]` RelativeWeight** _(Write)_: Specifies the priority for allocating + the physical computer's processing power to this virtual machine relative to others. + Allowed values range from 1 to 10000. +* **`[Uint64]` Reserve** _(Write)_: Specifies the percentage of processor resources + to be reserved for this virtual machine. Allowed values range from 0 to 100. +* **`[String]` ResourcePoolName** _(Write)_: Specifies the name of the processor + resource pool to be used. +* **`[Boolean]` CompatibilityForMigrationEnabled** _(Write)_: Specifies whether + the virtual processors features are to be limited for compatibility when migrating + the virtual machine to another host. +* **`[Boolean]` CompatibilityForOlderOperatingSystemsEnabled** _(Write)_: Specifies + whether the virtual processor’s features are to be limited for compatibility + with older operating systems. +* **`[Boolean]` RestartIfNeeded** _(Write)_: If specified, shutdowns and restarts + the VM if needed for property changes. + +#### Read-Only Properties from Get-TargetResource for xVMProcessor + +None + +#### Examples xVMProcessor + +* [Create a secure boot gen 2 VM for a given VHD with nested virtualisation enabled](/Examples/Sample_xVMHyperV_SimpleWithNestedVirtualization.ps1) + +### xVMScsiController + +Manages the SCSI controllers attached to a Hyper-V virtual machine. +When removing a controller, all the disks still connected to the controller will be detached. + +#### Requirements for xVMScsiController + +* The Hyper-V Role has to be installed on the machine. +* The Hyper-V PowerShell module has to be installed on the machine. + +#### Parameters for xVMScsiController + +* **`[String]` VMName** _(Key)_: Specifies the name of the virtual machine whose SCSI + controller is to be manipulated. +* **`[Uint32]` ControllerNumber** _(Key)_: Specifies the number of the controller to + be set: { 0 | 1 | 2 | 3 }. +* **`[String]` Ensure** _(Write)_: Specifies if the SCSI controller should exist or + not. The default value is Present. { *Present* | Absent }. + +#### Read-Only Properties from Get-TargetResource for xVMScsiController + +None + +#### Examples xVMScsiController + +* [Add a secondary SCSI controller](/Examples/Sample_xVMScsiController_AddControllers.ps1) + +### xVMSwitch + +Manages virtual switches in a Hyper-V host. + +#### Requirements for xVMSwitch + +* The Hyper-V Role has to be installed on the machine. +* The Hyper-V PowerShell module has to be installed on the machine. + +#### Parameters for xVMSwitch + +* **`[String]` Name** _(Key)_: The desired VM Switch name. +* **`[String]` Type** _(Key)_: The desired type of switch. + { External | Internal | Private } +* **`[String[]]` NetAdapterName** _(Write)_: Network adapter name(s) + for external switch type. +* **`[Boolean]` AllowManagementOS** _(Write)_: Specify if the VM host + has access to the physical NIC. The default value is $false. +* **`[Boolean]` EnableEmbeddedTeaming** _(Write)_: Should embedded NIC teaming + be used (Windows Server 2016 only). The default value is $false. +* **`[String]` BandwidthReservationMode** _(Write)_: Specify the QoS mode used + (options other than NA are only supported on Hyper-V 2012+). + The default value is NA. { Default | Weight | Absolute | None | *NA* }. +* **`[String]` LoadBalancingAlgorithm** _(Write)_: Specify the Load Balancing algorithm which should be used for the embedded NIC teaming. + { Dynamic | HyperVPort }. +* **`[Boolean]` Id** _(Write)_: Specify the desired Unique ID of the Hyper-V switch. If not specified the ID will be generated by the system every time the Hyper-V Switch is created. (Windows Server 2016 only) +* **`[String]` Ensure** _(Write)_: Ensures that the VM Switch is Present or Absent. + The default value is Present. { *Present* | Absent }. + +#### Read-Only Properties from Get-TargetResource for xVMSwitch + +* **`[String]` NetAdapterInterfaceDescription** _(Read)_: Description of the + network interface. + +#### Examples xVMSwitch + +* [Create an internal VM Switch](/Examples/Sample_xVMSwitch_Internal.ps1) +* [Create an external VM Switch](/Examples/Sample_xVMSwitch_External.ps1) +* [Create an external VM Switch with embedded teaming](/Examples/Sample_xVMSwitch_ExternalSET.ps1) diff --git a/deployment/dsc/azshcihost/xHyper-V/3.17.0.0/Tests/Integration/IntegrationTestsCommon.psm1 b/deployment/dsc/azshcihost/xHyper-V/3.17.0.0/Tests/Integration/IntegrationTestsCommon.psm1 new file mode 100644 index 0000000..f407d69 --- /dev/null +++ b/deployment/dsc/azshcihost/xHyper-V/3.17.0.0/Tests/Integration/IntegrationTestsCommon.psm1 @@ -0,0 +1,59 @@ +<# + .SYNOPSIS + Tests if Hyper-V is installed on this OS. + + .OUTPUTS + True if Hyper-V is installed. False otherwise. +#> +function Test-HyperVInstalled +{ + [CmdletBinding()] + [OutputType([String])] + param + ( + ) + + # Ensure that the tests can be performed on this computer + if ($PSVersionTable.PSEdition -eq 'Core') + { + # Nano Server uses Get-WindowsOptionalFeature like Desktop OS + $ProductType = 1 + } + else + { + $ProductType = (Get-CimInstance Win32_OperatingSystem).ProductType + } # if + switch ($ProductType) { + 1 + { + # Desktop OS or Nano Server + $HyperVInstalled = (((Get-WindowsOptionalFeature ` + -FeatureName Microsoft-Hyper-V ` + -Online).State -eq 'Enabled') -and ` + ((Get-WindowsOptionalFeature ` + -FeatureName Microsoft-Hyper-V-Management-PowerShell ` + -Online).State -eq 'Enabled')) + Break + } + 3 + { + # Server OS + $HyperVInstalled = (((Get-WindowsFeature -Name Hyper-V).Installed) -and ` + ((Get-WindowsFeature -Name Hyper-V-PowerShell).Installed)) + Break + } + default + { + # Unsupported OS type for testing + Write-Verbose -Message "Integration tests cannot be run on this operating system." -Verbose + Break + } + } + + if ($HyperVInstalled -eq $false) + { + Write-Verbose -Message "Integration tests cannot be run because Hyper-V Components not installed." -Verbose + Return $false + } + Return $True +} # end function Test-HyperVInstalled diff --git a/deployment/dsc/azshcihost/xHyper-V/3.17.0.0/Tests/Integration/MSFT_xVMDvdDrive.Integration.Tests.ps1 b/deployment/dsc/azshcihost/xHyper-V/3.17.0.0/Tests/Integration/MSFT_xVMDvdDrive.Integration.Tests.ps1 new file mode 100644 index 0000000..50813a0 --- /dev/null +++ b/deployment/dsc/azshcihost/xHyper-V/3.17.0.0/Tests/Integration/MSFT_xVMDvdDrive.Integration.Tests.ps1 @@ -0,0 +1,141 @@ +$script:DSCModuleName = 'xHyper-V' +$script:DSCResourceName = 'MSFT_xVMDvdDrive' + +#region HEADER +# Integration Test Template Version: 1.1.1 +[String] $script:moduleRoot = Split-Path -Parent (Split-Path -Parent $PSScriptRoot) +if ( (-not (Test-Path -Path (Join-Path -Path $script:moduleRoot -ChildPath 'DSCResource.Tests'))) -or ` + (-not (Test-Path -Path (Join-Path -Path $script:moduleRoot -ChildPath 'DSCResource.Tests\TestHelper.psm1'))) ) +{ + & git @('clone','https://github.com/PowerShell/DscResource.Tests.git',(Join-Path -Path $script:moduleRoot -ChildPath '\DSCResource.Tests\')) +} + +Import-Module (Join-Path -Path $script:moduleRoot -ChildPath 'DSCResource.Tests\TestHelper.psm1') -Force +$TestEnvironment = Initialize-TestEnvironment ` + -DSCModuleName $script:DSCModuleName ` + -DSCResourceName $script:DSCResourceName ` + -TestType Integration +#endregion + +# Import the common integration test functions +Import-Module -Name ( Join-Path ` + -Path $PSScriptRoot ` + -ChildPath 'IntegrationTestsCommon.psm1' ) + +# Ensure that the tests can be performed on this computer +if (-not (Test-HyperVInstalled)) +{ + Return +} # if + +# Using try/finally to always cleanup even if something awful happens. +try +{ + $VMName = 'HyperVIntTestsVM' + ## Cannot use $TestDrive here as it no longer accessible outside of Describe/Context + $VMPath = Join-Path -Path $env:Temp -ChildPath $VMName + + # Make sure test VM does not exist + if (Get-VM -Name $VMName -ErrorAction SilentlyContinue) + { + $null = Remove-VM -Name $VMName -Force + } # if + + # Create the VM that will be used to test with + $null = New-VM -Name $VMName -NoVHD -Path $VMPath + + # Create a config data object to pass to the DSC Configs + $ConfigData = @{ + AllNodes = @( + @{ + NodeName = 'localhost' + VMName = $VMName + ControllerNumber = 0 + ControllerLocation = 0 + Path = '' + } + ) + } + + # Add DVD Drive + $ConfigFile = Join-Path -Path $PSScriptRoot -ChildPath "$($script:DSCResourceName)_add.config.ps1" + . $ConfigFile -Verbose -ErrorAction Stop + + Describe "$($script:DSCResourceName)_Add_Integration" { + Context 'Add a DVD Drive to a VM' { + #region DEFAULT TESTS + It 'Should compile without throwing' { + { + & "$($script:DSCResourceName)_Add_Config" ` + -OutputPath $TestDrive ` + -ConfigurationData $ConfigData + Start-DscConfiguration -Path $TestDrive ` + -ComputerName localhost -Wait -Verbose -Force + } | Should not throw + } + + It 'should be able to call Get-DscConfiguration without throwing' { + { Get-DscConfiguration -Verbose -ErrorAction Stop } | Should Not throw + } + #endregion + + It 'Should have set the resource and all the parameters should match' { + $current = Get-DscConfiguration | Where-Object { + $_.ConfigurationName -eq "$($script:DSCResourceName)_Add_Config" + } + $current.VMName | Should Be $VMName + $current.ControllerNumber | Should Be 0 + $current.ControllerLocation | Should Be 0 + $current.Path | Should BeNullOrEmpty + $current.Ensure | Should Be 'Present' + } + } + } + + # Dismount ISO + $ConfigFile = Join-Path -Path $PSScriptRoot -ChildPath "$($script:DSCResourceName)_remove.config.ps1" + . $ConfigFile -Verbose -ErrorAction Stop + + Describe "$($script:DSCResourceName)_Remove_Integration" { + Context 'Remove a DVD Drive from a VM' { + #region DEFAULT TESTS + It 'Should compile without throwing' { + { + & "$($script:DSCResourceName)_Remove_Config" ` + -OutputPath $TestDrive ` + -ConfigurationData $ConfigData + Start-DscConfiguration -Path $TestDrive ` + -ComputerName localhost -Wait -Verbose -Force + } | Should not throw + } + + It 'should be able to call Get-DscConfiguration without throwing' { + { Get-DscConfiguration -Verbose -ErrorAction Stop } | Should Not throw + } + #endregion + + It 'Should have set the resource and all the parameters should match' { + $current = Get-DscConfiguration | Where-Object { + $_.ConfigurationName -eq "$($script:DSCResourceName)_Remove_Config" + } + $current.VMName | Should Be $VMName + $current.ControllerNumber | Should Be 0 + $current.ControllerLocation | Should Be 0 + $current.Path | Should BeNullOrEmpty + $current.Ensure | Should Be 'Absent' + } + } + } +} +finally +{ + #region FOOTER + # Make sure the test VM has been removed + if (Get-VM -Name $VMName -ErrorAction SilentlyContinue) + { + $null = Remove-VM -Name $VMName -Force + } # if + + Restore-TestEnvironment -TestEnvironment $TestEnvironment + #endregion +} diff --git a/deployment/dsc/azshcihost/xHyper-V/3.17.0.0/Tests/Integration/MSFT_xVMDvdDrive_add.config.ps1 b/deployment/dsc/azshcihost/xHyper-V/3.17.0.0/Tests/Integration/MSFT_xVMDvdDrive_add.config.ps1 new file mode 100644 index 0000000..defc5f3 --- /dev/null +++ b/deployment/dsc/azshcihost/xHyper-V/3.17.0.0/Tests/Integration/MSFT_xVMDvdDrive_add.config.ps1 @@ -0,0 +1,14 @@ +configuration MSFT_xVMDvdDrive_Add_Config { + + Import-DscResource -ModuleName xHyper-V + + node localhost { + xVMDvdDrive Integration_Test { + VMName = $Node.VMName + ControllerNumber = $Node.ControllerNumber + ControllerLocation = $Node.ControllerLocation + Path = $Node.Path + Ensure = 'Present' + } + } +} diff --git a/deployment/dsc/azshcihost/xHyper-V/3.17.0.0/Tests/Integration/MSFT_xVMDvdDrive_remove.config.ps1 b/deployment/dsc/azshcihost/xHyper-V/3.17.0.0/Tests/Integration/MSFT_xVMDvdDrive_remove.config.ps1 new file mode 100644 index 0000000..fb9ea13 --- /dev/null +++ b/deployment/dsc/azshcihost/xHyper-V/3.17.0.0/Tests/Integration/MSFT_xVMDvdDrive_remove.config.ps1 @@ -0,0 +1,14 @@ +configuration MSFT_xVMDvdDrive_Remove_Config { + + Import-DscResource -ModuleName xHyper-V + + node localhost { + xVMDvdDrive Integration_Test { + VMName = $Node.VMName + ControllerNumber = $Node.ControllerNumber + ControllerLocation = $Node.ControllerLocation + Path = $Node.Path + Ensure = 'Absent' + } + } +} diff --git a/deployment/dsc/azshcihost/xHyper-V/3.17.0.0/Tests/Integration/MSFT_xVMHost.Integration.Tests.ps1 b/deployment/dsc/azshcihost/xHyper-V/3.17.0.0/Tests/Integration/MSFT_xVMHost.Integration.Tests.ps1 new file mode 100644 index 0000000..1a18764 --- /dev/null +++ b/deployment/dsc/azshcihost/xHyper-V/3.17.0.0/Tests/Integration/MSFT_xVMHost.Integration.Tests.ps1 @@ -0,0 +1,94 @@ +$script:DSCModuleName = 'xHyper-V' +$script:DSCResourceName = 'MSFT_xVMHost' + +#region HEADER +# Integration Test Template Version: 1.1.1 +[String] $script:moduleRoot = Split-Path -Parent (Split-Path -Parent $PSScriptRoot) +if ( (-not (Test-Path -Path (Join-Path -Path $script:moduleRoot -ChildPath 'DSCResource.Tests'))) -or ` + (-not (Test-Path -Path (Join-Path -Path $script:moduleRoot -ChildPath 'DSCResource.Tests\TestHelper.psm1'))) ) +{ + & git @('clone','https://github.com/PowerShell/DscResource.Tests.git',(Join-Path -Path $script:moduleRoot -ChildPath '\DSCResource.Tests\')) +} + +Import-Module (Join-Path -Path $script:moduleRoot -ChildPath 'DSCResource.Tests\TestHelper.psm1') -Force +$TestEnvironment = Initialize-TestEnvironment ` + -DSCModuleName $script:DSCModuleName ` + -DSCResourceName $script:DSCResourceName ` + -TestType Integration +#endregion + +# Import the common integration test functions +Import-Module -Name ( Join-Path ` + -Path $PSScriptRoot ` + -ChildPath 'IntegrationTestsCommon.psm1' ) + +# Ensure that the tests can be performed on this computer +if (-not (Test-HyperVInstalled)) +{ + Return +} # if + +$currentVmHost = Get-VMHost +# Set-VMHost appears to update $currentVmHost by reference?! +$currentVirtualHardDiskPath = $currentVmHost.VirtualHardDiskPath +$currentVirtualMachinePath = $currentVmHost.VirtualMachinePath +$currentEnableEnhancedSessionMode = $currentVmHost.EnableEnhancedSessionMode + +# Using try/finally to always cleanup even if something awful happens. +try +{ + # Import the configuration + $ConfigFile = Join-Path -Path $PSScriptRoot -ChildPath "$($script:DSCResourceName)_set.config.ps1" + . $ConfigFile -Verbose -ErrorAction Stop + + Describe "$($script:DSCResourceName)_Set_Integration" { + #region DEFAULT TESTS + It 'Should compile without throwing' { + { + & "$($script:DSCResourceName)_Set_Config" ` + -OutputPath $TestDrive ` + -ConfigurationData $ConfigData ` + -VirtualHardDiskPath $TestDrive ` + -VirtualMachinePath $TestDrive ` + -EnableEnhancedSessionMode (-not $currentEnableEnhancedSessionMode) + + $startDscConfigurationParams = @{ + Path = $TestDrive; + ComputerName = 'localhost'; + Wait = $true; + Verbose = $true; + Force = $true; + } + Start-DscConfiguration @startDscConfigurationParams + + } | Should not throw + } + + It 'should be able to call Get-DscConfiguration without throwing' { + { Get-DscConfiguration -Verbose -ErrorAction Stop } | Should Not throw + } + #endregion + + It 'Should have set the resource and all the parameters should match' { + $current = Get-DscConfiguration | Where-Object { + $_.ConfigurationName -eq "$($script:DSCResourceName)_Set_Config" + } + + $current.VirtualHardDiskPath | Should Be $TestDrive.FullName + $current.VirtualMachinePath | Should Be $TestDrive.FullName + $current.EnableEnhancedSessionMode | Should Be (-not $currentEnableEnhancedSessionMode) + } + } +} +finally +{ + #region FOOTER + + # Restore current host settings + Set-VMHost -VirtualHardDiskPath $currentVirtualHardDiskPath ` + -VirtualMachinePath $currentVirtualMachinePath ` + -EnableEnhancedSessionMode $currentEnableEnhancedSessionMode -Verbose + + Restore-TestEnvironment -TestEnvironment $TestEnvironment + #endregion +} diff --git a/deployment/dsc/azshcihost/xHyper-V/3.17.0.0/Tests/Integration/MSFT_xVMHost_set.config.ps1 b/deployment/dsc/azshcihost/xHyper-V/3.17.0.0/Tests/Integration/MSFT_xVMHost_set.config.ps1 new file mode 100644 index 0000000..14c43bc --- /dev/null +++ b/deployment/dsc/azshcihost/xHyper-V/3.17.0.0/Tests/Integration/MSFT_xVMHost_set.config.ps1 @@ -0,0 +1,29 @@ +configuration MSFT_xVMHost_Set_Config +{ + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $VirtualHardDiskPath, + + [Parameter(Mandatory = $true)] + [System.String] + $VirtualMachinePath, + + [Parameter()] + [System.Boolean] + $EnableEnhancedSessionMode + ) + + Import-DscResource -ModuleName xHyper-V + + node localhost { + xVMHost Integration_Test { + IsSingleInstance = 'Yes' + VirtualHardDiskPath = $VirtualHardDiskPath + VirtualMachinePath = $VirtualMachinePath + EnableEnhancedSessionMode = $EnableEnhancedSessionMode + } + } + +} diff --git a/deployment/dsc/azshcihost/xHyper-V/3.17.0.0/Tests/Integration/MSFT_xVMProcessor.Integration.Tests.ps1 b/deployment/dsc/azshcihost/xHyper-V/3.17.0.0/Tests/Integration/MSFT_xVMProcessor.Integration.Tests.ps1 new file mode 100644 index 0000000..a5751f5 --- /dev/null +++ b/deployment/dsc/azshcihost/xHyper-V/3.17.0.0/Tests/Integration/MSFT_xVMProcessor.Integration.Tests.ps1 @@ -0,0 +1,103 @@ +$script:DSCModuleName = 'xHyper-V' +$script:DSCResourceName = 'MSFT_xVMProcessor' + +#region HEADER +# Integration Test Template Version: 1.1.1 +[String] $script:moduleRoot = Split-Path -Parent (Split-Path -Parent $PSScriptRoot) +if ( (-not (Test-Path -Path (Join-Path -Path $script:moduleRoot -ChildPath 'DSCResource.Tests'))) -or ` + (-not (Test-Path -Path (Join-Path -Path $script:moduleRoot -ChildPath 'DSCResource.Tests\TestHelper.psm1'))) ) +{ + & git @('clone','https://github.com/PowerShell/DscResource.Tests.git',(Join-Path -Path $script:moduleRoot -ChildPath '\DSCResource.Tests\')) +} + +Import-Module (Join-Path -Path $script:moduleRoot -ChildPath 'DSCResource.Tests\TestHelper.psm1') -Force +$TestEnvironment = Initialize-TestEnvironment ` + -DSCModuleName $script:DSCModuleName ` + -DSCResourceName $script:DSCResourceName ` + -TestType Integration +#endregion + +# Import the common integration test functions +Import-Module -Name ( Join-Path ` + -Path $PSScriptRoot ` + -ChildPath 'IntegrationTestsCommon.psm1' ) + +# Ensure that the tests can be performed on this computer +if (-not (Test-HyperVInstalled)) +{ + Return +} # if + +# Using try/finally to always cleanup even if something awful happens. +try +{ + $VMName = 'HyperVIntTestsVM' + $VMPath = Join-Path -Path $env:Temp ` + -ChildPath $VMName + + # Make sure test VM does not exist + if (Get-VM -Name $VMName -ErrorAction SilentlyContinue) + { + $null = Remove-VM -Name $VMName -Force + } # if + + # Create the VM that will be used to test with + $null = New-VM -Name $VMName -NoVHD -Path $VMPath + + # Create a config data object to pass to the DSC Configs + $ConfigData = @{ + AllNodes = @( + @{ + NodeName = 'localhost' + VMName = $VMName + CompatibilityForMigrationEnabled = $true + CompatibilityForOlderOperatingSystemsEnabled = $true + } + ) + } + + # Set processor option(s) + $ConfigFile = Join-Path -Path $PSScriptRoot -ChildPath "$($script:DSCResourceName)_set.config.ps1" + . $ConfigFile -Verbose -ErrorAction Stop + + Describe "$($script:DSCResourceName)_Set_Integration" { + Context 'Configure VM' { + #region DEFAULT TESTS + It 'Should compile without throwing' { + { + & "$($script:DSCResourceName)_Set_Config" ` + -OutputPath $TestDrive ` + -ConfigurationData $ConfigData + Start-DscConfiguration -Path $TestDrive ` + -ComputerName localhost -Wait -Verbose -Force + } | Should not throw + } + + It 'should be able to call Get-DscConfiguration without throwing' { + { Get-DscConfiguration -Verbose -ErrorAction Stop } | Should Not throw + } + #endregion + + It 'Should have set the resource and all the parameters should match' { + $current = Get-DscConfiguration | Where-Object { + $_.ConfigurationName -eq "$($script:DSCResourceName)_Set_Config" + } + $current.VMName | Should Be $VMName + $current.CompatibilityForMigrationEnabled | Should Be $true + $current.CompatibilityForOlderOperatingSystemsEnabled | Should Be $true + } + } + } +} +finally +{ + #region FOOTER + # Make sure the test VM has been removed + if (Get-VM -Name $VMName -ErrorAction SilentlyContinue) + { + $null = Remove-VM -Name $VMName -Force + } # if + + Restore-TestEnvironment -TestEnvironment $TestEnvironment + #endregion +} diff --git a/deployment/dsc/azshcihost/xHyper-V/3.17.0.0/Tests/Integration/MSFT_xVMProcessor_set.config.ps1 b/deployment/dsc/azshcihost/xHyper-V/3.17.0.0/Tests/Integration/MSFT_xVMProcessor_set.config.ps1 new file mode 100644 index 0000000..ee46073 --- /dev/null +++ b/deployment/dsc/azshcihost/xHyper-V/3.17.0.0/Tests/Integration/MSFT_xVMProcessor_set.config.ps1 @@ -0,0 +1,12 @@ +configuration MSFT_xVMProcessor_Set_Config { + + Import-DscResource -ModuleName xHyper-V + + node localhost { + xVMProcessor Integration_Test { + VMName = $Node.VMName + CompatibilityForMigrationEnabled = $Node.CompatibilityForMigrationEnabled + CompatibilityForOlderOperatingSystemsEnabled = $Node.CompatibilityForOlderOperatingSystemsEnabled + } + } +} diff --git a/deployment/dsc/azshcihost/xHyper-V/3.17.0.0/Tests/Unit/HyperVCommon.tests.ps1 b/deployment/dsc/azshcihost/xHyper-V/3.17.0.0/Tests/Unit/HyperVCommon.tests.ps1 new file mode 100644 index 0000000..1158f2e --- /dev/null +++ b/deployment/dsc/azshcihost/xHyper-V/3.17.0.0/Tests/Unit/HyperVCommon.tests.ps1 @@ -0,0 +1,338 @@ +$script:DSCModuleName = 'xHyper-V' +$script:DSCResourceName = 'HyperVCommon' + +#region HEADER +# Unit Test Template Version: 1.1.0 +[String] $script:moduleRoot = Split-Path -Parent (Split-Path -Parent $PSScriptRoot) +if ( (-not (Test-Path -Path (Join-Path -Path $script:moduleRoot -ChildPath 'DSCResource.Tests'))) -or ` + (-not (Test-Path -Path (Join-Path -Path $script:moduleRoot -ChildPath 'DSCResource.Tests\TestHelper.psm1'))) ) +{ + & git @('clone','https://github.com/PowerShell/DscResource.Tests.git',(Join-Path -Path $script:moduleRoot -ChildPath '\DSCResource.Tests\')) +} + +Import-Module (Join-Path -Path $script:moduleRoot -ChildPath 'DSCResource.Tests\TestHelper.psm1') -Force +$TestEnvironment = Initialize-TestEnvironment ` + -DSCModuleName $script:DSCModuleName ` + -DSCResourceName $script:DSCResourceName ` + -TestType Unit +#endregion HEADER + +# Begin Testing +try +{ + #region Pester Tests + + $LocalizedData = InModuleScope $script:DSCResourceName { + $LocalizedData + } + + InModuleScope $script:DSCResourceName { + + Describe 'HyperVCommon\Set-VMProperty' { + + function Get-VM { + param + ( + [System.String] + $Name + ) + } + + function Get-VMProcessor { + param + ( + [System.String] + $VMName + ) + } + + function Set-VMProcessor { + param + ( + [System.String] + $VMName + ) + } + + # Guard mocks + Mock Get-VM { } + Mock Set-VMState { } + Mock Get-VMProcessor { } + Mock Set-VMProcessor { } + + It "Should throw if VM is running and 'RestartIfNeeded' is False" { + Mock Get-VM { return @{ State = 'Running' } } + + $setVMPropertyParams = @{ + VMName = 'Test'; + VMCommand = 'Set-VMProcessor'; + ChangeProperty = @{ ResourcePoolName = 'Dummy' } + } + { Set-VMProperty @setVMPropertyParams } | Should Throw 'RestartIfNeeded' + } + + It "Should stop and restart VM when running and 'RestartIfNeeded' is True" { + Mock Get-VM { return @{ State = 'Running' } } + + $setVMPropertyParams = @{ + VMName = 'Test'; + VMCommand = 'Set-VMProcessor'; + ChangeProperty = @{ ResourcePoolName = 'Dummy' } + RestartIfNeeded = $true; + } + Set-VMProperty @setVMPropertyParams + + Assert-MockCalled Set-VMState -ParameterFilter { $State -eq 'Off' } -Scope It + Assert-MockCalled Set-VMState -ParameterFilter { $State -eq 'Running' } -Scope It + } + + } + + Describe 'HyperVCommon\Set-VMState' { + + function Get-VM { + param + ( + [System.String] + $Name + ) + } + + function Resume-VM { + param + ( + [System.String] + $Name + ) + } + + function Start-VM { + param + ( + [System.String] + $Name + ) + } + + function Stop-VM { + param + ( + [System.String] + $Name + ) + } + + function Suspend-VM { + param + ( + [System.String] + $Name + ) + } + + # Guard mocks + Mock Resume-VM { } + Mock Start-VM { } + Mock Stop-VM { } + Mock Suspend-VM { } + Mock Wait-VMIPAddress { } + + It 'Should resume VM when current "State" is "Paused" and target state is "Running"' { + Mock Get-VM { return @{ State = 'Paused' } } + + Set-VMState -Name 'TestVM' -State 'Running' + + Assert-MockCalled Resume-VM -Scope It + Assert-MockCalled Wait-VMIPAddress -Scope It -Exactly 0 + } + + It 'Should resume VM and wait when current "State" is "Paused" and target state is "Running"' { + Mock Get-VM { return @{ State = 'Paused' } } + + Set-VMState -Name 'TestVM' -State 'Running' -WaitForIP $true + + Assert-MockCalled Resume-VM -Scope It + Assert-MockCalled Wait-VMIPAddress -Scope It + } + + It 'Should start VM when current "State" is "Off" and target state is "Running"' { + Mock Get-VM { return @{ State = 'Off' } } + + Set-VMState -Name 'TestVM' -State 'Running' + + Assert-MockCalled Start-VM -Scope It + Assert-MockCalled Wait-VMIPAddress -Scope It -Exactly 0 + } + + It 'Should start VM and wait when current "State" is "Off" and target state is "Running"' { + Mock Get-VM { return @{ State = 'Off' } } + + Set-VMState -Name 'TestVM' -State 'Running' -WaitForIP $true + + Assert-MockCalled Start-VM -Scope It + Assert-MockCalled Wait-VMIPAddress -Scope It + } + + It 'Should suspend VM when current "State" is "Running" and target state is "Paused"' { + Mock Get-VM { return @{ State = 'Running' } } + + Set-VMState -Name 'TestVM' -State 'Paused' + + Assert-MockCalled Suspend-VM -Scope It + } + + It 'Should stop VM when current "State" is "Running" and target state is "Off"' { + Mock Get-VM { return @{ State = 'Running' } } + + Set-VMState -Name 'TestVM' -State 'Off' + + Assert-MockCalled Stop-VM -Scope It + } + + It 'Should stop VM when current "State" is "Paused" and target state is "Off"' { + Mock Get-VM { return @{ State = 'Paused' } } + + Set-VMState -Name 'TestVM' -State 'Off' + + Assert-MockCalled Stop-VM -Scope It + } + } # describe HyperVCommon\Set-VMState + } + + Describe 'HyperVCommon\Wait-VMIPAddress' { + + function Get-VMNetworkAdapter { + param + ( + [System.String] + $VMName + ) + } + + # Guard mocks + Mock Get-VMNetworkAdapter -ModuleName $script:DSCResourceName { } + + It 'Should return when VM network adapter reports 2 IP addresses' { + Mock Get-VMNetworkAdapter -ModuleName $script:DSCResourceName { return @{ IpAddresses = @('192.168.0.1','172.16.0.1') } } + + $result = Wait-VMIPAddress -Name 'Test' + + $result | Should BeNullOrEmpty + } + + It 'Should throw when after timeout is exceeded' { + Mock Get-VMNetworkAdapter -ModuleName $script:DSCResourceName { return $null } + + { Wait-VMIPAddress -Name 'Test' -Timeout 2 } | Should Throw 'timed out' + } + } # describe HyperVCommon\WaitVMIPAddress + + Describe 'HyperVCommon\ConvertTo-TimeSpan' { + + It 'Should convert 60 seconds to "System.TimeSpan" of 1 minute' { + $testSeconds = 60 + + $result = ConvertTo-TimeSpan -TimeInterval $testSeconds -TimeIntervalType Seconds + + $result.TotalMinutes | Should Be 1 + } + + It 'Should convert 60 minutes to "System.TimeSpan" of 60 minutes' { + $testMinutes = 60 + + $result = ConvertTo-TimeSpan -TimeInterval $testMinutes -TimeIntervalType Minutes + + $result.TotalHours | Should Be 1 + } + + It 'Should convert 48 hours to "System.TimeSpan" of 2 days' { + $testHours = 48 + + $result = ConvertTo-TimeSpan -TimeInterval $testHours -TimeIntervalType Hours + + $result.TotalDays | Should Be 2 + } + + } # describe HyperVCommon\ConvertTo-TimeSpan + + Describe 'HyperVCommon\ConvertFrom-TimeSpan' { + + It 'Should convert a "System.TimeSpan" of 1 minute to 60 seconds' { + $testTimeSpan = New-TimeSpan -Minutes 1 + + $result = ConvertFrom-TimeSpan -TimeSpan $testTimeSpan -TimeSpanType Seconds + + $result | Should Be 60 + } + + It 'Should convert a "System.TimeSpan" of 1 hour to 60 minutes' { + $testTimeSpan = New-TimeSpan -Hours 1 + + $result = ConvertFrom-TimeSpan -TimeSpan $testTimeSpan -TimeSpanType Minutes + + $result | Should Be 60 + } + + It 'Should convert a "System.TimeSpan" of 2 dayes to 48 hours' { + $testTimeSpan = New-TimeSpan -Days 2 + + $result = ConvertFrom-TimeSpan -TimeSpan $testTimeSpan -TimeSpanType Hours + + $result | Should Be 48 + } + + } # describe HyperVCommon\ConvertFrom-TimeSpan + + Describe 'HyperVCommon\Get-VMHyperV' { + + function Get-VM { + [CmdletBinding()] + param + ( + $Name + ) + } + + # Guard mocks + Mock Get-VM -ModuleName $script:DSCResourceName { } + + It 'Should not throw when no VM is found' { + Mock Get-VM -ModuleName $script:DSCResourceName { } + $testVMName = 'TestVM' + + $result = Get-VMHyperV -VMName $testVMName + + $result | Should BeNullOrEmpty + } + + It 'Should not throw when one VM is found' { + Mock Get-VM -ModuleName $script:DSCResourceName { + Write-Output -InputObject ([PSCustomObject] @{ Name = $VMName }) + } + $testVMName = 'TestVM' + + $result = Get-VMHyperV -VMName $testVMName + + $result.Name | Should Be $testVMName + } + + It 'Should throw when more than one VM is found' { + Mock Get-VM -ModuleName $script:DSCResourceName { + Write-Output -InputObject ([PSCustomObject] @{ Name = $VMName }) + Write-Output -InputObject ([PSCustomObject] @{ Name = $VMName }) + } + $testVMName = 'TestVM' + + { Get-VMHyperV -VMName $testVMName } | Should Throw 'More than one VM' + } + + } # describe HyperVCommon\Get-VMHyperV + +} +finally +{ + #region FOOTER + Restore-TestEnvironment -TestEnvironment $TestEnvironment + #endregion + +} diff --git a/deployment/dsc/azshcihost/xHyper-V/3.17.0.0/Tests/Unit/MSFT_xVHD.tests.ps1 b/deployment/dsc/azshcihost/xHyper-V/3.17.0.0/Tests/Unit/MSFT_xVHD.tests.ps1 new file mode 100644 index 0000000..83d779c --- /dev/null +++ b/deployment/dsc/azshcihost/xHyper-V/3.17.0.0/Tests/Unit/MSFT_xVHD.tests.ps1 @@ -0,0 +1,302 @@ +#region HEADER + +# Unit Test Template Version: 1.2.0 +$script:moduleRoot = Split-Path -Parent (Split-Path -Parent $PSScriptRoot) +if ( (-not (Test-Path -Path (Join-Path -Path $script:moduleRoot -ChildPath 'DSCResource.Tests'))) -or ` + (-not (Test-Path -Path (Join-Path -Path $script:moduleRoot -ChildPath 'DSCResource.Tests\TestHelper.psm1'))) ) +{ + & git @('clone', 'https://github.com/PowerShell/DscResource.Tests.git', (Join-Path -Path $script:moduleRoot -ChildPath '\DSCResource.Tests\')) +} + +Import-Module -Name (Join-Path -Path $script:moduleRoot -ChildPath (Join-Path -Path 'DSCResource.Tests' -ChildPath 'TestHelper.psm1')) -Force + +$TestEnvironment = Initialize-TestEnvironment ` + -DSCModuleName 'xHyper-V' ` + -DSCResourceName 'MSFT_xVHD' ` + -TestType Unit + +#endregion HEADER + +function Invoke-TestSetup +{ + +} + +function Invoke-TestCleanup +{ + Restore-TestEnvironment -TestEnvironment $TestEnvironment +} + +# Begin Testing +try +{ + Invoke-TestSetup + + InModuleScope 'MSFT_xVHD' { + Describe 'MSFT_xVHD\Get-TargetResource' { + # Create an empty function to be able to mock the missing Hyper-V cmdlet + function Get-VHD + { + + } + + Context 'Should stop when Hyper-V module is missing' { + Mock -CommandName Get-Module -ParameterFilter { ($Name -eq 'Hyper-V') -and ($ListAvailable -eq $true) } -MockWith { + return $false + } + + It 'Should throw when the module is missing' { + { Test-TargetResource -Name 'server.vhdx' -Path 'C:\VMs' -Type 'Fixed' -MaximumSizeBytes 1GB } | + Should Throw 'Please ensure that Hyper-V role is installed with its PowerShell module' + } + } + + # Mocks "Get-Module -Name Hyper-V" so that the DSC resource thinks the Hyper-V module is on the test system + Mock -CommandName Get-Module -ParameterFilter { ($Name -eq 'Hyper-V') -and ($ListAvailable -eq $true) } -MockWith { + return $true + } + + Mock -CommandName GetNameWithExtension -MockWith { 'server.vhdx' } + + Context 'VHD Present' { + It 'Should return a hashtable with Ensure being Present' { + Mock -CommandName Get-VHD -MockWith { + [pscustomobject]@{ + Path = 'server.vhdx' + } + } + + $getTargetResult = Get-TargetResource -Name 'server' -Path 'c:\boguspath' -Generation 'vhdx' + $getTargetResult.Ensure | Should Be 'Present' + $getTargetResult | Should BeOfType hashtable + } + } + + Context 'VHD Not Present' { + It 'Should return a hashtable with Ensure being Absent' { + Mock -CommandName Get-VHD + + $getTargetResult = Get-TargetResource -Name 'server' -Path 'c:\boguspath' -Generation 'vhdx' + $getTargetResult.Ensure | Should Be 'Absent' + $getTargetResult | Should BeOfType hashtable + } + } + } + + Describe 'MSFT_xVHD\GetNameWithExtension' { + Context 'Name does not have extension' { + It 'Should return server.vhdx with generation vhdx' { + GetNameWithExtension -Name 'server' -Generation 'vhdx' | + Should Be 'server.vhdx' + } + + It 'Should return server.vhd with generation vhd' { + GetNameWithExtension -Name 'server' -Generation 'vhd' | + Should Be 'server.vhd' + } + + It 'Should not throw' { + { GetNameWithExtension -Name 'server' -Generation 'vhd' } | + Should Not Throw + } + } + + Context 'Name has extension' { + It 'Should return server.vhdx with Name server.vhdx and generation vhdx' { + GetNameWithExtension -Name 'server.vhd' -Generation 'vhd' | + Should Be 'server.vhd' + } + + It 'Should throw with mismatch with extension from name and generation' { + { GetNameWithExtension -Name 'server.vhdx' -Generation 'vhd' } | + Should Throw 'the extension vhdx on the name does not match the generation vhd' + } + } + } + + Describe 'MSFT_xVHD\Test-TargetResource' { + # Create an empty function to be able to mock the missing Hyper-V cmdlet + function Test-VHD + { + + } + + Context 'Should stop when Hyper-V module is missing' { + Mock -CommandName Get-Module -ParameterFilter { ($Name -eq 'Hyper-V') -and ($ListAvailable -eq $true) } -MockWith { + return $false + } + + It 'Should throw when the module is missing' { + { Test-TargetResource -Name 'server.vhdx' -Path 'C:\VMs' -Type 'Fixed' -MaximumSizeBytes 1GB } | + Should Throw 'Please ensure that Hyper-V role is installed with its PowerShell module' + } + } + + # Mocks "Get-Module -Name Hyper-V" so that the DSC resource thinks the Hyper-V module is on the test system + Mock -CommandName Get-Module -ParameterFilter { ($Name -eq 'Hyper-V') -and ($ListAvailable -eq $true) } -MockWith { + return $true + } + + Context 'Parameter validation' { + It 'Fixed and Dynamic VHDs need MaximumSizeBytes specified' { + { Test-TargetResource -Name 'server' -Path 'C:\VMs' -Type 'Dynamic' } | + Should Throw 'Specify MaximumSizeBytes property for Fixed and Dynamic VHDs.' + } + + It 'Parent Path is passed for a non Differencing disk' { + { Test-TargetResource -Name 'server' -Path 'C:\VMs' -ParentPath 'C:\VMs\Parent' -Type 'Fixed' -MaximumSizeBytes 1GB } | + Should Throw 'Parent path is only supported for Differencing disks' + } + + It 'Differencing disk needs a Parent Path' { + { Test-TargetResource -Name 'server' -Path 'C:\VMs' -Type 'Differencing' } | + Should Throw 'Differencing requires a parent path' + } + } + + Context 'ParentPath specified' { + It 'Should throw when ParentPath does not exist' { + Mock -CommandName Test-Path -MockWith { $false } + + { Test-TargetResource -Name 'server' -Path 'C:\VMs' -Type 'Differencing' -ParentPath 'c:\boguspath' } | + Should Throw 'c:\boguspath does not exists' + } + + # "Generation $Generation should match ParentPath extension $($ParentPath.Split('.')[-1])" + It 'Should throw when file extension and generation have a mismatch' { + Mock -CommandName Test-Path -MockWith { $true } + + { Test-TargetResource -Name 'server' -Path 'C:\VMs' -Type 'Differencing' -ParentPath 'c:\boguspath.vhd' -Generation 'Vhdx' } | + Should Throw 'Generation Vhdx should match ParentPath extension vhd' + } + } + + Context 'Path does not exist' { + It 'Should throw when the path does not exist' { + Mock -CommandName Test-Path -MockWith { $false } + + { Test-TargetResource -Name 'server.vhdx' -Path 'C:\VMs' -Type 'Fixed' -MaximumSizeBytes 1GB } | + Should Throw 'C:\VMs does not exists' + } + } + + Context 'Vhd exists' { + BeforeEach { + Mock -CommandName Test-Path -MockWith { $true } + Mock -CommandName GetNameWithExtension -MockWith { 'server.vhdx' } + Mock -CommandName Test-VHD -MockWith { $true } + } + + It 'Should not throw' { + { Test-TargetResource -Name 'server.vhdx' -Path 'C:\VMs' -Type 'Fixed' -MaximumSizeBytes 1GB } | + Should not Throw + } + + It 'Should return a boolean and it should be true' { + $testResult = Test-TargetResource -Name 'server.vhdx' -Path 'C:\VMs' -Type 'Fixed' -MaximumSizeBytes 1GB + $testResult | Should BeOfType bool + $testResult -eq $true | Should Be $true + } + } + + Context 'Vhd does not exist' { + BeforeEach { + Mock -CommandName Test-Path -MockWith { $true } + Mock -CommandName GetNameWithExtension -MockWith { 'server.vhdx' } + Mock -CommandName Test-VHD -MockWith { $false } + } + + It 'Should not throw' { + { Test-TargetResource -Name 'server.vhdx' -Path 'C:\VMs' -Type 'Fixed' -MaximumSizeBytes 1GB } | + Should not Throw + } + + It 'Should return a boolean and it should be false' { + $testResult = Test-TargetResource -Name 'server.vhdx' -Path 'C:\VMs' -Type 'Fixed' -MaximumSizeBytes 1GB + $testResult | Should BeOfType bool + $testResult -eq $true | Should Be $false + } + } + } + + Describe 'MSFT_xVHD\Set-TargetResource' { + # Create an empty function to be able to mock the missing Hyper-V cmdlet + function Get-VHD + { + + } + + function Set-VHD + { + + } + + function Resize-VHD + { + + } + + function New-VHD + { + + } + + Context 'Ensure is Absent' { + Mock -CommandName Test-Path -MockWith { $true } + Mock -CommandName Remove-Item + Mock -CommandName GetNameWithExtension -MockWith { 'server.vhdx' } + + It 'Should remove when Ensure is Absent and vhdx exists' { + $null = Set-TargetResource -Name 'server.vhdx' -Path 'TestDrive:\' -Ensure 'Absent' + Assert-MockCalled -CommandName Remove-Item -Times 1 -Exactly + } + } + + Context 'Ensure is Present' { + BeforeEach { + Mock -CommandName Get-VHD -MockWith { + [pscustomobject]@{ + Path = 'server.vhdx' + ParentPath = 'c:\boguspath\server.vhdx' + Size = 1073741824 + Type = 'Differencing' + } + } + + Mock -CommandName Set-VHD + Mock -CommandName Resize-VHD + Mock -CommandName GetNameWithExtension -MockWith { 'server.vhdx' } + Mock -CommandName New-VHD -MockWith { } + } + + It 'Should Create a VHD when Ensure is present and no VHD exists yet for non Differencing disk' { + Mock -CommandName Get-VHD -MockWith { throw } + + $null = Set-TargetResource -Name 'server.vhdx' -Path 'TestDrive:\' -Ensure 'Present' + Assert-MockCalled -CommandName New-VHD -Exactly -Times 1 -Scope It + } + + It 'Should Create a VHD when Ensure is present and no VHD exists yet for Differencing disk' { + Mock -CommandName Get-VHD -MockWith { throw } + + $null = Set-TargetResource -Name 'server.vhdx' -Path 'TestDrive:\' -Ensure 'Present' -ParentPath 'c:\boguspath\server.vhdx' -Type 'Differencing' + Assert-MockCalled -CommandName New-VHD -Exactly -Times 1 -Scope It + } + + It 'Should resize a VHD which has a different size as intended' { + $null = Set-TargetResource -Name 'server.vhdx' -Path 'TestDrive:\' -MaximumSizeBytes 2GB -Ensure 'Present' + Assert-MockCalled -CommandName Resize-VHD -Exactly -Times 1 -Scope It + } + + It 'Should update the parentpath if it is different from intent' { + $null = Set-TargetResource -Name 'server.vhdx' -Path 'TestDrive:\' -ParentPath 'c:\boguspath2\server.vhdx' -Ensure 'Present' + Assert-MockCalled -CommandName Set-VHD -Exactly -Times 1 -Scope It + } + } + } + } +} +finally +{ + Invoke-TestCleanup +} diff --git a/deployment/dsc/azshcihost/xHyper-V/3.17.0.0/Tests/Unit/MSFT_xVMDvdDrive.tests.ps1 b/deployment/dsc/azshcihost/xHyper-V/3.17.0.0/Tests/Unit/MSFT_xVMDvdDrive.tests.ps1 new file mode 100644 index 0000000..a194462 --- /dev/null +++ b/deployment/dsc/azshcihost/xHyper-V/3.17.0.0/Tests/Unit/MSFT_xVMDvdDrive.tests.ps1 @@ -0,0 +1,881 @@ +$script:DSCModuleName = 'xHyper-V' +$script:DSCResourceName = 'MSFT_xVMDvdDrive' + +#region HEADER +# Unit Test Template Version: 1.1.0 +[String] $script:moduleRoot = Split-Path -Parent (Split-Path -Parent $PSScriptRoot) +if ( (-not (Test-Path -Path (Join-Path -Path $script:moduleRoot -ChildPath 'DSCResource.Tests'))) -or ` + (-not (Test-Path -Path (Join-Path -Path $script:moduleRoot -ChildPath 'DSCResource.Tests\TestHelper.psm1'))) ) +{ + & git @('clone','https://github.com/PowerShell/DscResource.Tests.git',(Join-Path -Path $script:moduleRoot -ChildPath '\DSCResource.Tests\')) +} + +Import-Module (Join-Path -Path $script:moduleRoot -ChildPath 'DSCResource.Tests\TestHelper.psm1') -Force +$TestEnvironment = Initialize-TestEnvironment ` + -DSCModuleName $script:DSCModuleName ` + -DSCResourceName $script:DSCResourceName ` + -TestType Unit +#endregion HEADER + +# Begin Testing +try +{ + #region Pester Tests + InModuleScope $script:DSCResourceName { + # Function to create a exception object for testing output exceptions + function Get-InvalidArgumentError + { + [CmdletBinding()] + param + ( + [Parameter(Mandatory)] + [ValidateNotNullOrEmpty()] + [System.String] + $ErrorId, + + [Parameter(Mandatory)] + [ValidateNotNullOrEmpty()] + [System.String] + $ErrorMessage + ) + + $exception = New-Object -TypeName System.ArgumentException ` + -ArgumentList $ErrorMessage + $errorCategory = [System.Management.Automation.ErrorCategory]::InvalidArgument + $errorRecord = New-Object -TypeName System.Management.Automation.ErrorRecord ` + -ArgumentList $exception, $ErrorId, $errorCategory, $null + return $errorRecord + } # end function Get-InvalidArgumentError + + #region Pester Test Initialization + + $script:VMName = 'HyperVUnitTestsVM' + $script:TestISOPath = 'd:\test\test.iso' + + $script:splatGetDvdDrive = @{ + VMName = $script:VMName + ControllerNumber = 0 + ControllerLocation = 1 + Verbose = $True + } + $script:splatAddDvdDriveNoPath = @{ + VMName = $script:VMName + ControllerNumber = 0 + ControllerLocation = 1 + Path = '' + Ensure = 'Present' + Verbose = $True + } + $script:splatAddDvdDrive = @{ + VMName = $script:VMName + ControllerNumber = 0 + ControllerLocation = 1 + Path = $script:TestISOPath + Ensure = 'Present' + Verbose = $True + } + $script:splatRemoveDvdDrive = @{ + VMName = $script:VMName + ControllerNumber = 0 + ControllerLocation = 1 + Ensure = 'Absent' + Verbose = $True + } + $script:mockGetModule = [pscustomobject] @{ + Name = 'Hyper-V' + } + $script:mockGetVM = [pscustomobject] @{ + Name = $VMName + } + $script:mockGetVMScsiController = [pscustomobject] @{ + VMName = $VMName + } + $script:mockGetVMHardDiskDrive = [pscustomobject] @{ + VMName = $VMName + } + $script:mockNoDvdDrive = @{ + VMName = $script:VMName + ControllerNumber = 0 + ControllerLocation = 1 + Ensure = 'Absent' + } + $script:mockDvdDriveWithPath = @{ + VMName = $script:VMName + ControllerNumber = 0 + ControllerLocation = 1 + Path = $script:TestISOPath + Ensure = 'Present' + } + $script:mockDvdDriveWithDiffPath = @{ + VMName = $script:VMName + ControllerNumber = 0 + ControllerLocation = 1 + Path = 'd:\diff\diff.iso' + Ensure = 'Present' + } + #endregion + + #region Function Get-TargetResource + Describe 'MSFT_xVMDvdDrive\Get-TargetResource' { + #region VM Functions + function Get-VM { + Param + ( + [String] + $Name + ) + } + + function Get-VMScsiController { + Param + ( + [String] + $VMName, + + [Uint32] + $ControllerNumber + ) + } + + function Get-VMIdeController { + Param + ( + [String] + $VMName, + + [Uint32] + $ControllerNumber + ) + } + + function Get-VMHardDiskDrive { + Param + ( + [String] + $VMName, + + [Uint32] + $ControllerNumber, + + [Uint32] + $ControllerLocation + ) + } + + function Get-VMDvdDrive { + Param + ( + [String] + $VMName + ) + } + #endregion + + Context 'DVD Drive does not exist' { + # Verifiable (should be called) mocks + Mock ` + -CommandName Test-ParameterValid ` + -Verifiable + + Mock ` + -CommandName Get-VMDvdDrive ` + -MockWith {} ` + -ParameterFilter { $VMName -eq $script:splatGetDvdDrive.VMName } ` + -Verifiable + + It 'should not throw exception' { + { + $script:resource = Get-TargetResource @script:splatGetDvdDrive + } | Should Not Throw + } + + It 'should return expected values' { + $script:resource.VMName | Should Be $script:splatGetDvdDrive.VMName + $script:resource.ControllerNumber | Should Be $script:splatGetDvdDrive.ControllerNumber + $script:resource.ControllerLocation | Should Be $script:splatGetDvdDrive.ControllerLocation + $script:resource.Path | Should BeNullOrEmpty + $script:resource.Ensure | Should Be 'Absent' + } + + It 'all the get mocks should be called' { + Assert-VerifiableMock + Assert-MockCalled -CommandName Test-ParameterValid -Exactly 1 + Assert-MockCalled -CommandName Get-VMDvdDrive -Exactly 1 ` + -ParameterFilter { $VMName -eq $script:splatGetDvdDrive.VMName } + } + } + + Context 'DVD Drive exists, but has empty path' { + # Verifiable (should be called) mocks + Mock ` + -CommandName Test-ParameterValid ` + -Verifiable + + Mock ` + -CommandName Get-VMDvdDrive ` + -MockWith { $script:splatAddDvdDriveNoPath } ` + -ParameterFilter { $VMName -eq $script:splatGetDvdDrive.VMName } ` + -Verifiable + + It 'should not throw exception' { + { + $script:resource = Get-TargetResource @script:splatGetDvdDrive + } | Should Not Throw + } + + It 'should return expected values' { + $script:resource.VMName | Should Be $script:splatGetDvdDrive.VMName + $script:resource.ControllerNumber | Should Be $script:splatGetDvdDrive.ControllerNumber + $script:resource.ControllerLocation | Should Be $script:splatGetDvdDrive.ControllerLocation + $script:resource.Path | Should BeNullOrEmpty + $script:resource.Ensure | Should Be 'Present' + } + + It 'all the get mocks should be called' { + Assert-VerifiableMock + Assert-MockCalled -CommandName Test-ParameterValid -Exactly 1 + Assert-MockCalled -CommandName Get-VMDvdDrive -Exactly 1 ` + -ParameterFilter { $VMName -eq $script:splatGetDvdDrive.VMName } + } + } + + Context 'DVD Drive exists, and has a test ISO path' { + # Verifiable (should be called) mocks + Mock ` + -CommandName Test-ParameterValid ` + -Verifiable + + Mock ` + -CommandName Get-VMDvdDrive ` + -MockWith { $script:splatAddDvdDrive } ` + -ParameterFilter { $VMName -eq $script:splatGetDvdDrive.VMName } ` + -Verifiable + + It 'should not throw exception' { + { + $script:resource = Get-TargetResource @script:splatGetDvdDrive + } | Should Not Throw + } + + It 'should return expected values' { + $script:resource.VMName | Should Be $script:splatGetDvdDrive.VMName + $script:resource.ControllerNumber | Should Be $script:splatGetDvdDrive.ControllerNumber + $script:resource.ControllerLocation | Should Be $script:splatGetDvdDrive.ControllerLocation + $script:resource.Path | Should Be $script:TestISOPath + $script:resource.Ensure | Should Be 'Present' + } + + It 'all the get mocks should be called' { + Assert-VerifiableMock + Assert-MockCalled -CommandName Test-ParameterValid -Exactly 1 + Assert-MockCalled -CommandName Get-VMDvdDrive -Exactly 1 ` + -ParameterFilter { $VMName -eq $script:splatGetDvdDrive.VMName } + } + } + } + #endregion + + #region Function Set-TargetResource + Describe 'MSFT_xVMDvdDrive\Set-TargetResource' { + #region VM Functions + function Add-VMDvdDrive { + Param + ( + [String] + $VMName, + + [Uint32] + $ControllerNumber, + + [Uint32] + $ControllerLocation, + + [String] + $Path + ) + } + + function Set-VMDvdDrive { + Param + ( + [String] + $VMName, + + [Uint32] + $ControllerNumber, + + [Uint32] + $ControllerLocation, + + [String] + $Path + ) + } + + function Remove-VMDvdDrive { + Param + ( + [String] + $VMName, + + [Uint32] + $ControllerNumber, + + [Uint32] + $ControllerLocation + ) + } + #endregion + + Context 'DVD Drive does not exist but should' { + # Verifiable (should be called) mocks + Mock ` + -CommandName Get-TargetResource ` + -MockWith { $script:mockNoDvdDrive } ` + -Verifiable + + Mock ` + -CommandName Add-VMDvdDrive ` + -ParameterFilter { $VMName -eq $script:splatAddDvdDriveNoPath.VMName } ` + -Verifiable + + # Mocks that should not be called + Mock -CommandName Set-VMDvdDrive + Mock -CommandName Remove-VMDvdDrive + + It 'should not throw exception' { + { Set-TargetResource @script:splatAddDvdDriveNoPath } | Should Not Throw + } + + It 'all the get mocks should be called' { + Assert-VerifiableMock + Assert-MockCalled -CommandName Get-TargetResource -Exactly 1 + Assert-MockCalled -CommandName Add-VMDvdDrive -Exactly 1 ` + -ParameterFilter { $VMName -eq $script:splatAddDvdDriveNoPath.VMName } + Assert-MockCalled -CommandName Set-VMDvdDrive -Exactly 0 + Assert-MockCalled -CommandName Remove-VMDvdDrive -Exactly 0 + } + } + + Context 'DVD Drive does exist and should, path matches' { + # Verifiable (should be called) mocks + Mock ` + -CommandName Get-TargetResource ` + -MockWith { $script:mockDvdDriveWithPath } ` + -Verifiable + + # Mocks that should not be called + Mock -CommandName Add-VMDvdDrive + Mock -CommandName Set-VMDvdDrive + Mock -CommandName Remove-VMDvdDrive + + It 'should not throw exception' { + { Set-TargetResource @script:splatAddDvdDrive } | Should Not Throw + } + + It 'all the get mocks should be called' { + Assert-VerifiableMock + Assert-MockCalled -CommandName Get-TargetResource -Exactly 1 + Assert-MockCalled -CommandName Add-VMDvdDrive -Exactly 0 + Assert-MockCalled -CommandName Set-VMDvdDrive -Exactly 0 + Assert-MockCalled -CommandName Remove-VMDvdDrive -Exactly 0 + } + } + + Context 'DVD Drive does exist and should, path does not match' { + # Verifiable (should be called) mocks + Mock ` + -CommandName Get-TargetResource ` + -MockWith { $script:mockDvdDriveWithDiffPath } ` + -Verifiable + + Mock ` + -CommandName Set-VMDvdDrive ` + -ParameterFilter { $VMName -eq $script:splatAddDvdDrive.VMName } ` + -Verifiable + + # Mocks that should not be called + Mock -CommandName Add-VMDvdDrive + Mock -CommandName Remove-VMDvdDrive + + It 'should not throw exception' { + { Set-TargetResource @script:splatAddDvdDrive } | Should Not Throw + } + + It 'all the get mocks should be called' { + Assert-VerifiableMock + Assert-MockCalled -CommandName Get-TargetResource -Exactly 1 + Assert-MockCalled -CommandName Add-VMDvdDrive -Exactly 0 + Assert-MockCalled -CommandName Set-VMDvdDrive -Exactly 1 ` + -ParameterFilter { $VMName -eq $script:splatAddDvdDrive.VMName } + Assert-MockCalled -CommandName Remove-VMDvdDrive -Exactly 0 + } + } + + Context 'DVD Drive exists and should not' { + # Verifiable (should be called) mocks + Mock ` + -CommandName Get-TargetResource ` + -MockWith { $script:mockDvdDriveWithPath } ` + -Verifiable + + Mock ` + -CommandName Remove-VMDvdDrive ` + -ParameterFilter { $VMName -eq $script:splatRemoveDvdDrive.VMName } ` + -Verifiable + + # Mocks that should not be called + Mock -CommandName Add-VMDvdDrive + Mock -CommandName Set-VMDvdDrive + + It 'should not throw exception' { + { Set-TargetResource @script:splatRemoveDvdDrive } | Should Not Throw + } + + It 'all the get mocks should be called' { + Assert-VerifiableMock + Assert-MockCalled -CommandName Get-TargetResource -Exactly 1 + Assert-MockCalled -CommandName Add-VMDvdDrive -Exactly 0 + Assert-MockCalled -CommandName Set-VMDvdDrive -Exactly 0 + Assert-MockCalled -CommandName Remove-VMDvdDrive -Exactly 1 ` + -ParameterFilter { $VMName -eq $script:splatRemoveDvdDrive.VMName } + } + } + + Context 'DVD Drive does not exist and should not' { + # Verifiable (should be called) mocks + Mock ` + -CommandName Get-TargetResource ` + -MockWith { $script:mockNoDvdDrive } ` + -Verifiable + + # Mocks that should not be called + Mock -CommandName Add-VMDvdDrive + Mock -CommandName Set-VMDvdDrive + Mock -CommandName Remove-VMDvdDrive + + It 'should not throw exception' { + { Set-TargetResource @script:splatRemoveDvdDrive } | Should Not Throw + } + + It 'all the get mocks should be called' { + Assert-VerifiableMock + Assert-MockCalled -CommandName Get-TargetResource -Exactly 1 + Assert-MockCalled -CommandName Add-VMDvdDrive -Exactly 0 + Assert-MockCalled -CommandName Set-VMDvdDrive -Exactly 0 + Assert-MockCalled -CommandName Remove-VMDvdDrive -Exactly 0 + } + } + } + #endregion + + #region Function Test-TargetResource + Describe 'MSFT_xVMDvdDrive\Test-TargetResource' { + Context 'DVD Drive does not exist but should' { + # Verifiable (should be called) mocks + Mock ` + -CommandName Get-TargetResource ` + -MockWith { $script:mockNoDvdDrive } ` + -Verifiable + + It 'should return false' { + Test-TargetResource @script:splatAddDvdDriveNoPath | Should Be $False + } + + It 'all the get mocks should be called' { + Assert-VerifiableMock + Assert-MockCalled -CommandName Get-TargetResource -Exactly 1 + } + } + + Context 'DVD Drive does exist and should, path matches' { + # Verifiable (should be called) mocks + Mock ` + -CommandName Get-TargetResource ` + -MockWith { $script:mockDvdDriveWithPath } ` + -Verifiable + + It 'should return true' { + Test-TargetResource @script:splatAddDvdDrive | Should Be $True + } + + It 'all the get mocks should be called' { + Assert-VerifiableMock + Assert-MockCalled -CommandName Get-TargetResource -Exactly 1 + } + } + + Context 'DVD Drive does exist and should, path does not match' { + # Verifiable (should be called) mocks + Mock ` + -CommandName Get-TargetResource ` + -MockWith { $script:mockDvdDriveWithDiffPath } ` + -Verifiable + + It 'should return false' { + Test-TargetResource @script:splatAddDvdDrive | Should Be $False + } + + It 'all the get mocks should be called' { + Assert-VerifiableMock + Assert-MockCalled -CommandName Get-TargetResource -Exactly 1 + } + } + + Context 'DVD Drive exists and should not' { + # Verifiable (should be called) mocks + Mock ` + -CommandName Get-TargetResource ` + -MockWith { $script:mockDvdDriveWithPath } ` + -Verifiable + + It 'should return false' { + Test-TargetResource @script:splatRemoveDvdDrive | Should Be $False + } + + It 'all the get mocks should be called' { + Assert-VerifiableMock + Assert-MockCalled -CommandName Get-TargetResource -Exactly 1 + } + } + + Context 'DVD Drive does not exist and should not' { + # Verifiable (should be called) mocks + Mock ` + -CommandName Get-TargetResource ` + -MockWith { $script:mockNoDvdDrive } ` + -Verifiable + + It 'should return true' { + Test-TargetResource @script:splatRemoveDvdDrive | Should Be $True + } + + It 'all the get mocks should be called' { + Assert-VerifiableMock + Assert-MockCalled -CommandName Get-TargetResource -Exactly 1 + } + } + } + #endregion + + #region Function Test-ParameterValid + Describe 'MSFT_xVMDvdDrive\Test-ParameterValid' { + #region VM Functions + function Get-VM { + Param + ( + [String] + $Name + ) + } + + function Get-VMScsiController { + Param + ( + [String] + $VMName, + + [Uint32] + $ControllerNumber + ) + } + + function Get-VMIdeController { + Param + ( + [String] + $VMName, + + [Uint32] + $ControllerNumber + ) + } + + function Get-VMHardDiskDrive { + Param + ( + [String] + $VMName, + + [Uint32] + $ControllerNumber, + + [Uint32] + $ControllerLocation + ) + } + + function Get-VMDvdDrive { + Param + ( + [String] + $VMName + ) + } + #endregion + + Context 'Hyper-V Module is not available' { + # Verifiable (should be called) mocks + Mock ` + -CommandName Get-Module ` + -Verifiable + + # Mocks that should not be called + Mock -CommandName Get-VM + Mock -CommandName Get-VMScsiController + Mock -CommandName Get-VMIdeController + Mock -CommandName Get-VMHardDiskDrive + + It 'should throw exception' { + $errorRecord = Get-InvalidArgumentError ` + -ErrorId 'RoleMissingError' ` + -ErrorMessage ($LocalizedData.RoleMissingError -f ` + 'Hyper-V') + + { Test-ParameterValid @script:splatAddDvdDriveNoPath } | Should Throw $errorRecord + } + + It 'all the get mocks should be called' { + Assert-VerifiableMock + Assert-MockCalled -CommandName Get-Module -Exactly 1 + } + } + + Context 'VM does not exist' { + # Verifiable (should be called) mocks + Mock ` + -CommandName Get-Module ` + -MockWith { $script:mockGetModule } ` + -Verifiable + + Mock ` + -CommandName Get-VM ` + -MockWith { Throw } ` + -ParameterFilter { $VMName -eq $script:splatAddDvdDriveNoPath.VMName } ` + -Verifiable + + # Mocks that should not be called + Mock -CommandName Get-VMScsiController + Mock -CommandName Get-VMIdeController + Mock -CommandName Get-VMHardDiskDrive + + It 'should throw exception' { + { Test-ParameterValid @script:splatAddDvdDriveNoPath } | Should Throw + } + + It 'all the get mocks should be called' { + Assert-VerifiableMock + Assert-MockCalled -CommandName Get-Module -Exactly 1 + Assert-MockCalled -CommandName Get-VM -Exactly 1 ` + -ParameterFilter { $VMName -eq $script:splatAddDvdDriveNoPath.VMName } + } + } + + Context 'VM exists, controller does not exist' { + # Verifiable (should be called) mocks + Mock ` + -CommandName Get-Module ` + -MockWith { $script:mockGetModule } ` + -Verifiable + + Mock ` + -CommandName Get-VM ` + -MockWith { $script:mockGetVM } ` + -ParameterFilter { $VMName -eq $script:splatAddDvdDriveNoPath.VMName } ` + -Verifiable + + Mock ` + -CommandName Get-VMScsiController ` + -ParameterFilter { $VMName -eq $script:splatAddDvdDriveNoPath.VMName } ` + -Verifiable + + Mock ` + -CommandName Get-VMIdeController ` + -ParameterFilter { $VMName -eq $script:splatAddDvdDriveNoPath.VMName } ` + -Verifiable + + # Mocks that should not be called + Mock -CommandName Get-VMHardDiskDrive + + It 'should throw exception' { + $errorRecord = Get-InvalidArgumentError ` + -ErrorId 'VMControllerDoesNotExistError' ` + -ErrorMessage ($LocalizedData.VMControllerDoesNotExistError -f ` + $script:VMName,0) + + { Test-ParameterValid @script:splatAddDvdDriveNoPath } | Should Throw $errorRecord + } + + It 'all the get mocks should be called' { + Assert-VerifiableMock + Assert-MockCalled -CommandName Get-Module -Exactly 1 + Assert-MockCalled -CommandName Get-VM -Exactly 1 ` + -ParameterFilter { $VMName -eq $script:splatAddDvdDriveNoPath.VMName } + Assert-MockCalled -CommandName Get-VMScsiController -Exactly 1 ` + -ParameterFilter { $VMName -eq $script:splatAddDvdDriveNoPath.VMName } + Assert-MockCalled -CommandName Get-VMIdeController -Exactly 1 ` + -ParameterFilter { $VMName -eq $script:splatAddDvdDriveNoPath.VMName } + } + } + + Context 'VM exists, SCSI contrller exists, HD assigned' { + # Verifiable (should be called) mocks + Mock ` + -CommandName Get-Module ` + -MockWith { $script:mockGetModule } ` + -Verifiable + + Mock ` + -CommandName Get-VM ` + -MockWith { $script:mockGetVM } ` + -ParameterFilter { $VMName -eq $script:splatAddDvdDriveNoPath.VMName } ` + -Verifiable + + Mock ` + -CommandName Get-VMScsiController ` + -MockWith { $script:mockGetVMScsiController } ` + -ParameterFilter { $VMName -eq $script:splatAddDvdDriveNoPath.VMName } ` + -Verifiable + + Mock ` + -CommandName Get-VMHardDiskDrive ` + -MockWith { $script:mockGetVMHardDiskDrive } ` + -ParameterFilter { $VMName -eq $script:splatAddDvdDriveNoPath.VMName } ` + -Verifiable + + # Mocks that should not be called + Mock -CommandName Get-VMIdeController + + It 'should throw exception' { + $errorRecord = Get-InvalidArgumentError ` + -ErrorId 'ControllerConflictError' ` + -ErrorMessage ($LocalizedData.ControllerConflictError -f ` + $script:VMName,0,1) + + { Test-ParameterValid @script:splatAddDvdDriveNoPath } | Should Throw $errorRecord + } + + It 'all the get mocks should be called' { + Assert-VerifiableMock + Assert-MockCalled -CommandName Get-Module -Exactly 1 + Assert-MockCalled -CommandName Get-VM -Exactly 1 ` + -ParameterFilter { $VMName -eq $script:splatAddDvdDriveNoPath.VMName } + Assert-MockCalled -CommandName Get-VMScsiController -Exactly 1 ` + -ParameterFilter { $VMName -eq $script:splatAddDvdDriveNoPath.VMName } + Assert-MockCalled -CommandName Get-VMHardDiskDrive -Exactly 1 ` + -ParameterFilter { $VMName -eq $script:splatAddDvdDriveNoPath.VMName } + } + } + + Context 'VM exists, SCSI contrller exists, HD not assigned, Path invalid' { + # Verifiable (should be called) mocks + Mock ` + -CommandName Get-Module ` + -MockWith { $script:mockGetModule } ` + -Verifiable + + Mock ` + -CommandName Get-VM ` + -MockWith { $script:mockGetVM } ` + -ParameterFilter { $VMName -eq $script:splatAddDvdDrive.VMName } ` + -Verifiable + + Mock ` + -CommandName Get-VMScsiController ` + -MockWith { $script:mockGetVMScsiController } ` + -ParameterFilter { $VMName -eq $script:splatAddDvdDrive.VMName } ` + -Verifiable + + Mock ` + -CommandName Get-VMHardDiskDrive ` + -ParameterFilter { $VMName -eq $script:splatAddDvdDrive.VMName } ` + -Verifiable + + Mock ` + -CommandName Test-Path ` + -MockWith { $False } ` + -Verifiable + + # Mocks that should not be called + Mock -CommandName Get-VMIdeController + + It 'should throw exception' { + $errorRecord = Get-InvalidArgumentError ` + -ErrorId 'PathDoesNotExistError' ` + -ErrorMessage ($LocalizedData.PathDoesNotExistError -f ` + $script:TestISOPath) + + { Test-ParameterValid @script:splatAddDvdDrive } | Should Throw $errorRecord + } + + It 'all the get mocks should be called' { + Assert-VerifiableMock + Assert-MockCalled -CommandName Get-Module -Exactly 1 + Assert-MockCalled -CommandName Get-VM -Exactly 1 ` + -ParameterFilter { $VMName -eq $script:splatAddDvdDrive.VMName } + Assert-MockCalled -CommandName Get-VMScsiController -Exactly 1 ` + -ParameterFilter { $VMName -eq $script:splatAddDvdDrive.VMName } + Assert-MockCalled -CommandName Get-VMHardDiskDrive -Exactly 1 ` + -ParameterFilter { $VMName -eq $script:splatAddDvdDrive.VMName } + Assert-MockCalled -CommandName Test-Path -Exactly 1 + } + } + + Context 'VM exists, SCSI contrller exists, HD not assigned, Path Valid' { + # Verifiable (should be called) mocks + Mock ` + -CommandName Get-Module ` + -MockWith { $script:mockGetModule } ` + -Verifiable + + Mock ` + -CommandName Get-VM ` + -MockWith { $script:mockGetVM } ` + -ParameterFilter { $VMName -eq $script:splatAddDvdDrive.VMName } ` + -Verifiable + + Mock ` + -CommandName Get-VMScsiController ` + -MockWith { $script:mockGetVMScsiController } ` + -ParameterFilter { $VMName -eq $script:splatAddDvdDrive.VMName } ` + -Verifiable + + Mock ` + -CommandName Get-VMHardDiskDrive ` + -ParameterFilter { $VMName -eq $script:splatAddDvdDrive.VMName } ` + -Verifiable + + Mock ` + -CommandName Test-Path ` + -MockWith { $True } ` + -Verifiable + + # Mocks that should not be called + Mock -CommandName Get-VMIdeController + + It 'should not throw exception' { + Test-ParameterValid @script:splatAddDvdDrive | Should Be $True + } + + It 'all the get mocks should be called' { + Assert-VerifiableMock + Assert-MockCalled -CommandName Get-Module -Exactly 1 + Assert-MockCalled -CommandName Get-VM -Exactly 1 ` + -ParameterFilter { $VMName -eq $script:splatAddDvdDrive.VMName } + Assert-MockCalled -CommandName Get-VMScsiController -Exactly 1 ` + -ParameterFilter { $VMName -eq $script:splatAddDvdDrive.VMName } + Assert-MockCalled -CommandName Get-VMHardDiskDrive -Exactly 1 ` + -ParameterFilter { $VMName -eq $script:splatAddDvdDrive.VMName } + Assert-MockCalled -CommandName Test-Path -Exactly 1 + } + } + } + #endregion + } +} +finally +{ + #region FOOTER + Restore-TestEnvironment -TestEnvironment $TestEnvironment + #endregion +} diff --git a/deployment/dsc/azshcihost/xHyper-V/3.17.0.0/Tests/Unit/MSFT_xVMHardDiskDrive.Tests.ps1 b/deployment/dsc/azshcihost/xHyper-V/3.17.0.0/Tests/Unit/MSFT_xVMHardDiskDrive.Tests.ps1 new file mode 100644 index 0000000..9ebefdb --- /dev/null +++ b/deployment/dsc/azshcihost/xHyper-V/3.17.0.0/Tests/Unit/MSFT_xVMHardDiskDrive.Tests.ps1 @@ -0,0 +1,381 @@ +$script:DSCModuleName = 'xHyper-V' +$script:DSCResourceName = 'MSFT_xVMHardDiskDrive' + +#region HEADER + +# Unit Test Template Version: 1.2.0 +$script:moduleRoot = Split-Path -Parent (Split-Path -Parent $PSScriptRoot) +if ( (-not (Test-Path -Path (Join-Path -Path $script:moduleRoot -ChildPath 'DSCResource.Tests'))) -or ` + (-not (Test-Path -Path (Join-Path -Path $script:moduleRoot -ChildPath 'DSCResource.Tests\TestHelper.psm1'))) ) +{ + & git @('clone','https://github.com/PowerShell/DscResource.Tests.git',(Join-Path -Path $script:moduleRoot -ChildPath '\DSCResource.Tests\')) +} + +Import-Module -Name (Join-Path -Path $script:moduleRoot -ChildPath (Join-Path -Path 'DSCResource.Tests' -ChildPath 'TestHelper.psm1')) -Force + +$TestEnvironment = Initialize-TestEnvironment ` + -DSCModuleName $script:DSCModuleName ` + -DSCResourceName $script:DSCResourceName ` + -TestType Unit + +#endregion HEADER + +function Invoke-TestSetup { + +} + +function Invoke-TestCleanup { + Restore-TestEnvironment -TestEnvironment $TestEnvironment +} + +# Begin Testing +try +{ + Invoke-TestSetup + + InModuleScope $script:DSCResourceName { + + $testVMName = 'UnitTestVM' + $testHardDiskPath = 'TestDrive:\{0}.vhdx' -f $testVMName + + Describe 'MSFT_xVMHardDiskDrive\Get-TargetResource' { + + $stubHardDiskDrive = @{ + VMName = $testVMName + Path = $testHardDiskPath + ControllerLocation = 0 + ControllerNumber = 0 + ControllerType = 'SCSI' + } + + # Guard mocks + Mock Assert-Module { } + + function Get-VMHardDiskDrive { + [CmdletBinding()] + param + ( + [System.String] + $VMName + ) + } + + It 'Should return a [System.Collections.Hashtable] object type' { + Mock Get-VMHardDiskDrive { return $stubHardDiskDrive } + + $result = Get-TargetResource -VMName $testVMName -Path $testhardDiskPath + + $result -is [System.Collections.Hashtable] | Should Be $true + } + + It 'Should return "Present" when hard disk is attached' { + Mock Get-VMHardDiskDrive { return $stubHardDiskDrive } + + $result = Get-TargetResource -VMName $testVMName -Path $testhardDiskPath + + $result.Ensure | Should Be 'Present' + } + + It 'Should return "Absent" when hard disk is not attached' { + Mock Get-VMHardDiskDrive { } + + $result = Get-TargetResource -VMName $testVMName -Path $testhardDiskPath + + $result.Ensure | Should Be 'Absent' + } + + It 'Should assert Hyper-V module is installed' { + Mock Assert-Module { } + Mock Get-VMHardDiskDrive { return $stubHardDiskDrive } + + $null = Get-TargetResource -VMName $testVMName -Path $testhardDiskPath + + Assert-MockCalled Assert-Module -ParameterFilter { $Name -eq 'Hyper-V' } -Scope It + } + } # descrive Get-TargetResource + + Describe 'MSFT_xVMHardDiskDrive\Test-TargetResource' { + + # Guard mocks + Mock Assert-Module { } + + function Get-VMHardDiskDrive { + [CmdletBinding()] + param + ( + [System.String] + $VMName + ) + } + + $stubTargetResource = @{ + VMName = $testVMName + Path = $testHardDiskPath + ControllerType = 'SCSI' + ControllerNumber = 0 + ControllerLocation = 0 + Ensure = 'Present' + } + + It 'Should return a [System.Boolean] object type' { + Mock Get-TargetResource { return $stubTargetResource } + + $result = Test-TargetResource -VMName $testVMName -Path $testHardDiskPath + + $result -is [System.Boolean] | Should Be $true + } + + $parameterNames = @( + 'ControllerNumber', + 'ControllerLocation' + ) + + foreach ($parameterName in $parameterNames) + { + $parameterValue = $stubTargetResource[$parameterName] + $testTargetResourceParams = @{ + VMName = $testVMName + Path = $testHardDiskPath + } + + It "Should pass when parameter '$parameterName' is correct" { + # Pass value verbatim so it should always pass first + $testTargetResourceParams[$parameterName] = $parameterValue + + $result = Test-TargetResource @testTargetResourceParams + + $result | Should Be $true + } + + It "Should fail when parameter '$parameterName' is incorrect" { + # Add one to cause a test failure + $testTargetResourceParams[$parameterName] = $parameterValue + 1 + + $result = Test-TargetResource @testTargetResourceParams + + $result | Should Be $false + } + } + + It "Should pass when parameter 'ControllerType' is correct" { + $testTargetResourceParams = @{ + VMName = $testVMName + Path = $testHardDiskPath + ControllerType = $stubTargetResource['ControllerType'] + } + + $result = Test-TargetResource @testTargetResourceParams + + $result | Should Be $true + } + + It "Should fail when parameter 'ControllerType' is incorrect" { + $testTargetResourceParams = @{ + VMName = $testVMName + Path = $testHardDiskPath + ControllerType = 'IDE' + } + + $result = Test-TargetResource @testTargetResourceParams + + $result | Should Be $false + } + + It "Should pass when parameter 'Ensure' is correct" { + $testTargetResourceParams = @{ + VMName = $testVMName + Path = $testHardDiskPath + Ensure = $stubTargetResource['Ensure'] + } + + $result = Test-TargetResource @testTargetResourceParams + + $result | Should Be $true + } + + It "Should fail when parameter 'Ensure' is incorrect" { + $testTargetResourceParams = @{ + VMName = $testVMName + Path = $testHardDiskPath + Ensure = 'Absent' + } + + $result = Test-TargetResource @testTargetResourceParams + + $result | Should Be $false + } + + It 'Should throw when IDE controller number 2 is specified' { + $testTargetResourceParams = @{ + VMName = $testVMName + Path = $testHardDiskPath + ControllerType = 'IDE' + ControllerNumber = 2 + } + + { Test-TargetResource @testTargetResourceParams } | Should Throw 'not valid' + } + + It 'Should throw when IDE controller location 2 is specified' { + $testTargetResourceParams = @{ + VMName = $testVMName + Path = $testHardDiskPath + ControllerType = 'IDE' + ControllerLocation = 2 + } + + { Test-TargetResource @testTargetResourceParams } | Should Throw 'not valid' + } + } # describe Test-TargetResource + + Describe 'MSFT_xVMHardDiskDrive\Set-TargetResource' { + + function Get-VMHardDiskDrive { + [CmdletBinding()] + param + ( + [Parameter(ValueFromPipeline)] + [System.String] + $VMName, + + [System.String] + $Path, + + [System.String] + $ControllerType, + + [System.Int32] + $ControllerNumber, + + [System.Int32] + $ControllerLocation + ) + } + + function Set-VMHardDiskDrive { + [CmdletBinding()] + param + ( + [Parameter(ValueFromPipeline)] + [System.String] + $VMName, + + [System.String] + $Path, + + [System.String] + $ControllerType, + + [System.Int32] + $ControllerNumber, + + [System.Int32] + $ControllerLocation + ) + } + + function Add-VMHardDiskDrive { + [CmdletBinding()] + param + ( + [Parameter(ValueFromPipeline)] + [System.String] + $VMName, + + [System.String] + $Path, + + [System.String] + $ControllerType, + + [System.Int32] + $ControllerNumber, + + [System.Int32] + $ControllerLocation + ) + } + + function Remove-VMHardDiskDrive { + [CmdletBinding()] + param + ( + [Parameter(ValueFromPipeline)] + [System.String] + $VMName, + + [System.String] + $Path, + + [System.String] + $ControllerType, + + [System.Int32] + $ControllerNumber, + + [System.Int32] + $ControllerLocation + ) + } + + # Guard mocks + Mock Assert-Module { } + Mock Get-VMHardDiskDrive { } + Mock Set-VMHardDiskDrive { } + Mock Add-VMHardDiskDrive { } + Mock Remove-VMHardDiskDrive { } + + $stubHardDiskDrive = @{ + VMName = $testVMName + Path = $testHardDiskPath + ControllerLocation = 0 + ControllerNumber = 0 + ControllerType = 'SCSI' + } + + It 'Should assert Hyper-V module is installed' { + Mock Get-VMHardDiskDrive { return $stubHardDiskDrive } + + $null = Set-TargetResource -VMName $testVMName -Path $testHardDiskPath + + Assert-MockCalled Assert-Module -ParameterFilter { $Name -eq 'Hyper-V' } -Scope It + } + + It 'Should update existing hard disk' { + Mock Get-VMHardDiskDrive { return $stubHardDiskDrive } + + $null = Set-TargetResource -VMName $testVMName -Path $testHardDiskPath + + Assert-MockCalled Set-VMHardDiskDrive -Scope It + } + + It 'Should add hard disk when is not attached' { + Mock Get-VMHardDiskDrive { } + Mock Get-VMHardDiskDrive -ParameterFilter { $PSBoundParameters.ContainsKey('ControllerType') } + + $null = Set-TargetResource -VMName $testVMName -Path $testHardDiskPath + + Assert-MockCalled Add-VMHardDiskDrive -Scope It + } + + It 'Should throw when an existing disk is attached to controller/location' { + Mock Get-VMHardDiskDrive { } + Mock Get-VMHardDiskDrive -ParameterFilter { $PSBoundParameters.ContainsKey('ControllerType') } { return $stubHardDiskDrive } + + { Set-TargetResource -VMName $testVMName -Path $testHardDiskPath } | Should Throw 'disk present' + } + + It 'Should remove attached hard disk when Ensure is "Absent"' { + Mock Get-VMHardDiskDrive { return $stubHardDiskDrive } + + $null = Set-TargetResource -VMName $testVMName -Path $testHardDiskPath -Ensure 'Absent' + + Assert-MockCalled Remove-VMHardDiskDrive -Scope It + } + } # describe Set-TargetResource + } # InModuleScope +} +finally +{ + Invoke-TestCleanup +} diff --git a/deployment/dsc/azshcihost/xHyper-V/3.17.0.0/Tests/Unit/MSFT_xVMHost.tests.ps1 b/deployment/dsc/azshcihost/xHyper-V/3.17.0.0/Tests/Unit/MSFT_xVMHost.tests.ps1 new file mode 100644 index 0000000..7396683 --- /dev/null +++ b/deployment/dsc/azshcihost/xHyper-V/3.17.0.0/Tests/Unit/MSFT_xVMHost.tests.ps1 @@ -0,0 +1,346 @@ +$script:DSCModuleName = 'xHyper-V' +$script:DSCResourceName = 'MSFT_xVMHost' + +#region HEADER + +# Unit Test Template Version: 1.2.0 +$script:moduleRoot = Split-Path -Parent (Split-Path -Parent $PSScriptRoot) +if ( (-not (Test-Path -Path (Join-Path -Path $script:moduleRoot -ChildPath 'DSCResource.Tests'))) -or ` + (-not (Test-Path -Path (Join-Path -Path $script:moduleRoot -ChildPath 'DSCResource.Tests\TestHelper.psm1'))) ) +{ + & git @('clone','https://github.com/PowerShell/DscResource.Tests.git',(Join-Path -Path $script:moduleRoot -ChildPath '\DSCResource.Tests\')) +} + +Import-Module -Name (Join-Path -Path $script:moduleRoot -ChildPath (Join-Path -Path 'DSCResource.Tests' -ChildPath 'TestHelper.psm1')) -Force + +$TestEnvironment = Initialize-TestEnvironment ` + -DSCModuleName $script:DSCModuleName ` + -DSCResourceName $script:DSCResourceName ` + -TestType Unit + +#endregion HEADER + +function Invoke-TestSetup { + +} + +function Invoke-TestCleanup { + Restore-TestEnvironment -TestEnvironment $TestEnvironment +} + +# Begin Testing +try +{ + Invoke-TestSetup + + InModuleScope $script:DSCResourceName { + + $testVMName = 'UnitTestVM' + + Describe 'MSFT_xVMHost\Get-TargetResource' { + + $fakeVMHost = @{ + ResourceMeteringSaveInterval = 60; + } + + # Guard mocks + + function Get-VMHost { } + + It 'Should return a [System.Collections.Hashtable] object type' { + Mock Assert-Module { } + Mock Get-VMHost { return $fakeVMHost } + + $result = Get-TargetResource -IsSingleInstance 'Yes' + + $result -is [System.Collections.Hashtable] | Should Be $true + } + + It 'Should assert Hyper-V module is installed' { + Mock Assert-Module { } + Mock Get-VMHost { return $fakeVMHost } + + $result = Get-TargetResource -IsSingleInstance 'Yes' + + Assert-MockCalled Assert-Module -ParameterFilter { $Name -eq 'Hyper-V' } -Scope It + } + + } # describe Get-TargetResource + + Describe 'MSFT_xVMHost\Test-TargetResource' { + + # Guard mocks + Mock Assert-Module { } + + function Get-VMHost { } + + $fakeTargetResource = @{ + IsSingleInstance = 'Yes'; + EnableEnhancedSessionMode = $true; + FibreChannelWwnn = 'C003FF0000FFFF00'; + FibreChannelWwpnMaximum = 'C003FFFBEAE1FFFF'; + FibreChannelWwpnMinimum = 'C003FFFBEAE10000'; + MacAddressMinimum = '00155D327500'; + MacAddressMaximum = '00155D3275FF'; + MaximumStorageMigrations = 2; + MaximumVirtualMachineMigrations = 2; + NumaSpanningEnabled = $true; + ResourceMeteringSaveIntervalMinute = 60; + UseAnyNetworkForMigration = $false; + VirtualMachinePath ='C:\ProgramData\Microsoft\Windows\Hyper-V'; + VirtualMachineMigrationAuthenticationType = 'CredSSP'; + VirtualMachineMigrationPerformanceOption = 'TCPIP'; + VirtualHardDiskPath = 'C:\Users\Public\Documents\Hyper-V\Virtual Hard Disks'; + VirtualMachineMigrationEnabled = $true + } + + It 'Should return a [System.Boolean] object type' { + Mock Get-TargetResource { return $fakeTargetResource } + + $testTargetResourceParams = @{ + IsSingleInstance = 'Yes'; + EnableEnhancedSessionMode = $fakeTargetResource.EnableEnhancedSessionMode; + VirtualMachineMigrationEnabled = $fakeTargetResource.VirtualMachineMigrationEnabled + } + $result = Test-TargetResource @testTargetResourceParams + + $result -is [System.Boolean] | Should Be $true + } + + It 'Should assert Hyper-V module is installed' { + Mock Get-TargetResource { return $fakeTargetResource } + + $testTargetResourceParams = @{ + IsSingleInstance = 'Yes'; + EnableEnhancedSessionMode = $fakeTargetResource.EnableEnhancedSessionMode; + } + $result = Test-TargetResource @testTargetResourceParams + + Assert-MockCalled Assert-Module -ParameterFilter { $Name -eq 'Hyper-V' } -Scope It + } + + $parameterNames = @( + 'EnableEnhancedSessionMode', + 'FibreChannelWwnn', + 'FibreChannelWwpnMaximum', + 'FibreChannelWwpnMinimum', + 'MacAddressMaximum', + 'MacAddressMinimum', + 'MaximumStorageMigrations', + 'MaximumVirtualMachineMigrations', + 'NumaSpanningEnabled', + 'ResourceMeteringSaveIntervalMinute', + 'UseAnyNetworkForMigration', + 'VirtualHardDiskPath', + 'VirtualMachinePath', + 'VirtualMachineMigrationEnabled' + ) + + # Test each individual parameter value separately + foreach ($parameterName in $parameterNames) + { + $parameterValue = $fakeTargetResource[$parameterName]; + $testTargetResourceParams = @{ + IsSingleInstance = 'Yes'; + } + + # Pass value verbatim so it should always pass first + It "Should pass when parameter '$parameterName' is correct" { + $testTargetResourceParams[$parameterName] = $parameterValue + + $result = Test-TargetResource @testTargetResourceParams; + + $result | Should Be $true; + } + + if ($parameterValue -is [System.Boolean]) { + + # Invert parameter value to cause a test failure + $testTargetResourceParams[$parameterName] = -not $parameterValue + } + elseif ($parameterValue -is [System.String]) { + + # Repeat string to cause a test failure + $testTargetResourceParams[$parameterName] = "$parameterValue$parameterValue" + } + elseif ($parameterValue -is [System.Int32] -or $parameterValue -is [System.Int64]) { + + # Add one to cause a test failure + $testTargetResourceParams[$parameterName] = $parameterValue + 1 + } + + It "Should fail when parameter '$parameterName' is incorrect" { + $result = Test-TargetResource @testTargetResourceParams; + + $result | Should Be $false; + } + } + + It "Should pass when parameter <Parameter> is correct" -TestCases @( + @{ Parameter = 'VirtualMachineMigrationAuthenticationType'; + Value = $fakeTargetResource.VirtualMachineMigrationAuthenticationType; + Expected = $true; } + @{ Parameter = 'VirtualMachineMigrationPerformanceOption'; + Value = $fakeTargetResource.VirtualMachineMigrationPerformanceOption; + Expected = $true; } + @{ Parameter = 'VirtualMachineMigrationEnabled'; + Value = $fakeTargetResource.VirtualMachineMigrationEnabled; + Expected = $true; } + ) -Test { + param ( + [System.String] $Parameter, + [System.Object] $Value, + [System.Boolean] $Expected + ) + + $testTargetResourceParams = @{ + IsSingleInstance = 'Yes'; + $Parameter = $Value; + } + $result = Test-TargetResource @testTargetResourceParams | Should Be $Expected; + } + + It "Should fail when parameter <Parameter> is incorrect" -TestCases @( + @{ Parameter = 'VirtualMachineMigrationAuthenticationType'; + Value = 'Kerberos'; + Expected = $false; } + @{ Parameter = 'VirtualMachineMigrationPerformanceOption'; + Value = 'Compression'; + Expected = $false; } + @{ Parameter = 'VirtualMachineMigrationEnabled'; + Value = $true; + Expected = $true; } + ) -Test { + param + ( + [System.String] $Parameter, + [System.Object] $Value, + [System.Boolean] $Expected + ) + + $testTargetResourceParams = @{ + IsSingleInstance = 'Yes'; + $Parameter = $Value; + } + $result = Test-TargetResource @testTargetResourceParams | Should Be $Expected; + } + + } # describe Test-TargetResource + + Describe 'MSFT_xVMHost\Set-TargetResource' { + + function Get-VMHost { } + function Set-VMHost { param ($ResourceMeteringSaveInterval) } + + function Enable-VMMigration { } + + function Disable-VMMigration { } + + # Guard mocks + Mock Assert-Module { } + Mock Get-VMHost { } + Mock Set-VMHost { } + Mock Enable-VMMigration { } + Mock Disable-VMMigration { } + + It 'Should assert Hyper-V module is installed' { + $setTargetResourceParams = @{ + IsSingleInstance = 'Yes'; + } + + $result = Set-TargetResource @setTargetResourceParams + + Assert-MockCalled Assert-Module -ParameterFilter { $Name -eq 'Hyper-V' } -Scope It + } + + It 'Should call "Set-VMHost" with [System.TimeSpan] object when "ResourceMeteringSaveIntervalMinute" specified' { + $setTargetResourceParams = @{ + IsSingleInstance = 'Yes'; + ResourceMeteringSaveIntervalMinute = 60; + } + $result = Set-TargetResource @setTargetResourceParams + + Assert-MockCalled Set-VMHost -ParameterFilter { $ResourceMeteringSaveInterval -is [System.TimeSpan] } + } + + It 'Should call "Enable-VMMigration" when "VirtualMachineMigrationEnabled" is set to true and computer is domain joined' { + Mock -CommandName 'Get-CimInstance' -MockWith { + [pscustomobject]@{ + PartOfDomain = $true + } + } + + Mock -CommandName 'Write-Error' + + $setTargetResourceParams = @{ + IsSingleInstance = 'Yes' + VirtualMachineMigrationEnabled = $true + } + + $result = Set-TargetResource @setTargetResourceParams + Assert-MockCalled -CommandName Write-Error -Times 0 -Exactly -Scope it + Assert-MockCalled -CommandName Enable-VMMigration -Times 1 -Exactly -Scope it + Assert-MockCalled -CommandName Disable-VMMigration -Times 0 -Exactly -Scope it + } + + It 'Should not call "Enable-VMMigration" and should throw when "VirtualMachineMigrationEnabled" is set to true and computer is not domain joined' { + Mock -CommandName 'Get-CimInstance' -MockWith { + [pscustomobject]@{ + PartOfDomain = $false + } + } + + $setTargetResourceParams = @{ + IsSingleInstance = 'Yes' + VirtualMachineMigrationEnabled = $true + } + + { Set-TargetResource @setTargetResourceParams } | Should -Throw + Assert-MockCalled -CommandName Enable-VMMigration -Times 0 -Exactly -Scope it + Assert-MockCalled -CommandName Disable-VMMigration -Times 0 -Exactly -Scope it + } + + It 'Should call "Disable-VMMigration" when "VirtualMachineMigrationEnabled" is set to false' { + $setTargetResourceParams = @{ + IsSingleInstance = 'Yes' + VirtualMachineMigrationEnabled = $false + } + + $result = Set-TargetResource @setTargetResourceParams + Assert-MockCalled -CommandName Enable-VMMigration -Times 0 -Exactly -Scope it + Assert-MockCalled -CommandName Disable-VMMigration -Times 1 -Exactly -Scope it + } + + It 'Should not call "Disable-VMMigration" or "Enable-VMMigration" when "VirtualMachineMigrationEnabled" is not set' { + $setTargetResourceParams = @{ + IsSingleInstance = 'Yes' + } + + $result = Set-TargetResource @setTargetResourceParams + + Assert-MockCalled -CommandName Enable-VMMigration -Times 0 -Exactly -Scope it + Assert-MockCalled -CommandName Disable-VMMigration -Times 0 -Exactly -Scope it + } + + It 'Should not call "Set-VMHost" when only "VirtualMachineMigrationEnabled" is set' { + $setTargetResourceParams = @{ + IsSingleInstance = 'Yes' + VirtualMachineMigrationEnabled = $false + Verbose = $true + } + + $result = Set-TargetResource @setTargetResourceParams + + Assert-MockCalled -CommandName Enable-VMMigration -Times 0 -Exactly -Scope it + Assert-MockCalled -CommandName Disable-VMMigration -Times 1 -Exactly -Scope it + Assert-MockCalled -CommandName Set-VMHost -Times 0 -Exactly -Scope it + } + + } # describe Set-TargetResource + + } # InModuleScope +} +finally +{ + Invoke-TestCleanup +} diff --git a/deployment/dsc/azshcihost/xHyper-V/3.17.0.0/Tests/Unit/MSFT_xVMHyperV.Tests.ps1 b/deployment/dsc/azshcihost/xHyper-V/3.17.0.0/Tests/Unit/MSFT_xVMHyperV.Tests.ps1 new file mode 100644 index 0000000..2c7f8fd --- /dev/null +++ b/deployment/dsc/azshcihost/xHyper-V/3.17.0.0/Tests/Unit/MSFT_xVMHyperV.Tests.ps1 @@ -0,0 +1,669 @@ +[CmdletBinding()] +param() + +if (!$PSScriptRoot) # $PSScriptRoot is not defined in 2.0 +{ + $PSScriptRoot = [System.IO.Path]::GetDirectoryName($MyInvocation.MyCommand.Path) +} + +$ErrorActionPreference = 'stop' +Set-StrictMode -Version latest + +$RepoRoot = (Resolve-Path $PSScriptRoot\..\..).Path + +$ModuleName = 'MSFT_xVMHyperV' +Import-Module (Join-Path $RepoRoot "DSCResources\$ModuleName\$ModuleName.psm1") -Force; + +Describe 'xVMHyper-V' { + InModuleScope $ModuleName { + + # Create empty functions to be able to mock the missing Hyper-V cmdlets + # CmdletBinding required on Get-VM to support $ErrorActionPreference + function Get-VM { [CmdletBinding()] param( [Parameter(ValueFromRemainingArguments)] $Name) } + # Generation parameter is required for the mocking -ParameterFilter to work + function New-VM { param ( $Generation) } + function Set-VM { param ( $Name, $AutomaticCheckpointsEnabled ) } + function Stop-VM { } + function Remove-VM { } + function Get-VMNetworkAdapter { } + function Set-VMNetworkAdapter { } + function Add-VMNetworkAdapter { } + function Connect-VMNetworkAdapter { param ( $SwitchName ) } + function Get-VMFirmware { } + function Set-VMFirmware { } + function Get-VMIntegrationService { param ([Parameter(ValueFromPipeline)] $VM, $Name)} + function Enable-VMIntegrationService { param ([Parameter(ValueFromPipeline)] $VM, $Name)} + function Disable-VMIntegrationService { param ([Parameter(ValueFromPipeline)] $VM, $name)} + function Get-VHD { param ( $Path ) } + function Set-VMMemory { } + + $stubVhdxDisk = New-Item -Path 'TestDrive:\TestVM.vhdx' -ItemType File; + $studVhdxDiskSnapshot = New-Item -Path "TestDrive:\TestVM_D0145678-1576-4435-AB18-9F000C1C17D0.avhdx" -ItemType File; + $stubVhdDisk = New-Item -Path 'TestDrive:\TestVM.vhd' -ItemType File; + $StubVMConfig = New-Item -Path 'TestDrive:\TestVM.xml' -ItemType File; + $stubNIC1 = @{ SwitchName = 'Test Switch 1'; MacAddress = 'AA-BB-CC-DD-EE-FF'; IpAddresses = @('192.168.0.1','10.0.0.1'); }; + $stubNIC2 = @{ SwitchName = 'Test Switch 2'; MacAddress = 'AA-BB-CC-DD-EE-FE'; IpAddresses = @('192.168.1.1'); }; + $stubVM = @{ + HardDrives = @( + @{ Path = $stubVhdxDisk.FullName; } + @{ Path = $stubVhdDisk.FullName; } + ); + #State = 'Running'; + Path = $StubVMConfig.FullPath; + Generation = 1; + SecureBoot = $true; + MemoryStartup = 512MB; + MinimumMemory = 128MB; + MaximumMemory = 4096MB; + ProcessorCount = 1; + ID = [System.Guid]::NewGuid().ToString(); + CPUUsage = 10; + MemoryAssigned = 512MB; + Uptime = New-TimeSpan -Hours 12; + CreationTime = (Get-Date).AddHours(-12); + DynamicMemoryEnabled = $true; + NetworkAdapters = @($stubNIC1,$stubNIC2); + Notes = ''; + } + $stubGuestServiceInterfaceId = 'Microsoft:{0}\6C09BB55-D683-4DA0-8931-C9BF705F6480' -f $stubVM.ID + + Mock -CommandName Get-VM -ParameterFilter { $Name -eq 'RunningVM' } -MockWith { + $runningVM = $stubVM.Clone(); + $runningVM['State'] = 'Running'; + return [PSCustomObject] $runningVM; + } + Mock -CommandName Get-VM -ParameterFilter { $Name -eq 'StoppedVM' } -MockWith { + $stoppedVM = $stubVM.Clone(); + $stoppedVM['State'] = 'Off'; + return [PSCustomObject] $stoppedVM; + } + Mock -CommandName Get-VM -ParameterFilter { $Name -eq 'PausedVM' } -MockWith { + $pausedVM = $stubVM.Clone(); + $pausedVM['State'] = 'Paused'; + return [PSCustomObject] $pausedVM; + } + Mock -CommandName Get-VM -ParameterFilter { $Name -eq 'NonexistentVM' } -MockWith { + Write-Error 'VM not found.'; + } + Mock -CommandName Get-VM -ParameterFilter { $Name -eq 'DuplicateVM' } -MockWith { + return @([PSCustomObject] $stubVM, [PSCustomObject] $stubVM); + } + Mock -CommandName Get-VM -ParameterFilter { $Name -eq 'Generation1Vhd' } -MockWith { + $vhdVM = $stubVM.Clone(); + $vhdVM['HardDrives'] = @( @{ Path = $stubVhdDisk.FullName } ); + return [PSCustomObject] $vhdVM; + } + Mock -CommandName Get-VM -ParameterFilter { $Name -eq 'Generation2VM' } -MockWith { + $gen2VM = $stubVM.Clone(); + $gen2VM['Generation'] = 2; + return [PSCustomObject] $gen2VM; + } + Mock -CommandName Get-VM -ParameterFilter { $Name -eq 'VMWithAutomaticCheckpoints'} -MockWith { + $AutomaticCheckPointVM = $stubVM.Clone(); + $AutomaticCheckPointVM['AutomaticCheckpointsEnabled'] = $true; + return [PSCustomObject] $AutomaticCheckPointVM; + } + Mock -CommandName Get-VM -ParameterFilter { $Name -eq 'VMWithoutAutomaticCheckpoints'} -MockWith { + $NoAutomaticCheckPointVM = $stubVM.Clone() + $NoAutomaticCheckPointVM['AutomaticCheckpointsEnabled'] = $false + return [PSCustomObject] $NoAutomaticCheckPointVM + } + Mock -CommandName Get-VM -ParameterFilter { $Name -eq 'VMAutomaticCheckpointsUnsupported'} -MockWith { + $AutomaticCheckPointUnsupportedVM = $stubVM.Clone() + return [PSCustomObject] $AutomaticCheckPointUnsupportedVM + } + Mock -CommandName Get-VMIntegrationService -MockWith {return [pscustomobject]@{Enabled=$false;Id=$stubGuestServiceInterfaceId}} + Mock -CommandName Get-Module -ParameterFilter { ($Name -eq 'Hyper-V') -and ($ListAvailable -eq $true) } -MockWith { return $true; } + Mock -CommandName Get-VhdHierarchy -ParameterFilter { $VhdPath.EndsWith('.vhd') } -MockWith { + # Return single Vhd chain for .vhds + return @($stubVhdDisk.FullName); + } + Mock -CommandName Get-VhdHierarchy -ParameterFilter { $VhdPath.EndsWith('.vhdx') } -MockWith { + # Return snapshot hierarchy for .vhdxs + return @($stubVhdxDiskSnapshot.FullName, $stubVhdxDisk.FullName); + } + Context 'Validates Get-TargetResource Method' { + + It 'Returns a hashtable' { + $targetResource = Get-TargetResource -Name 'RunningVM' -VhdPath $stubVhdxDisk.FullName; + $targetResource -is [System.Collections.Hashtable] | Should Be $true; + } + + It 'Throws when multiple VMs are present' { + { Get-TargetResource -Name 'DuplicateVM' -VhdPath $stubVhdxDisk.FullName } | Should Throw; + } + + It 'Does not call Get-VMFirmware if a generation 1 VM' { + Mock -CommandName Get-VMFirmware -MockWith { throw; } + $null = Get-TargetResource -Name 'RunningVM' -VhdPath $stubVhdxDisk.FullName; + Assert-MockCalled -CommandName Get-VMFirmware -Scope It -Exactly 0; + } + + It 'Calls Get-VMFirmware if a generation 2 VM' { + Mock -CommandName Get-VMFirmware -MockWith { return $true; } + $null = Get-TargetResource -Name 'Generation2VM' -VhdPath $stubVhdxDisk.FullName; + Assert-MockCalled -CommandName Get-VMFirmware -Scope It -Exactly 1; + } + + It 'Hash table contains key EnableGuestService' { + $targetResource = Get-TargetResource -Name 'RunningVM' -VhdPath $stubVhdxDisk.FullName; + $targetResource.ContainsKey('EnableGuestService') | Should Be $true; + } + It 'Hash table contains key AutomaticCheckpointEnabled' { + $targetResource = Get-TargetResource -Name 'VMWithAutomaticCheckpoints' -VhdPath $stubVhdxDisk.FullName; + $targetResource.ContainsKey('AutomaticCheckpointsEnabled') | Should Be $true; + } + It 'Throws when Hyper-V Tools are not installed' { + # This test needs to be the last in the Context otherwise all subsequent Get-Module checks will fail + Mock -CommandName Get-Module -ParameterFilter { ($Name -eq 'Hyper-V') -and ($ListAvailable -eq $true) } -MockWith { } + { Get-TargetResource -Name 'RunningVM' @testParams } | Should Throw; + } + } #end context Validates Get-TargetResource Method + + Context 'Validates Test-TargetResource Method' { + $testParams = @{ + VhdPath = $stubVhdxDisk.FullName; + } + + It 'Returns a boolean' { + $targetResource = Test-TargetResource -Name 'RunningVM' @testParams; + $targetResource -is [System.Boolean] | Should Be $true; + } + + It 'Returns $true when VM is present and "Ensure" = "Present"' { + Test-TargetResource -Name 'RunningVM' @testParams | Should Be $true; + } + + It 'Returns $false when VM is not present and "Ensure" = "Present"' { + Test-TargetResource -Name 'NonexistentVM' @testParams | Should Be $false; + } + + It 'Returns $true when VM is not present and "Ensure" = "Absent"' { + Test-TargetResource -Name 'NonexistentVM' -Ensure Absent @testParams | Should Be $true; + } + + It 'Returns $false when VM is present and "Ensure" = "Absent"' { + Test-TargetResource -Name 'RunningVM' -Ensure Absent @testParams | Should Be $false; + } + + It 'Returns $true when VM is in the "Running" state and no state is explicitly specified' { + Test-TargetResource -Name 'RunningVM' @testParams | Should Be $true; + } + + It 'Returns $true when VM is in the "Stopped" state and no state is explicitly specified' { + Test-TargetResource -Name 'StoppedVM' @testParams | Should Be $true; + } + + It 'Returns $true when VM is in the "Paused" state and no state is explicitly specified' { + Test-TargetResource -Name 'PausedVM' @testParams | Should Be $true; + } + + It 'Returns $true when VM is in the "Running" state and requested "State" = "Running"' { + Test-TargetResource -Name 'RunningVM' @testParams | Should Be $true; + } + + It 'Returns $true when VM is in the "Off" state and requested "State" = "Off"' { + Test-TargetResource -Name 'StoppedVM' -State Off @testParams | Should Be $true; + } + + It 'Returns $true when VM is in the "Paused" state and requested "State" = Paused"' { + Test-TargetResource -Name 'PausedVM' -State Paused @testParams | Should Be $true; + } + + It 'Returns $false when VM is in the "Running" state and requested "State" = "Off"' { + Test-TargetResource -Name 'RunningVM' -State Off @testParams | Should Be $false; + } + + It 'Returns $false when VM is in the "Off" state and requested "State" = "Runnning"' { + Test-TargetResource -Name 'StoppedVM' -State Running @testParams | Should Be $false; + } + + It 'Returns $true when VM .vhd file is specified with a generation 1 VM' { + Test-TargetResource -Name 'Generation1Vhd' -VhdPath $stubVhdDisk -Generation 1 -Verbose | Should Be $true; + } + + It 'Returns $true when VM .vhdx file is specified with a generation 1 VM' { + Test-TargetResource -Name 'StoppedVM' -VhdPath $stubVhdxDisk -Generation 1 | Should Be $true; + } + + It 'Returns $true when VM .vhdx file is specified with a generation 2 VM' { + Mock -CommandName Test-VMSecureBoot -MockWith { return $true; } + Test-TargetResource -Name 'Generation2VM' -Generation 2 @testParams | Should Be $true; + } + + It 'Throws when a VM .vhd file is specified with a generation 2 VM' { + { Test-TargetResource -Name 'Gen2VM' -VhdPath $stubVhdDisk -Generation 2 } | Should Throw; + } + + It 'Returns $true when multiple NICs are assigned in the correct order' { + Test-TargetResource -Name 'RunningVM' @testParams -SwitchName @($stubNIC1.SwitchName,$stubNIC2.SwitchName) | Should Be $true; + } + + It 'Returns $false when multiple NICs are not assigned/assigned in the wrong order' { + Test-TargetResource -Name 'RunningVM' @testParams -SwitchName @($stubNIC2.SwitchName,$stubNIC1.SwitchName) | Should Be $false; + } + + It 'Returns $true when multiple MAC addresses are assigned in the correct order' { + Test-TargetResource -Name 'RunningVM' @testParams -MACAddress @($stubNIC1.MACAddress,$stubNIC2.MACAddress) | Should Be $true; + } + + It 'Returns $false when multiple MAC addresses not assigned/assigned in the wrong order' { + Test-TargetResource -Name 'RunningVM' @testParams -MACAddress @($stubNIC1.MACAddress,$stubNIC2.MACAddress) | Should Be $true; + } + + It 'Returns $true regardless of "SecureBoot" setting on a generation 1 VM' { + Test-TargetResource -Name 'RunningVM' -SecureBoot $true @testParams | Should Be $true; + Test-TargetResource -Name 'RunningVM' -SecureBoot $false @testParams | Should Be $true; + } + + It 'Returns $true when SecureBoot is On and requested "SecureBoot" = "$true"' { + Mock -CommandName Test-VMSecureBoot -MockWith { return $true; } + Test-TargetResource -Name 'Generation2VM' -Generation 2 @testParams | Should Be $true; + } + + It 'Returns $false when SecureBoot is On and requested "SecureBoot" = "$false"' { + Mock -CommandName Test-VMSecureBoot -MockWith { return $true; } + Test-TargetResource -Name 'Generation2VM' -SecureBoot $false -Generation 2 @testParams | Should be $false; + } + + It 'Returns $true when VM has snapshot chain' { + Mock -CommandName Get-VhdHierarchy -MockWith { Write-Host $VhdPath; return @($studVhdxDiskSnapshot, $stubVhdxDisk); } + Test-TargetResource -Name 'Generation2VM' -VhdPath $stubVhdxDisk -Verbose | Should Be $true; + } + + It 'Returns $false when EnableGuestService is off and requested "EnableGuestService" = "$true"' { + Test-TargetResource -Name 'RunningVM' -EnableGuestService $true @testParams | Should be $false; + } + + It 'Returns $true when EnableGuestService is off and "EnableGuestService" is not requested"' { + Test-TargetResource -Name 'RunningVM' @testParams | Should be $true; + } + + Mock -CommandName Get-Command -ParameterFilter { $Name -eq 'Set-VM' -and $Module -eq 'Hyper-V'} -MockWith { + [pscustomobject]@{ + parameters = @{ + # Does not contains parameter AutomaticCheckpointsEnabled + } + } + } + It 'Throws when AutomaticCheckpointsEnabled is configured but not supported' { + { Test-TargetResource -Name 'VMAutomaticCheckpoinstUnsupported' -AutomaticCheckpointsEnabled $true @testParams } | Should Throw; + } + + Mock -CommandName Get-Command -ParameterFilter { $Name -eq 'Set-VM' -and $Module -eq 'Hyper-V'} -MockWith { + [pscustomobject]@{ + parameters = @{ + 'AutomaticCheckpointsEnabled' = '' + } + } + } + It 'Returns $true when AutomaticCheckpointsEnabled is on and requested "AutomaticCheckpointsEnabled" is not requested' { + Test-TargetResource -Name 'VMWithAutomaticCheckpoints' @testParams | Should be $true; + } + It 'Returns $true when AutomaticCheckpointsEnabled is on and requested "AutomaticCheckpointsEnabled" = "$true"' { + Test-TargetResource -Name 'VMWithAutomaticCheckpoints' -AutomaticCheckpointsEnabled $true @testParams | Should be $true; + } + It 'Returns $true when AutomaticCheckpointsEnabled is off and requested "AutomaticCheckpointsEnabled" = "$false"' { + Test-TargetResource -Name 'VMWithoutAutomaticCheckpoints' -AutomaticCheckpointsEnabled $false @testParams | Should be $true; + } + It 'Returns $false when AutomaticCheckpointsEnabled is off and requested "AutomaticCheckpointsEnabled" = "$true"' { + Test-TargetResource -Name 'VMWithoutAutomaticCheckpoints' -AutomaticCheckpointsEnabled $true @testParams | Should be $false; + } + It 'Returns $false when AutomaticCheckpointsEnabled is on and requested "AutomaticCheckpointsEnabled" = "$false"' { + Test-TargetResource -Name 'VMWithAutomaticCheckpoints' -AutomaticCheckpointsEnabled $false @testParams | Should be $false; + } + + It 'Returns $true when EnableGuestService is on and requested "EnableGuestService" = "$true"' { + Mock -CommandName Get-VMIntegrationService -MockWith {return [pscustomobject]@{Enabled=$true;Id=$stubGuestServiceInterfaceId}} + Test-TargetResource -Name 'RunningVM' -EnableGuestService $true @testParams | Should be $true; + } + + It 'Throws when Hyper-V Tools are not installed' { + # This test needs to be the last in the Context otherwise all subsequent Get-Module checks will fail + Mock -CommandName Get-Module -ParameterFilter { ($Name -eq 'Hyper-V') -and ($ListAvailable -eq $true) } -MockWith { } + { Test-TargetResource -Name 'RunningVM' @testParams } | Should Throw; + } + + } #end context Validates Test-TargetResource Method + + Context 'Validates Set-TargetResource Method' { + $testParams = @{ + VhdPath = $stubVhdxDisk.FullName; + } + + Mock -CommandName Get-VM -ParameterFilter { $Name -eq 'NewVM' } -MockWith { } + Mock -CommandName New-VM -MockWith { + $newVM = $stubVM.Clone(); + $newVM['State'] = 'Off'; + $newVM['Generation'] = $Generation; + return $newVM; + } + Mock -CommandName Set-VM -MockWith { return $true; } + Mock -CommandName Stop-VM -MockWith { return $true; } # requires output to be able to pipe something into Remove-VM + Mock -CommandName Remove-VM -MockWith { return $true; } + Mock -CommandName Set-VMNetworkAdapter -MockWith { return $true; } + Mock -CommandName Get-VMNetworkAdapter -MockWith { return $stubVM.NetworkAdapters.IpAddresses; } + Mock -CommandName Set-VMState -MockWith { return $true; } + Mock -CommandName Set-VMMemory -MockWith { } + + It 'Removes an existing VM when "Ensure" = "Absent"' { + Set-TargetResource -Name 'RunningVM' -Ensure Absent @testParams; + Assert-MockCalled -CommandName Remove-VM -Scope It; + } + + It 'Creates and starts a VM VM with disabled dynamic memory that does not exist when "Ensure" = "Present" and "State" = "Running"' { + Set-TargetResource -Name 'NewVM' -State Running @testParams; + Assert-MockCalled -CommandName New-VM -Exactly -Times 1 -Scope It; + Assert-MockCalled -CommandName Set-VM -Exactly -Times 1 -Scope It; + Assert-MockCalled -CommandName Set-VMState -Exactly -Times 1 -Scope It; + } + + It 'Creates but does not start a VM with disabled dynamic memory that does not exist when "Ensure" = "Present"' { + Set-TargetResource -Name 'NewVM' @testParams; + Assert-MockCalled -CommandName New-VM -Exactly -Times 1 -Scope It; + Assert-MockCalled -CommandName Set-VM -Exactly -Times 1 -Scope It; + Assert-MockCalled -CommandName Set-VMState -Exactly -Times 0 -Scope It; + } + + It 'Creates but does not start a VM with disabled dynamic memory when only StartupMemory is specified' { + Set-TargetResource -Name 'NewVM' @testParams -StartupMemory 4GB; + Assert-MockCalled -CommandName New-VM -Exactly -Times 1 -Scope It; + Assert-MockCalled -CommandName Set-VM -Exactly -Times 1 -Scope It + Assert-MockCalled -CommandName Set-VMState -Exactly -Times 0 -Scope It; + } + + It 'Creates but does not start a VM with disabled dynamic memory when identical values for startup, minimum and maximum memory are specified' { + Set-TargetResource -Name 'NewVM' @testParams -StartupMemory 4GB -MinimumMemory 4GB -MaximumMemory 4GB; + Assert-MockCalled -CommandName New-VM -Exactly -Times 1 -Scope It + Assert-MockCalled -CommandName Set-VM -Exactly -Times 1 -Scope It + Assert-MockCalled -CommandName Set-VMMemory -Exactly -Times 1 -Scope It + Assert-MockCalled -CommandName Set-VMState -Exactly -Times 0 -Scope It + } + + It 'Creates but does not start a VM with enabled dynamic memory because a MinimumMemory value is specified' { + Set-TargetResource -Name 'NewVM' @testParams -MinimumMemory 512MB + Assert-MockCalled -CommandName New-VM -Exactly -Times 1 -Scope It + Assert-MockCalled -CommandName Set-VM -Exactly -Times 1 -Scope It + Assert-MockCalled -CommandName Set-VMMemory -Exactly -Times 0 -Scope It + Assert-MockCalled -CommandName Set-VMState -Exactly -Times 0 -Scope It + } + + It 'Creates but does not start a VM with enabled dynamic memory because a MaximumMemory value is specified' { + Set-TargetResource -Name 'NewVM' @testParams -MaximumMemory 16GB + Assert-MockCalled -CommandName New-VM -Exactly -Times 1 -Scope It + Assert-MockCalled -CommandName Set-VM -Exactly -Times 1 -Scope It + Assert-MockCalled -CommandName Set-VMMemory -Exactly -Times 0 -Scope It + Assert-MockCalled -CommandName Set-VMState -Exactly -Times 0 -Scope It + } + + It 'Does not change VM state when VM "State" = "Running" and requested "State" = "Running"' { + Set-TargetResource -Name 'RunningVM' -State Running @testParams; + Assert-MockCalled -CommandName Set-VMState -Exactly -Times 0 -Scope It; + } + + It 'Does not change VM state when VM "State" = "Off" and requested "State" = "Off"' { + Set-TargetResource -Name 'StoppedVM' -State Off @testParams; + Assert-MockCalled -CommandName Set-VMState -Exactly -Times 0 -Scope It; + } + + It 'Changes VM state when existing VM "State" = "Off" and requested "State" = "Running"' { + Set-TargetResource -Name 'StoppedVM' -State Running @testParams; + Assert-MockCalled -CommandName Set-VMState -Exactly -Times 1 -Scope It; + } + + It 'Changes VM state when existing VM "State" = "Running" and requested "State" = "Off"' { + Set-TargetResource -Name 'RunningVM' -State Off @testParams; + Assert-MockCalled -CommandName Set-VMState -Exactly -Times 1 -Scope It; + } + + It 'Creates a generation 1 VM by default/when not explicitly specified' { + Set-TargetResource -Name 'NewVM' @testParams; + Assert-MockCalled -CommandName New-VM -ParameterFilter { $Generation -eq 1 } -Scope It; + } + + It 'Creates a generation 1 VM when explicitly specified' { + Set-TargetResource -Name 'NewVM' -Generation 1 @testParams; + Assert-MockCalled -CommandName New-VM -ParameterFilter { $Generation -eq 1 } -Scope It; + } + + It 'Creates a generation 2 VM when explicitly specified' { + Set-TargetResource -Name 'NewVM' -Generation 2 @testParams; + Assert-MockCalled -CommandName New-VM -ParameterFilter { $Generation -eq 2 } -Scope It; + } + + It 'Calls "Add-VMNetworkAdapter" for each NIC when creating a new VM' { + Mock -CommandName Add-VMNetworkAdapter -MockWith { } + Set-TargetResource -Name 'NewVM' @testParams -SwitchName 'Switch1','Switch2'; + # The first NIC is assigned during the VM creation + Assert-MockCalled -CommandName Add-VMNetworkAdapter -Exactly 1 -Scope It; + } + + It 'Calls "Connect-VMNetworkAdapter" for each existing NIC when updating an existing VM' { + Mock -CommandName Connect-VMNetworkAdapter -MockWith { } + Set-TargetResource -Name 'StoppedVM' @testParams -SwitchName 'Switch1','Switch2'; + # The first NIC is assigned during the VM creation + Assert-MockCalled -CommandName Connect-VMNetworkAdapter -Exactly 2 -Scope It; + } + + It 'Calls "Add-VMNetworkAdapter" for each missing NIC when updating an existing VM' { + Mock -CommandName Connect-VMNetworkAdapter -MockWith { } + Mock -CommandName Add-VMNetworkAdapter -MockWith { } + Set-TargetResource -Name 'StoppedVM' @testParams -SwitchName 'Switch1','Switch2','Switch3'; + # The first NIC is assigned during the VM creation + Assert-MockCalled -CommandName Connect-VMNetworkAdapter -Exactly 2 -Scope It; + Assert-MockCalled -CommandName Add-VMNetworkAdapter -Exactly 1 -Scope It; + } + + It 'Does not change switch assignments if no switch assignments are specified' { + Mock -CommandName Connect-VMNetworkAdapter -MockWith { } + Set-TargetResource -Name 'StoppedVM' @testParams; + Assert-MockCalled -CommandName Connect-VMNetworkAdapter -Exactly 0 -Scope It; + } + + It 'Does not change NIC assignments if the switch assisgnments are correct' { + Mock -CommandName Set-VMNetworkAdapter -MockWith { } + Set-TargetResource -Name 'StoppedVM' @testParams -SwitchName $stubNIC1.SwitchName,$stubNIC2.SwitchName; + Assert-MockCalled -CommandName Set-VMNetworkAdapter -Exactly 0 -Scope It; + } + + It 'Errors when updating MAC addresses on a running VM and "RestartIfNeeded" = "$false"' { + { Set-TargetResource -Name 'RunningVM' @testParams -MACAddress 'AABBCCDDEEFE','AABBCCDDEEFF' -ErrorAction Stop } | Should Throw; + } + + It 'Does not change MAC addresses if no MAC addresses assignments are specified' { + Mock -CommandName Set-VMNetworkAdapter -ParameterFilter { $StaticMacAddress -ne $null } -MockWith { } + Set-TargetResource -Name 'StoppedVM' @testParams; + Assert-MockCalled -CommandName Set-VMNetworkAdapter -ParameterFilter { $StaticMacAddress -ne $null } -Exactly 0 -Scope It; + } + + It 'Calls "Set-VMNetworkAdapter" for each MAC address on a stopped VM' { + Mock -CommandName Set-VMNetworkAdapter -MockWith { } + Set-TargetResource -Name 'StoppedVM' @testParams -MACAddress 'AABBCCDDEEFE','AABBCCDDEEFF'; + # The first NIC is assigned during the VM creation + Assert-MockCalled -CommandName Set-VMNetworkAdapter -Exactly 2 -Scope It; + } + + It 'Does not change Secure Boot call "Set-VMProperty" when creating a generation 1 VM' { + Mock Set-VMProperty -MockWith { } + Set-TargetResource -Name 'RunningVM' @testParams; + Assert-MockCalled Set-VMProperty -ParameterFilter { $VMCommand -eq 'Set-VMFirmware' } -Exactly 0 -Scope It; + } + + It 'Does call "Set-VMProperty" when creating a generation 2 VM' { + Mock Test-VMSecureBoot -MockWith { return $true; } + Mock Set-VMProperty -MockWith { } + Set-TargetResource -Name 'RunningVM' -Generation 2 -SecureBoot $false @testParams; + Assert-MockCalled Set-VMProperty -ParameterFilter { $VMCommand -eq 'Set-VMFirmware' } -Exactly 1 -Scope It; + } + + It 'Does not change Secure Boot for generation 1 VM' { + Mock Test-VMSecureBoot -MockWith { return $true; } + Mock Set-VMProperty -MockWith { } + Set-TargetResource -Name 'StoppedVM' -SecureBoot $true @testParams; + Set-TargetResource -Name 'StoppedVM' -SecureBoot $false @testParams; + Assert-MockCalled Set-VMProperty -ParameterFilter { $VMCommand -eq 'Set-VMFirmware' } -Exactly 0 -Scope It; + } + + It 'Does not change Secure Boot for generation 2 VM with VM "SecureBoot" match' { + Mock Test-VMSecureBoot -MockWith { return $true; } + Mock Set-VMProperty -MockWith { } + Set-TargetResource -Name 'StoppedVM' -SecureBoot $true -Generation 2 @testParams; + Assert-MockCalled Set-VMProperty -ParameterFilter { $VMCommand -eq 'Set-VMFirmware' } -Exactly 0 -Scope It; + } + + It 'Does change Secure Boot for generation 2 VM with VM "SecureBoot" mismatch' { + Mock Test-VMSecureBoot -MockWith { return $false; } + Mock Set-VMProperty -MockWith { } + Set-TargetResource -Name 'StoppedVM' -SecureBoot $true -Generation 2 @testParams; + Assert-MockCalled Set-VMProperty -ParameterFilter { $VMCommand -eq 'Set-VMFirmware' } -Exactly 1 -Scope It; + } + + It 'Does call "Enable-VMIntegrationService" when "EnableGuestService" = "$true"' { + Mock -CommandName Enable-VMIntegrationService -MockWith { } + Set-TargetResource -Name 'RunningVM' -EnableGuestService $true @testParams + Assert-MockCalled -CommandName Enable-VMIntegrationService -Exactly -Times 1 -Scope It + } + + It 'Does call "Disable-VMIntegrationService" when "Guest Service Interface" = "Enabled" and "EnableGuestService" = "$false" specified' { + Mock -CommandName Disable-VMIntegrationService -MockWith { } + Mock -CommandName Get-VMIntegrationService -MockWith {return [pscustomobject]@{Enabled=$true;Id=$stubGuestServiceInterfaceId}} + Set-TargetResource -Name 'RunningVM' -EnableGuestService $false @testParams + Assert-MockCalled -CommandName Disable-VMIntegrationService -Exactly -Times 1 -Scope It + } + + Mock -CommandName Get-Command -ParameterFilter { $Name -eq 'Set-VM' -and $Module -eq 'Hyper-V'} -MockWith { + [pscustomobject]@{ + parameters = @{ + # Does not contain parameter AutomaticCheckpointsEnabled + } + } + } + It 'Throws when AutomaticCheckpointsEnabled is configured but not supported' { + { Set-TargetResource -Name 'VMAutomaticCheckpointsUnsupported' -AutomaticCheckpointsEnabled $true @testParams } | Should Throw; + } + It 'Does not call "Set-VM" when "AutomaticCheckpointsEnabled" is unsupported and unspecified' { + Set-TargetResource -Name 'VMAutomaticCheckpointsUnsupported' @testParams + Assert-MockCalled -CommandName Set-VM -Exactly -Times 0 -Scope It + } + + Mock -CommandName Get-Command -ParameterFilter { $Name -eq 'Set-VM' -and $Module -eq 'Hyper-V'} -MockWith { + [pscustomobject]@{ + parameters = @{ + 'AutomaticCheckpointsEnabled' = '' + } + } + } + $AutomaticCheckpointsEnabledTestCases = @( + @{ + VMName = 'VMWithAutomaticCheckpoints' + SetAutomaticCheckpointsEnabled = $true + Assert = 'Does not call "Set-VM"' + Times = 0 + }, + @{ + VMName = 'VMWithoutAutomaticCheckpoints' + SetAutomaticCheckpointsEnabled = $false + Assert = 'Does not call "Set-VM"' + Times = 0 + }, + @{ + VMName = 'VMWithAutomaticCheckpoints' + SetAutomaticCheckpointsEnabled = $false + Assert = 'Does call "Set-VM"' + Times = 1 + }, + @{ + VMName = 'VMWithoutAutomaticCheckpoints' + SetAutomaticCheckpointsEnabled = $true + Assert = 'Does call "Set-VM"' + Times = 1 + } + ) + It '<Assert> on VM <VMName> when "AutomaticCheckpointsEnabled" is set to "<SetAutomaticCheckpointsEnabled>"' -TestCases $AutomaticCheckpointsEnabledTestCases { + Param($VMName,$SetAutomaticCheckpointsEnabled,$Times) + Set-TargetResource -Name $VMName -AutomaticCheckpointsEnabled $SetAutomaticCheckpointsEnabled @testParams + Assert-MockCalled -CommandName Set-VM -ParameterFilter {$Name -eq $VMName -and $AutomaticCheckpointsEnabled -eq $SetAutomaticCheckpointsEnabled} -Exactly -Times $Times -Scope It + } + It 'Disables dynamic memory of RuningVM if only StartupMemory specified' { + Mock Set-VMProperty -MockWith { } + Set-TargetResource -Name 'RunningVM' -StartupMemory 4GB @testParams + Assert-MockCalled -CommandName Set-VMProperty -ParameterFilter { + $VMCommand -eq 'Set-VM' -and + ($ChangeProperty.StaticMemory -eq $true) -and + ($ChangeProperty.DynamicMemory -eq $false) + } -Exactly -Times 1 -Scope It + } + + It 'Disables dynamic memory of RuningVM if StartupMemory, MinimumMemory and MaximumMemory are specified with the same values' { + Mock Set-VMProperty -MockWith { } + Set-TargetResource -Name 'RunningVM' -StartupMemory 4GB -MinimumMemory 4GB -MaximumMemory 4GB @testParams + Assert-MockCalled -CommandName Set-VMProperty -ParameterFilter { + $VMCommand -eq 'Set-VM' -and + ($ChangeProperty.StaticMemory -eq $true) -and + ($ChangeProperty.DynamicMemory -eq $false) + } -Exactly -Times 1 -Scope It + } + + It 'Enables dynamic memory of RuningVM if MinimumMemory is specified ' { + Mock Set-VMProperty -MockWith { } + Set-TargetResource -Name 'RunningVM' -MinimumMemory 4GB @testParams + Assert-MockCalled -CommandName Set-VMProperty -ParameterFilter { + $VMCommand -eq 'Set-VM' -and + ($ChangeProperty.StaticMemory -eq $false) -and + ($ChangeProperty.DynamicMemory -eq $true) + } -Exactly -Times 1 -Scope It + } + + It 'Enables dynamic memory of RuningVM if MaximumMemory is specified ' { + Mock Set-VMProperty -MockWith { } + Set-TargetResource -Name 'RunningVM' -MaximumMemory 4GB @testParams + Assert-MockCalled -CommandName Set-VMProperty -ParameterFilter { + $VMCommand -eq 'Set-VM' -and + ($ChangeProperty.StaticMemory -eq $false) -and + ($ChangeProperty.DynamicMemory -eq $true) + } -Exactly -Times 1 -Scope It + } + + It 'Throws when Hyper-V Tools are not installed' { + Mock -CommandName Get-Module -ParameterFilter { ($Name -eq 'Hyper-V') -and ($ListAvailable -eq $true) } -MockWith { } + { Set-TargetResource -Name 'RunningVM' @testParams } | Should Throw; + } + } #end context Validates Set-TargetResource Method + + Context 'Validates Test-VMSecureBoot Method' { + + It 'Returns $true when "SecureBoot" = "On"' { + Mock -CommandName Get-VM -MockWith { } + Mock -CommandName Get-VMFirmware -MockWith { return [PSCustomObject] @{ SecureBoot = 'On' }; } + Test-VMSecureBoot -Name 'TestVM' | Should Be $true; + } + + It 'Returns $false when "SecureBoot" = "Off"' { + Mock -CommandName Get-VM -MockWith { } + Mock -CommandName Get-VMFirmware -MockWith { return [PSCustomObject] @{ SecureBoot = 'Off' }; } + Test-VMSecureBoot -Name 'TestVM' | Should Be $false; + } + + } #end context Validates Test-VMSecureBoot Method + + Context 'Validates Get-VhdHierarchy Method' { + + It 'Does not throw with null parent path (#52)' { + + # Must use a different file extension to ensure existing mocks Get-VhdHierarchy or not called + $fakeVhdPath = 'BaseVhd.avhdx'; + Mock -CommandName Get-VHD -ParameterFilter { $Path -eq $fakeVhdPath } -MockWith { + return [PSCustomObject] @{ + Path = $fakeVhdPath; + ParentPath = $null; + } + } + + { Get-VhdHierarchy -VhdPath $fakeVhdPath } | Should Not Throw; + } + + } #end context validates Get-VhdHierarchy + + + } #end inmodulescope +} #end describe xVMHyper-V diff --git a/deployment/dsc/azshcihost/xHyper-V/3.17.0.0/Tests/Unit/MSFT_xVMNetworkAdapter.Tests.ps1 b/deployment/dsc/azshcihost/xHyper-V/3.17.0.0/Tests/Unit/MSFT_xVMNetworkAdapter.Tests.ps1 new file mode 100644 index 0000000..cd65429 --- /dev/null +++ b/deployment/dsc/azshcihost/xHyper-V/3.17.0.0/Tests/Unit/MSFT_xVMNetworkAdapter.Tests.ps1 @@ -0,0 +1,357 @@ +$Global:DSCModuleName = 'xHyper-V' +$Global:DSCResourceName = 'MSFT_xVMNetworkAdapter' + +#region HEADER +if ( (-not (Test-Path -Path '.\DSCResource.Tests\')) -or ` + (-not (Test-Path -Path '.\DSCResource.Tests\TestHelper.psm1')) ) +{ + & git @('clone','https://github.com/PowerShell/DscResource.Tests.git') +} +else +{ + & git @('-C',(Join-Path -Path (Get-Location) -ChildPath '\DSCResource.Tests\'),'pull') +} +Import-Module .\DSCResource.Tests\TestHelper.psm1 -Force +$TestEnvironment = Initialize-TestEnvironment ` + -DSCModuleName $Global:DSCModuleName ` + -DSCResourceName $Global:DSCResourceName ` + -TestType Unit +#endregion + +# Begin Testing +try +{ + #region Pester Tests + InModuleScope $Global:DSCResourceName { + + # Create the Mock Objects that will be used for running tests + $MockHostAdapter = [PSCustomObject] @{ + Id = 'HostManagement1' + Name = 'Management' + SwitchName = 'HostSwitch' + VMName = 'ManagementOS' + } + + $propertiesStatic = @{ + IpAddress = "192.168.0.1" + Subnet = "255.255.255.0" + } + + $networkSettingsStatic = New-CimInstance -ClassName xNetworkSettings -Property $properties -Namespace root/microsoft/windows/desiredstateconfiguration -ClientOnly + + $TestAdapter = [PSObject]@{ + Id = $MockHostAdapter.Id + Name = $MockHostAdapter.Name + SwitchName = $MockHostAdapter.SwitchName + VMName = $MockHostAdapter.VMName + } + + $MockAdapter = [PSObject]@{ + Name = $TestAdapter.Name + SwitchName = $MockHostAdapter.SwitchName + IsManagementOs = $True + MacAddress = '14FEB5C6CE98' + } + + $MockAdapterVlanUntagged = [PSObject]@{ + OperationMode = 'Untagged' + } + + $MockAdapterVlanTagged = [PSObject]@{ + OperationMode = 'Access' + AccessVlanId = '1' + } + + Describe "$($Global:DSCResourceName)\Get-TargetResource" { + #Function placeholders + function Get-VMNetworkAdapter { } + function Set-VMNetworkAdapter { } + function Remove-VMNetworkAdapter { } + function Get-VMNetworkAdapterVlan { } + function Add-VMNetworkAdapter { } + function Get-NetworkInformation { } + Context 'NetAdapter does not exist' { + Mock Get-VMNetworkAdapter + Mock Get-VMNetworkAdapterVlan + It 'should return ensure as absent' { + $Result = Get-TargetResource ` + @TestAdapter + $Result.Ensure | Should Be 'Absent' + } + It 'should call the expected mocks' { + Assert-MockCalled -commandName Get-VMNetworkAdapter -Exactly 1 + Assert-MockCalled -commandName Get-VMNetworkAdapterVlan -Exactly 0 + } + } + + Context 'NetAdapter exists' { + Mock -CommandName Get-VMNetworkAdapter -MockWith { + $MockAdapter + } + Mock -CommandName Get-VMNetworkAdapterVlan -MockWith { + $MockAdapterVlanUntagged + } + Mock -CommandName Get-NetworkInformation -MockWith { + return @{ + IpAddress = '10.10.10.10' + Subnet = '255.255.255.0' + DefaultGateway = '10.10.10.1' + DnsServer = '10.10.10.1' + } + } + + It 'should return adapter properties' { + $Result = Get-TargetResource @TestAdapter + $Result.Ensure | Should Be 'Present' + $Result.Name | Should Be $TestAdapter.Name + $Result.SwitchName | Should Be $TestAdapter.SwitchName + $Result.VMName | Should Be 'ManagementOS' + $Result.Id | Should Be $TestAdapter.Id + $Result.VlanId | Should -BeNullOrEmpty + $Result.NetworkSetting | Should -Not -BeNullOrEmpty + } + It 'should call the expected mocks' { + Assert-MockCalled -commandName Get-VMNetworkAdapter -Exactly 1 + Assert-MockCalled -commandName Get-VMNetworkAdapterVlan -Exactly 1 + } + } + + Context 'NetAdapter exists' { + Mock -CommandName Get-VMNetworkAdapter -MockWith { + $MockAdapter + } + Mock -CommandName Get-VMNetworkAdapterVlan -MockWith { + $MockAdapterVlanTagged + } + + It 'should return adapter properties' { + $Result = Get-TargetResource @TestAdapter + $Result.Ensure | Should Be 'Present' + $Result.Name | Should Be $TestAdapter.Name + $Result.SwitchName | Should Be $TestAdapter.SwitchName + $Result.VMName | Should Be 'ManagementOS' + $Result.Id | Should Be $TestAdapter.Id + $Result.VlanId | Should Be '1' + } + It 'should call the expected mocks' { + Assert-MockCalled -commandName Get-VMNetworkAdapter -Exactly 1 + Assert-MockCalled -commandName Get-VMNetworkAdapterVlan -Exactly 1 + } + } + } + + Describe "$($Global:DSCResourceName)\Set-TargetResource" { + #Function placeholders + function Get-VMNetworkAdapter { } + function Get-VMNetworkAdapterVlan { } + function Set-VMNetworkAdapter { } + function Set-VMNetworkAdapterVlan { } + function Remove-VMNetworkAdapter { } + function Add-VMNetworkAdapter { } + function Get-NetworkInformation { } + function Set-NetworkInformation { } + + $newAdapter = [PSObject]@{ + Id = 'UniqueString' + Name = $TestAdapter.Name + SwitchName = $TestAdapter.SwitchName + VMName = 'VMName' + NetworkSetting = $networkSettingsStatic + Ensure = 'Present' + } + + Context 'Adapter does not exist but should' { + + Mock Get-VMNetworkAdapter + Mock Get-VMNetworkAdapterVlan + Mock Add-VMNetworkAdapter + Mock Remove-VMNetworkAdapter + Mock Set-VMNetworkAdapterVlan + Mock Set-NetworkInformation + + It 'should not throw error' { + { + Set-TargetResource @newAdapter + } | Should Not Throw + } + It 'should call expected Mocks' { + Assert-MockCalled -commandName Get-VMNetworkAdapter -Exactly 1 + Assert-MockCalled -CommandName Set-VMNetworkAdapterVlan -Exactly 0 + Assert-MockCalled -commandName Add-VMNetworkAdapter -Exactly 1 + Assert-MockCalled -commandName Remove-VMNetworkAdapter -Exactly 0 + Assert-MockCalled -CommandName Set-NetworkInformation -Exactly 1 + } + } + + Context 'Adapter exists but should not exist' { + Mock Get-VMNetworkAdapter + Mock Add-VMNetworkAdapter + Mock Remove-VMNetworkAdapter + Mock Set-VMNetworkAdapterVlan + + It 'should not throw error' { + { + $updateAdapter = $newAdapter.Clone() + $updateAdapter.Ensure = 'Absent' + Set-TargetResource @updateAdapter + } | Should Not Throw + } + It 'should call expected Mocks' { + Assert-MockCalled -commandName Get-VMNetworkAdapter -Exactly 1 + Assert-MockCalled -commandName Add-VMNetworkAdapter -Exactly 0 + Assert-MockCalled -commandName Remove-VMNetworkAdapter -Exactly 1 + Assert-MockCalled -CommandName Set-VMNetworkAdapterVlan -Exactly 0 + } + } + } + + Describe "$($Global:DSCResourceName)\Test-TargetResource" { + #Function placeholders + function Get-VMNetworkAdapter { } + function Get-VMNetworkAdapterVlan { } + function Set-VMNetworkAdapter { } + function Remove-VMNetworkAdapter { } + function Add-VMNetworkAdapter { } + function Get-NetworkInformation { } + + $newAdapter = [PSObject]@{ + Id = 'UniqueString' + Name = $TestAdapter.Name + SwitchName = $TestAdapter.SwitchName + VMName = 'ManagementOS' + Ensure = 'Present' + } + + Context 'Adapter does not exist but should' { + Mock Get-VMNetworkAdapter + Mock Get-VMNetworkAdapterVlan + + It 'should return false' { + Test-TargetResource @newAdapter | Should be $false + } + It 'should call expected Mocks' { + Assert-MockCalled -commandName Get-VMNetworkAdapter -Exactly 1 + } + } + + Context 'Adapter exists but should not exist' { + Mock Get-VMNetworkAdapter -MockWith { $MockAdapter } + + It 'should return $false' { + $updateAdapter = $newAdapter.Clone() + $updateAdapter.Ensure = 'Absent' + Test-TargetResource @updateAdapter | Should Be $false + } + It 'should call expected Mocks' { + Assert-MockCalled -commandName Get-VMNetworkAdapter -Exactly 1 + } + } + + Context 'Adapter exists and no action needed without Vlan tag' { + Mock Get-VMNetworkAdapter -MockWith { $MockAdapter } + + It 'should return true' { + $updateAdapter = $newAdapter.Clone() + Test-TargetResource @updateAdapter | Should Be $true + } + It 'should call expected Mocks' { + Assert-MockCalled -commandName Get-VMNetworkAdapter -Exactly 1 + } + } + + Context 'Adapter exists and no action needed with Vlan tag' { + Mock Get-VMNetworkAdapter -MockWith { $MockAdapter } + Mock Get-VMNetworkAdapterVlan -MockWith { $MockAdapterVlanTagged } + Mock -CommandName Get-NetworkInformation + + It 'should return true' { + $updateAdapter = $newAdapter.Clone() + $updateAdapter.VMName = "VMName" + $updateAdapter.MacAddress = '14FEB5C6CE98' + $updateAdapter.VlanId = '1' + Test-TargetResource @updateAdapter | Should Be $true + } + It 'should call expected Mocks' { + Assert-MockCalled -commandName Get-VMNetworkAdapter -Exactly 1 + Assert-MockCalled -commandName Get-VMNetworkAdapterVlan -Exactly 1 + } + } + + Context 'Adapter exists but Vlan is not tagged' { + Mock Get-VMNetworkAdapter -MockWith { $MockAdapter } + Mock Get-VMNetworkAdapterVlan + Mock -CommandName Get-NetworkInformation + + It 'should return false' { + $updateAdapter = $newAdapter.Clone() + $updateAdapter.VMName = "VMName" + $updateAdapter.MacAddress = '14FEB5C6CE98' + $updateAdapter.VlanId = '1' + Test-TargetResource @updateAdapter | Should Be $false + } + It 'should call expected Mocks' { + Assert-MockCalled -commandName Get-VMNetworkAdapter -Exactly 1 + Assert-MockCalled -commandName Get-VMNetworkAdapterVlan -Exactly 1 + } + } + + Context 'Adapter exists but Vlan tag is wrong' { + Mock Get-VMNetworkAdapter -MockWith { $MockAdapter } + Mock Get-VMNetworkAdapterVlan -MockWith { $MockAdapterVlanTagged } + Mock -CommandName Get-NetworkInformation + + It 'should return false' { + $updateAdapter = $newAdapter.Clone() + $updateAdapter.VMName = "VMName" + $updateAdapter.MacAddress = '14FEB5C6CE98' + $updateAdapter.VlanId = '2' + Test-TargetResource @updateAdapter | Should Be $false + } + It 'should call expected Mocks' { + Assert-MockCalled -commandName Get-VMNetworkAdapter -Exactly 1 + Assert-MockCalled -commandName Get-VMNetworkAdapterVlan -Exactly 1 + } + } + + Context 'Adapter does not exist and no action needed' { + Mock Get-VMNetworkAdapter + + It 'should return true' { + $updateAdapter = $newAdapter.Clone() + $updateAdapter.Ensure = 'Absent' + Test-TargetResource @updateAdapter | Should Be $true + } + It 'should call expected Mocks' { + Assert-MockCalled -commandName Get-VMNetworkAdapter -Exactly 1 + } + } + + Context 'Adapter exists but network settings are not correct' { + Mock Get-VMNetworkAdapter -MockWith { $MockAdapter } + Mock Get-VMNetworkAdapterVlan -MockWith { $MockAdapterVlanTagged } + Mock -CommandName Get-NetworkInformation -MockWith { + @{ Dhcp = $false } + } + + It 'should return false' { + $updateAdapter = $newAdapter.Clone() + $updateAdapter.VMName = "VMName" + $updateAdapter.MacAddress = '14FEB5C6CE98' + Test-TargetResource @updateAdapter | Should Be $false + } + It 'should call expected Mocks' { + Assert-MockCalled -commandName Get-VMNetworkAdapter -Exactly 1 + Assert-MockCalled -commandName Get-NetworkInformation -Exactly 1 + } + } + } + + } + #endregion +} +finally +{ + #region FOOTER + Restore-TestEnvironment -TestEnvironment $TestEnvironment + #endregion +} diff --git a/deployment/dsc/azshcihost/xHyper-V/3.17.0.0/Tests/Unit/MSFT_xVMProcessor.Tests.ps1 b/deployment/dsc/azshcihost/xHyper-V/3.17.0.0/Tests/Unit/MSFT_xVMProcessor.Tests.ps1 new file mode 100644 index 0000000..88b37d7 --- /dev/null +++ b/deployment/dsc/azshcihost/xHyper-V/3.17.0.0/Tests/Unit/MSFT_xVMProcessor.Tests.ps1 @@ -0,0 +1,343 @@ +$script:DSCModuleName = 'xHyper-V' +$script:DSCResourceName = 'MSFT_xVMProcessor' + +#region HEADER + +# Unit Test Template Version: 1.2.0 +$script:moduleRoot = Split-Path -Parent (Split-Path -Parent $PSScriptRoot) +if ( (-not (Test-Path -Path (Join-Path -Path $script:moduleRoot -ChildPath 'DSCResource.Tests'))) -or ` + (-not (Test-Path -Path (Join-Path -Path $script:moduleRoot -ChildPath 'DSCResource.Tests\TestHelper.psm1'))) ) +{ + & git @('clone','https://github.com/PowerShell/DscResource.Tests.git',(Join-Path -Path $script:moduleRoot -ChildPath '\DSCResource.Tests\')) +} + +Import-Module -Name (Join-Path -Path $script:moduleRoot -ChildPath (Join-Path -Path 'DSCResource.Tests' -ChildPath 'TestHelper.psm1')) -Force + +$TestEnvironment = Initialize-TestEnvironment ` + -DSCModuleName $script:DSCModuleName ` + -DSCResourceName $script:DSCResourceName ` + -TestType Unit + +#endregion HEADER + +function Invoke-TestSetup { + +} + +function Invoke-TestCleanup { + Restore-TestEnvironment -TestEnvironment $TestEnvironment +} + +# Begin Testing +try +{ + Invoke-TestSetup + + InModuleScope $script:DSCResourceName { + + $testVMName = 'UnitTestVM' + $testResourcePoolName = 'Unit Test Resource Pool' + + Describe 'MSFT_xVMProcessor\Get-TargetResource' { + + $fakeVMProcessor = @{ + EnableHostResourceProtection = $true + } + + # Guard mocks + Mock Assert-Module { } + + function Get-VMProcessor { + [CmdletBinding()] + param + ( + [System.String] + $VMName + ) + } + + It 'Should return a [System.Collections.Hashtable] object type' { + Mock Get-VMProcessor { return $fakeVMProcessor } + + $result = Get-TargetResource -VMName $testVMName + + $result -is [System.Collections.Hashtable] | Should Be $true + } + + It 'Should assert Hyper-V module is installed' { + Mock Assert-Module { } + Mock Get-VMProcessor { return $fakeVMProcessor } + + $null = Get-TargetResource -VMName $testVMName + + Assert-MockCalled Assert-Module -ParameterFilter { $Name -eq 'Hyper-V' } -Scope It + } + + It 'Should throw when VM processor is not found' { + Mock Get-Module { return $true } + Mock Get-VMProcessor { Write-Error 'Not Found' } + { $null = Get-TargetResource -VMName $testVMName } | Should Throw 'Not Found' + } + } # descrive Get-TargetResource + + Describe 'MSFT_xVMProcessor\Test-TargetResource' { + + # Guard mocks + Mock Assert-Module { } + Mock Assert-TargetResourceParameter { } + + function Get-VM { + param ( + [System.String] + $Name + ) + } + + function Get-VMProcessor { + param ( + [System.String] + $VMName + ) + } + + function Set-VMProcessor { + param ( + [System.String] + $VMName + ) + } + + $fakeTargetResource = @{ + VMName = $testVMName + EnableHostResourceProtection = $true + ExposeVirtualizationExtensions = $true + HwThreadCountPerCore = 1 + Maximum = 99 + MaximumCountPerNumaNode = 4 + MaximumCountPerNumaSocket = 1 + RelativeWeight = 99 + Reserve = 0 + ResourcePoolName = $testResourcePoolName + CompatibilityForMigrationEnabled = $false + CompatibilityForOlderOperatingSystemsEnabled = $false + } + + It 'Should return a [System.Boolean] object type' { + Mock Get-TargetResource { return $fakeTargetResource } + + $result = Test-TargetResource -VMName $testVMName + + $result -is [System.Boolean] | Should Be $true + } + + It 'Should assert Hyper-V module is installed' { + Mock Get-VMProcessor { return $fakeVMProcessor } + + $null = Test-TargetResource -VMName $testVMName + + Assert-MockCalled Assert-Module -ParameterFilter { $Name -eq 'Hyper-V' } -Scope It + } + + It 'Should assert parameter values are valid' { + Mock Get-VMProcessor { return $fakeVMProcessor } + + $null = Test-TargetResource -VMName $testVMName + + Assert-MockCalled Assert-TargetResourceParameter -Scope It + } + + $parameterNames = @( + 'EnableHostResourceProtection', + 'ExposeVirtualizationExtensions', + 'HwThreadCountPerCore', + 'Maximum', + 'MaximumCountPerNumaNode', + 'MaximumCountPerNumaSocket', + 'RelativeWeight', + 'Reserve', + 'ResourcePoolName', + 'CompatibilityForMigrationEnabled', + 'CompatibilityForOlderOperatingSystemsEnabled' + ) + + # Test each individual parameter value separately + foreach ($parameterName in $parameterNames) + { + $parameterValue = $fakeTargetResource[$parameterName] + $testTargetResourceParams = @{ + VMName = $testVMName + } + + # Pass value verbatim so it should always pass first + It "Should pass when parameter '$parameterName' is correct" { + $testTargetResourceParams[$parameterName] = $parameterValue + + $result = Test-TargetResource @testTargetResourceParams + + $result | Should Be $true + } + + if ($parameterValue -is [System.Boolean]) + { + # Invert parameter value to cause a test failure + $testTargetResourceParams[$parameterName] = -not $parameterValue + } + elseif ($parameterValue -is [System.String]) + { + # Repeat string to cause a test failure + $testTargetResourceParams[$parameterName] = "$parameterValue$parameterValue" + } + elseif ($parameterValue -is [System.Int32] -or $parameterValue -is [System.Int64]) + { + # Add one to cause a test failure + $testTargetResourceParams[$parameterName] = $parameterValue + 1 + } + + It "Should fail when parameter '$parameterName' is incorrect" { + $result = Test-TargetResource @testTargetResourceParams + + $result | Should Be $false + } + } + } # describe Test-TargetResource + + Describe 'MSFT_xVMProcessor\Set-TargetResource' { + + function Get-VM { + param + ( + [System.String] + $Name + ) + } + + function Get-VMProcessor { + param + ( + [System.String] + $VMName + ) + } + + function Set-VMProcessor { + param + ( + [System.String] + $VMName + ) + } + + # Guard mocks + Mock Assert-Module { } + Mock Assert-TargetResourceParameter { } + Mock Get-VM { } + Mock Set-VMProcessor { } + Mock Set-VMProperty { } + + It 'Should assert Hyper-V module is installed' { + $null = Set-TargetResource -VMName $testVMName + + Assert-MockCalled Assert-Module -ParameterFilter { $Name -eq 'Hyper-V' } -Scope It + } + + It 'Should assert parameter values are valid' { + $null = Set-TargetResource -VMName $testVMName + + Assert-MockCalled Assert-TargetResourceParameter -Scope It + } + + $restartRequiredParameters = @{ + 'ExposeVirtualizationExtensions' = $false; + 'CompatibilityForMigrationEnabled' = $true; + 'CompatibilityForOlderOperatingSystemsEnabled' = $true; + 'HwThreadCountPerCore' = 2; + 'MaximumCountPerNumaNode' = 2 + 'MaximumCountPerNumaSocket' = 2 + 'ResourcePoolName' = $testResourcePoolName; + } + + foreach ($parameter in $restartRequiredParameters.GetEnumerator()) + { + $setTargetResourceParams = @{ + VMName = $testVMName; + $parameter.Name = $parameter.Value; + } + + It "Should not throw when VM is off, '$($parameter.Name)' is specified and 'RestartIfNeeded' is False" { + Mock Get-VM { return @{ State = 'Off' } } + + { Set-TargetResource @setTargetResourceParams } | Should Not Throw + } + + It "Should throw when VM is running, '$($parameter.Name)' is specified and 'RestartIfNeeded' is False" { + Mock Get-VM { return @{ State = 'Running' } } + + { Set-TargetResource @setTargetResourceParams } | Should Throw + } + + It "Should shutdown VM when running, '$($parameter.Name)' is specified and 'RestartIfNeeded' is True" { + Mock Get-VM { return @{ State = 'Running' } } + + Set-TargetResource @setTargetResourceParams -RestartIfNeeded $true + + Assert-MockCalled Set-VMProperty -Scope It -Exactly 1 + } + } + + $noRestartRequiredParameters = @{ + 'EnableHostResourceProtection' = $true; + 'Maximum' = 50; + 'RelativeWeight' = 50; + 'Reserve' = 50; + } + + foreach ($parameter in $noRestartRequiredParameters.GetEnumerator()) + { + $setTargetResourceParams = @{ + VMName = $testVMName; + $parameter.Name = $parameter.Value; + } + + It "Should not shutdown VM running and '$($parameter.Name) is specified" { + Mock Get-VM { return @{ State = 'Running' } } + + Set-TargetResource @setTargetResourceParams + + Assert-MockCalled Set-VMProcessor -Scope It -Exactly 1 + Assert-MockCalled Set-VMProperty -Scope It -Exactly 0 + } + } + } # describe Set-TargetResource + + Describe 'MSFT_xVMProcessor\Assert-TargetResourceParameter' { + + # Return Windows Server 2012 R2/Windows 8.1 Update 1 + Mock Get-CimInstance { return @{ BuildNumber = '9600' } } + + It "Should not throw when parameter 'ResourcePoolName' is specified on 2012 R2 host" { + { Assert-TargetResourceParameter -ResourcePoolName 'TestPool' } | Should Not Throw + } + + $server2016OnlyParameters = @{ + EnableHostResourceProtection = $true; + ExposeVirtualizationExtensions = $true; + HwThreadCountPerCore = 1; + } + + foreach ($parameter in $server2016OnlyParameters.GetEnumerator()) + { + $assertTargetResourceParameterParams = @{ + $parameter.Name = $parameter.Value; + } + + It "Should throw when parameter '$($parameter.Name)' is specified on 2012 R2 host" { + { Assert-TargetResourceParameter @assertTargetResourceParameterParams } | Should Throw '14393' + } + } + } # describe Assert-TargetResourceParameter + } # InModuleScope +} +finally +{ + Invoke-TestCleanup +} diff --git a/deployment/dsc/azshcihost/xHyper-V/3.17.0.0/Tests/Unit/MSFT_xVMScsiController.Tests.ps1 b/deployment/dsc/azshcihost/xHyper-V/3.17.0.0/Tests/Unit/MSFT_xVMScsiController.Tests.ps1 new file mode 100644 index 0000000..64f534d --- /dev/null +++ b/deployment/dsc/azshcihost/xHyper-V/3.17.0.0/Tests/Unit/MSFT_xVMScsiController.Tests.ps1 @@ -0,0 +1,352 @@ +$script:DSCModuleName = 'xHyper-V' +$script:DSCResourceName = 'MSFT_xVMScsiController' + +#region HEADER + +# Unit Test Template Version: 1.2.0 +$script:moduleRoot = Split-Path -Parent (Split-Path -Parent $PSScriptRoot) +if ( (-not (Test-Path -Path (Join-Path -Path $script:moduleRoot -ChildPath 'DSCResource.Tests'))) -or ` + (-not (Test-Path -Path (Join-Path -Path $script:moduleRoot -ChildPath 'DSCResource.Tests\TestHelper.psm1'))) ) +{ + & git @('clone','https://github.com/PowerShell/DscResource.Tests.git',(Join-Path -Path $script:moduleRoot -ChildPath '\DSCResource.Tests\')) +} + +Import-Module -Name (Join-Path -Path $script:moduleRoot -ChildPath (Join-Path -Path 'DSCResource.Tests' -ChildPath 'TestHelper.psm1')) -Force + +$TestEnvironment = Initialize-TestEnvironment ` + -DSCModuleName $script:DSCModuleName ` + -DSCResourceName $script:DSCResourceName ` + -TestType Unit + +#endregion HEADER + +function Invoke-TestSetup { + +} + +function Invoke-TestCleanup { + Restore-TestEnvironment -TestEnvironment $TestEnvironment +} + +# Begin Testing +try +{ + Invoke-TestSetup + + InModuleScope $script:DSCResourceName { + + $testVMName = 'UnitTestVM' + + Describe 'MSFT_xVMScsiController\Get-TargetResource' { + + $stubScsiController = @{ + VMName = $testVMName + ControllerNumber = 0 + } + + # Guard mocks + Mock Assert-Module { } + + function Get-VMScsiController { + [CmdletBinding()] + param + ( + [System.String] + $VMName, + + [System.Int32] + $ControllerNumber + ) + } + + It 'Should return a [System.Collections.Hashtable] object type' { + Mock Get-VMScsiController { return $stubScsiController } + + $result = Get-TargetResource -VMName $testVMName -ControllerNumber 0 + + $result -is [System.Collections.Hashtable] | Should Be $true + } + + It 'Should return "Present" when controller is attached' { + Mock Get-VMScsiController { return $stubScsiController } + + $result = Get-TargetResource -VMName $testVMName -ControllerNumber 0 + + $result.Ensure | Should Be 'Present' + } + + It 'Should return "Absent" when controller is not attached' { + Mock Get-VMScsiController { } + + $result = Get-TargetResource -VMName $testVMName -ControllerNumber 0 + + $result.Ensure | Should Be 'Absent' + } + + It 'Should assert Hyper-V module is installed' { + Mock Assert-Module { } + Mock Get-VMScsiController { } + + $null = Get-TargetResource -VMName $testVMName -ControllerNumber 0 + + Assert-MockCalled Assert-Module -ParameterFilter { $Name -eq 'Hyper-V' } -Scope It + } + } # descrive Get-TargetResource + + Describe 'MSFT_xVMScsiController\Test-TargetResource' { + + # Guard mocks + Mock Assert-Module { } + + $stubTargetResource = @{ + VMName = $testVMName + ControllerNumber = 0 + Ensure = 'Present' + } + + It 'Should return a [System.Boolean] object type' { + Mock Get-TargetResource { return $stubTargetResource } + $testTargetResourceParams = @{ + VMName = $testVMName + ControllerNumber = 0 + } + + $result = Test-TargetResource @testTargetResourceParams + + $result -is [System.Boolean] | Should Be $true + } + + It "Should pass when parameter 'Ensure' is correct" { + Mock Get-TargetResource { return $stubTargetResource } + $testTargetResourceParams = @{ + VMName = $testVMName + ControllerNumber = 0 + Ensure = $stubTargetResource['Ensure'] + } + + $result = Test-TargetResource @testTargetResourceParams + + $result | Should Be $true + } + + It "Should fail when parameter 'Ensure' is incorrect" { + Mock Get-TargetResource { return $stubTargetResource } + $testTargetResourceParams = @{ + VMName = $testVMName + ControllerNumber = 0 + Ensure = 'Absent' + } + + $result = Test-TargetResource @testTargetResourceParams + + $result | Should Be $false + } + } # describe Test-TargetResource + + Describe 'MSFT_xVMScsiController\Set-TargetResource' { + + function Get-VMScsiController { + param + ( + [System.String] + $VMName + ) + } + + function Add-VMScsiController { + param + ( + [System.String] + $VMName + ) + } + + function Remove-VMScsiController { + param + ( + [System.String] + $VMName + ) + } + + function Remove-VMHardDiskDrive { + param ( + [System.Object] + $VMHardDiskDrive + ) + } + + # Guard mocks + Mock Assert-Module { } + Mock Get-VMScsiController { } + Mock Add-VMScsiController { } + Mock Remove-VMScsiController { } + Mock Remove-VMHardDiskDrive { } + Mock Set-VMState { } + + It 'Should assert Hyper-V module is installed' { + Mock Get-VMHyperV { return @{ State = 'Running' } } + $setTargetResourceParams = @{ + VMName = $testVMName + ControllerNumber = 0 + RestartIfNeeded = $true + } + + $null = Set-TargetResource @setTargetResourceParams + + Assert-MockCalled Assert-Module + } + + It 'Should throw if "RestartIfNeeded" is not specified and VM is "Running"' { + Mock Get-VMHyperV { return @{ State = 'Running' } } + $setTargetResourceParams = @{ + VMName = $testVMName + ControllerNumber = 0 + } + + { Set-TargetResource @setTargetResourceParams } | Should Throw 'RestartIfNeeded' + } + + It 'Should not throw if "RestartIfNeeded" is not specified and VM is "Off"' { + Mock Get-VMHyperV { return @{ State = 'Off' } } + $setTargetResourceParams = @{ + VMName = $testVMName + ControllerNumber = 0 + } + + { Set-TargetResource @setTargetResourceParams } | Should Not Throw + } + + It 'Should call "Set-VMState" to stop running VM' { + Mock Get-VMHyperV { return @{ State = 'Running' } } + $setTargetResourceParams = @{ + VMName = $testVMName + ControllerNumber = 0 + RestartIfNeeded = $true + } + + $null = Set-TargetResource @setTargetResourceParams + + Assert-MockCalled Set-VMState -ParameterFilter { $State -eq 'Off' } -Scope It + } + + It 'Should call "Set-VMState" to restore VM to its previous state' { + $testVMState = 'Paused' + Mock Get-VMHyperV { return @{ State = $testVMState } } + $setTargetResourceParams = @{ + VMName = $testVMName + ControllerNumber = 0 + RestartIfNeeded = $true + } + + $null = Set-TargetResource @setTargetResourceParams + + Assert-MockCalled Set-VMState -ParameterFilter { $State -eq $testVMState } -Scope It + } + + It 'Should add single controller when it does not exist' { + Mock Get-VMHyperV { return @{ State = 'Running' } } + Mock Get-VMScsiController { } + $setTargetResourceParams = @{ + VMName = $testVMName + ControllerNumber = 0 + RestartIfNeeded = $true + } + + $null = Set-TargetResource @setTargetResourceParams + + Assert-MockCalled Add-VMScsiController -Scope It -Exactly 1 + } + + It 'Should add single controller when one already exists' { + Mock Get-VMHyperV { return @{ State = 'Running' } } + $fakeVMScsiController = [PSCustomObject] @{ ControllerNumber = 0 } + Mock Get-VMScsiController { return $fakeVMScsiController } + $setTargetResourceParams = @{ + VMName = $testVMName + ControllerNumber = 1 + RestartIfNeeded = $true + } + + $null = Set-TargetResource @setTargetResourceParams + + Assert-MockCalled Add-VMScsiController -Scope It -Exactly 1 + } + + It 'Should throw when adding controller when intermediate controller(s) do not exist' { + Mock Get-VMHyperV { return @{ State = 'Running' } } + Mock Get-VMScsiController { } + $setTargetResourceParams = @{ + VMName = $testVMName + ControllerNumber = 1 + RestartIfNeeded = $true + } + + { Set-TargetResource @setTargetResourceParams } | Should Throw 'Cannot add controller' + } + + It 'Should remove controller when Ensure = "Absent"' { + Mock Get-VMHyperV { return @{ State = 'Running' } } + $fakeVMScsiControllers = @( + [PSCustomObject] @{ ControllerNumber = 0 } + [PSCustomObject] @{ ControllerNumber = 1 } + ) + Mock Get-VMScsiController { return $fakeVMScsiControllers } + $setTargetResourceParams = @{ + VMName = $testVMName + ControllerNumber = 1 + RestartIfNeeded = $true + Ensure = 'Absent' + } + + $null = Set-TargetResource @setTargetResourceParams -WarningAction SilentlyContinue + + Assert-MockCalled Remove-VMScsiController -Scope It + } + + It 'Should remove all attached disks when Ensure = "Absent"' { + Mock Get-VMHyperV { return @{ State = 'Running' } } + $fakeVMScsiController = [PSCustomObject] @{ + ControllerNumber = 0 + Drives = @( + [PSCustomObject] @{ Name = 'Hard Drive on SCSI controller number 0 at location 0' } + [PSCustomObject] @{ Name = 'Hard Drive on SCSI controller number 0 at location 1' } + ) + } + Mock Get-VMScsiController { return $fakeVMScsiController } + $setTargetResourceParams = @{ + VMName = $testVMName + ControllerNumber = 0 + RestartIfNeeded = $true + Ensure = 'Absent' + } + + $null = Set-TargetResource @setTargetResourceParams -WarningAction SilentlyContinue + + Assert-MockCalled Remove-VMHardDiskDrive -Scope It -Exactly ($fakeVMScsiController.Drives.Count) + } + + It 'Should throw removing a controller when additional/subsequent controller(s) exist' { + Mock Get-VMHyperV { return @{ State = 'Running' } } + $fakeVMScsiControllers = @( + [PSCustomObject] @{ ControllerNumber = 0 } + [PSCustomObject] @{ ControllerNumber = 1 } + ) + Mock Get-VMScsiController { return $fakeVMScsiControllers } + $setTargetResourceParams = @{ + VMName = $testVMName + ControllerNumber = 0 + RestartIfNeeded = $true + Ensure = 'Absent' + } + + { Set-TargetResource @setTargetResourceParams } | Should Throw 'Cannot remove controller' + } + + } # describe Set-TargetResource + } # InModuleScope +} +finally +{ + Invoke-TestCleanup +} diff --git a/deployment/dsc/azshcihost/xHyper-V/3.17.0.0/Tests/Unit/MSFT_xVMSwitch_BandwidthReservationMode.Tests.ps1 b/deployment/dsc/azshcihost/xHyper-V/3.17.0.0/Tests/Unit/MSFT_xVMSwitch_BandwidthReservationMode.Tests.ps1 new file mode 100644 index 0000000..eb29268 --- /dev/null +++ b/deployment/dsc/azshcihost/xHyper-V/3.17.0.0/Tests/Unit/MSFT_xVMSwitch_BandwidthReservationMode.Tests.ps1 @@ -0,0 +1,549 @@ +#region HEADER + +# Unit Test Template Version: 1.2.0 +$script:moduleRoot = Split-Path -Parent (Split-Path -Parent $PSScriptRoot) +if ( (-not (Test-Path -Path (Join-Path -Path $script:moduleRoot -ChildPath 'DSCResource.Tests'))) -or ` + (-not (Test-Path -Path (Join-Path -Path $script:moduleRoot -ChildPath 'DSCResource.Tests\TestHelper.psm1'))) ) +{ + & git @('clone', 'https://github.com/PowerShell/DscResource.Tests.git', (Join-Path -Path $script:moduleRoot -ChildPath '\DSCResource.Tests\')) +} + +Import-Module -Name (Join-Path -Path $script:moduleRoot -ChildPath (Join-Path -Path 'DSCResource.Tests' -ChildPath 'TestHelper.psm1')) -Force + +$TestEnvironment = Initialize-TestEnvironment ` + -DSCModuleName 'xHyper-V' ` + -DSCResourceName 'MSFT_xVMSwitch' ` + -TestType Unit + +#endregion HEADER + +function Invoke-TestSetup +{ + +} + +function Invoke-TestCleanup +{ + Restore-TestEnvironment -TestEnvironment $TestEnvironment +} + +# Begin Testing +try +{ + Invoke-TestSetup + + InModuleScope 'MSFT_xVMSwitch' { + + <# + Defines a variable that contains all the possible Bandwidth Reservation Modes which will be used + for foreach loops later on + #> + New-Variable -Name 'BANDWIDTH_RESERVATION_MODES' -Option 'Constant' -Value @('Default', 'Weight', 'Absolute', 'None') + + # Function to create a exception object for testing output exceptions + function Get-InvalidArgumentError + { + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $ErrorId, + + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $ErrorMessage + ) + + $exception = New-Object -TypeName System.ArgumentException ` + -ArgumentList $ErrorMessage + $errorCategory = [System.Management.Automation.ErrorCategory]::InvalidArgument + $errorRecord = New-Object -TypeName System.Management.Automation.ErrorRecord ` + -ArgumentList $exception, $ErrorId, $errorCategory, $null + return $errorRecord + } # end function Get-InvalidArgumentError + + # A helper function to mock a VMSwitch + function New-MockedVMSwitch + { + Param ( + [Parameter(Mandatory = $true)] + [string] + $Name, + + [Parameter(Mandatory = $true)] + [ValidateSet('Default', 'Weight', 'Absolute', 'None', 'NA')] + [string] + $BandwidthReservationMode, + + [Parameter()] + [bool] + $AllowManagementOS = $false + ) + + $mockedVMSwitch = @{ + Name = $Name + SwitchType = 'External' + AllowManagementOS = $AllowManagementOS + NetAdapterInterfaceDescription = 'Microsoft Network Adapter Multiplexor Driver' + } + + if ($BandwidthReservationMode -ne 'NA') + { + $mockedVMSwitch['BandwidthReservationMode'] = $BandwidthReservationMode + } + + return [PsObject]$mockedVMSwitch + } + + Describe 'Validates Get-TargetResource Function' { + # Create an empty function to be able to mock the missing Hyper-V cmdlet + function Get-VMSwitch + { + + } + + <# + Mocks Get-VMSwitch and will return $global:mockedVMSwitch which is + a variable that is created during most It statements to mock a VMSwitch + #> + Mock -CommandName Get-VMSwitch -MockWith { + param + ( + [string] + $ErrorAction + ) + + if ($ErrorAction -eq 'Stop' -and $global:mockedVMSwitch -eq $null) + { + throw [System.Management.Automation.ActionPreferenceStopException]'No switch can be found by given criteria.' + } + + return $global:mockedVMSwitch + } + + # Mocks Get-NetAdapter which returns a simplified network adapter + Mock -CommandName Get-NetAdapter -MockWith { + return [PSCustomObject]@{ + Name = 'SomeNIC' + InterfaceDescription = 'Microsoft Network Adapter Multiplexor Driver' + } + } + + # Mocks "Get-Module -Name Hyper-V" so that the DSC resource thinks the Hyper-V module is on the test system + Mock -CommandName Get-Module -ParameterFilter { ($Name -eq 'Hyper-V') -and ($ListAvailable -eq $true) } -MockWith { + return $true + } + + # Create all the test cases for Get-TargetResource + $getTestCases = @() + foreach ($brmMode in $BANDWIDTH_RESERVATION_MODES) { + $getTestCases += @{ + CurrentName = $brmMode + 'BRM' + CurrentBandwidthReservationMode = $brmMode + } + } + + # Test Get-TargetResource with the test cases created above + It 'Current switch''s BandwidthReservationMode is set to <CurrentBandwidthReservationMode>' -TestCases $getTestCases { + param + ( + [Parameter()] + [string] + $CurrentName, + + [Parameter()] + [string] + $CurrentBandwidthReservationMode + ) + + # Set the mocked VMSwitch to be returned from Get-VMSwitch based on the input from $getTestCases + $global:mockedVMSwitch = New-MockedVMSwitch -Name $CurrentName -BandwidthReservationMode $CurrentBandwidthReservationMode + + $targetResource = Get-TargetResource -Name $CurrentName -Type 'External' + $targetResource -is [System.Collections.Hashtable] | Should Be $true + $targetResource['BandwidthReservationMode'] | Should Be $CurrentBandwidthReservationMode + + Remove-Variable -Scope 'Global' -Name 'mockedVMSwitch' -ErrorAction 'SilentlyContinue' + } + + <# + Test Get-TargetResource when the VMSwitch's BandwidthReservationMode member variable is not + set which simulates older versions of Windows that don't support it + #> + It 'BandwidthReservationMode is set to null' { + # Set the mocked VMSwitch to be returned from Get-VMSwitch + $global:mockedVMSwitch = New-MockedVMSwitch -Name 'NaBRM' -BandwidthReservationMode 'NA' + + $targetResource = Get-TargetResource -Name 'NaBRM' -Type 'External' + $targetResource -is [System.Collections.Hashtable] | Should Be $true + $targetResource['BandwidthReservationMode'] | Should Be "NA" + + Remove-Variable -Scope 'Global' -Name 'mockedVMSwitch' -ErrorAction 'SilentlyContinue' + } + } + + # Create all the test cases for Test-TargetResource and Set-TargetResource when the switch already exists + $testSetTestCases = @() + foreach ($currentBrmMode in $BANDWIDTH_RESERVATION_MODES) + { + foreach ($desiredBrmMode in $BANDWIDTH_RESERVATION_MODES) + { + foreach ($ensureOption in @('Present', 'Absent')) + { + $case = @{ + CurrentName = $currentBrmMode + 'BRM' + CurrentBandwidthReservationMode = $currentBrmMode + DesiredName = $desiredBrmMode + 'BRM' + DesiredBandwidthReservationMode = $desiredBrmMode + Ensure = $ensureOption + ExpectedResult = $ensureOption -eq 'Present' -and $currentBrmMode -eq $desiredBrmMode + } + $testSetTestCases += $case + } + } + } + + # Create all the test cases for Test-TargetResource and Set-TargetResource when the switch does not exists + foreach ($desiredBrmMode in $BANDWIDTH_RESERVATION_MODES) + { + foreach ($ensureOption in @('Present', 'Absent')) + { + $case = @{ + CurrentName = $null + CurrentBandwidthReservationMode = $null + DesiredName = $desiredBrmMode + 'BRM' + DesiredBandwidthReservationMode = $desiredBrmMode + Ensure = $ensureOption + ExpectedResult = $ensureOption -eq 'Absent' + } + $testSetTestCases += $case + } + } + + Describe 'Validates Test-TargetResource Function' { + # Create an empty function to be able to mock the missing Hyper-V cmdlet + function Get-VMSwitch + { + + } + + <# + Mocks Get-VMSwitch and will return $global:mockedVMSwitch which is + a variable that is created during most It statements to mock a VMSwitch + #> + Mock -CommandName Get-VMSwitch -MockWith { + param ( + [string] + $ErrorAction + ) + + if ($ErrorAction -eq 'Stop' -and $global:mockedVMSwitch -eq $null) + { + throw [System.Management.Automation.ActionPreferenceStopException]'No switch can be found by given criteria.' + } + + return $global:mockedVMSwitch + } + + # Mocks Get-NetAdapter which returns a simplified network adapter + Mock -CommandName Get-NetAdapter -MockWith { + return [PSCustomObject]@{ + Name = 'SomeNIC' + InterfaceDescription = 'Microsoft Network Adapter Multiplexor Driver' + } + } + + # Mocks "Get-Module -Name Hyper-V" so that the DSC resource thinks the Hyper-V module is on the test system + Mock -CommandName Get-Module -ParameterFilter { ($Name -eq 'Hyper-V') -and ($ListAvailable -eq $true) } -MockWith { + return $true + } + + Mock -CommandName Get-OSVersion -MockWith { + return [Version]::Parse('6.3.9600') + } + + # Create all the test cases for Get-TargetResource + $getTestCases = @() + foreach ($brmMode in $BANDWIDTH_RESERVATION_MODES) + { + $getTestCases += @{ + CurrentName = $brmMode + 'BRM' + CurrentBandwidthReservationMode = $brmMode + } + } + + # Test Test-TargetResource with the test cases created above + It 'Current Name "<CurrentName>" | Current BandwidthReservationMode set to "<CurrentBandwidthReservationMode>" | Desired BandwidthReservationMode set to "<DesiredBandwidthReservationMode>" | Ensure "<Ensure>"' -TestCases $testSetTestCases { + param + ( + [Parameter()] + [string] + $CurrentName, + + [Parameter()] + [string] + $CurrentBandwidthReservationMode, + + [Parameter()] + [string] + $DesiredName, + + [Parameter()] + [string] + $DesiredBandwidthReservationMode, + + [Parameter()] + [string] + $Ensure, + + [Parameter()] + [bool] + $ExpectedResult + ) + + # Set the mocked VMSwitch to be returned from Get-VMSwitch if the switch exists + if ($CurrentName) + { + $global:mockedVMSwitch = New-MockedVMSwitch -Name $CurrentName -BandwidthReservationMode $CurrentBandwidthReservationMode -AllowManagementOS $true + } + + $targetResource = Test-TargetResource -Name $DesiredName -BandwidthReservationMode $DesiredBandwidthReservationMode -Type 'External' -NetAdapterName 'SomeNIC' -Ensure $Ensure -AllowManagementOS $true + $targetResource | Should Be $ExpectedResult + + Remove-Variable -Scope 'Global' -Name 'mockedVMSwitch' -ErrorAction 'SilentlyContinue' + } + + Mock -CommandName Get-OSVersion -MockWith { + return [Version]::Parse('6.1.7601') + } + + # Test Test-TargetResource when the version of Windows doesn't support BandwidthReservationMode + It 'Invalid Operating System Exception' { + $errorRecord = Get-InvalidArgumentError ` + -ErrorId 'BandwidthReservationModeError' ` + -ErrorMessage $LocalizedData.BandwidthReservationModeError + {Test-TargetResource -Name 'WeightBRM' -Type 'External' -NetAdapterName 'SomeNIC' -AllowManagementOS $true -BandwidthReservationMode 'Weight' -Ensure 'Present'} | Should Throw $errorRecord + } + + # Test Test-TargetResource when the version of Windows doesn't support BandwidthReservationMode and specifies NA for BandwidthReservationMode + It 'Simulates Windows Server 2008 R2 | Desired BandwidthReservationMode set to "NA" | Ensure Present | Expected Result is True' { + $global:mockedVMSwitch = New-MockedVMSwitch -Name 'SomeSwitch' -BandwidthReservationMode 'NA' -AllowManagementOS $true + $targetResource = Test-TargetResource -Name 'SomeSwitch' -BandwidthReservationMode 'NA' -Type 'External' -NetAdapterName 'SomeNIC' -Ensure 'Present' -AllowManagementOS $true + $targetResource | Should Be $true + } + + It 'Passes when "BandwidthReservationMode" does not match but is not specified (#48)' { + $global:mockedVMSwitch = New-MockedVMSwitch -Name 'SomeSwitch' -BandwidthReservationMode 'Absolute' + $targetResource = Test-TargetResource -Name 'SomeSwitch' -Type 'Internal' -Ensure 'Present' + $targetResource | Should Be $true + } + } + + Describe 'Validates Set-TargetResource Function' { + # Create empty functions to be able to mock the missing Hyper-V cmdlet + function Get-VMSwitch + { + + } + + function New-VMSwitch + { + + } + + function Remove-VMSwitch + { + + } + + function Set-VMSwitch + { + + } + + <# + Mocks Get-VMSwitch and will return $global:mockedVMSwitch which is + a variable that is created during most It statements to mock a VMSwitch + #> + Mock -CommandName Get-VMSwitch -MockWith { + param + ( + [string] + $Name, + + [string] + $SwitchType, + + [string] + $ErrorAction + ) + + if ($ErrorAction -eq 'Stop' -and $global:mockedVMSwitch -eq $null) + { + throw [System.Management.Automation.ActionPreferenceStopException]'No switch can be found by given criteria.' + } + + return $global:mockedVMSwitch + } + + <# + Mocks New-VMSwitch and will assign a mocked switch to $global:mockedVMSwitch. This returns $global:mockedVMSwitch + which is a variable that is created during most It statements to mock a VMSwitch + #> + Mock -CommandName New-VMSwitch -MockWith { + param + ( + [string] + $Name, + + [string] + $NetAdapterName, + + [string] + $MinimumBandwidthMode, + + [bool] + $AllowManagementOS + ) + + $global:mockedVMSwitch = New-MockedVMSwitch -Name $Name -BandwidthReservationMode $MinimumBandwidthMode -AllowManagementOS $AllowManagementOS + return $global:mockedVMSwitch + } + + <# + Mocks Set-VMSwitch and will modify $global:mockedVMSwitch which is + a variable that is created during most It statements to mock a VMSwitch + #> + Mock -CommandName Set-VMSwitch -MockWith { + param + ( + [bool] + $AllowManagementOS + ) + + if ($AllowManagementOS) + { + $global:mockedVMSwitch['AllowManagementOS'] = $AllowManagementOS + } + } + + <# + Mocks Remove-VMSwitch and will remove the variable $global:mockedVMSwitch which is + a variable that is created during most It statements to mock a VMSwitch + #> + Mock -CommandName Remove-VMSwitch -MockWith { + $global:mockedVMSwitch = $null + } + + # Mocks Get-NetAdapter which returns a simplified network adapter + Mock -CommandName Get-NetAdapter -MockWith { + return [PSCustomObject]@{ + Name = 'SomeNIC' + InterfaceDescription = 'Microsoft Network Adapter Multiplexor Driver' + } + } + + # Mocks "Get-Module -Name Hyper-V" so that the DSC resource thinks the Hyper-V module is on the test system + Mock -CommandName Get-Module -ParameterFilter { ($Name -eq 'Hyper-V') -and ($ListAvailable -eq $true) } -MockWith { + return $true + } + + Mock -CommandName Get-OSVersion -MockWith { + return [Version]::Parse('6.3.9600') + } + + # Create all the test cases for Get-TargetResource + $getTestCases = @() + foreach ($brmMode in $BANDWIDTH_RESERVATION_MODES) + { + $getTestCases += @{ + CurrentName = $brmMode + 'BRM' + CurrentBandwidthReservationMode = $brmMode + } + } + + It 'Current Name "<CurrentName>" | Current BandwidthReservationMode set to "<CurrentBandwidthReservationMode>" | Desired BandwidthReservationMode set to "<DesiredBandwidthReservationMode>" | Ensure "<Ensure>"' -TestCases $testSetTestCases { + param + ( + [Parameter()] + [string] + $CurrentName, + + [Parameter()] + [string] + $CurrentBandwidthReservationMode, + + [Parameter()] + [string] + $DesiredName, + + [Parameter()] + [string] + $DesiredBandwidthReservationMode, + + [Parameter()] + [string] + $Ensure, + + [Parameter()] + [bool] + $ExpectedResult + ) + + # Set the mocked VMSwitch to be returned from Get-VMSwitch if the switch exists + if ($CurrentName) + { + $global:mockedVMSwitch = New-MockedVMSwitch -Name $CurrentName -BandwidthReservationMode $CurrentBandwidthReservationMode -AllowManagementOS $true + } + + $targetResource = Set-TargetResource -Name $DesiredName -BandwidthReservationMode $DesiredBandwidthReservationMode -Type 'External' -NetAdapterName 'SomeNIC' -Ensure $Ensure -AllowManagementOS $true + $targetResource | Should Be $null + + if ($CurrentName -and $Ensure -eq 'Present') + { + if ($DesiredBandwidthReservationMode -ne $CurrentBandwidthReservationMode) + { + Assert-MockCalled -CommandName Get-VMSwitch -Times 2 -Scope 'It' + Assert-MockCalled -CommandName Remove-VMSwitch -Times 1 -Scope 'It' + Assert-MockCalled -CommandName New-VMSwitch -Times 1 -Scope 'It' + Assert-MockCalled -CommandName Set-VMSwitch -Times 0 -Scope 'It' + } + else + { + Assert-MockCalled -CommandName Get-VMSwitch -Times 1 -Scope 'It' + } + } + elseif ($Ensure -eq 'Present') + { + Assert-MockCalled -CommandName Get-VMSwitch -Times 1 -Scope 'It' + Assert-MockCalled -CommandName New-VMSwitch -Times 1 -Scope 'It' + } + else + { + Assert-MockCalled -CommandName Get-VMSwitch -Times 1 -Scope 'It' + Assert-MockCalled -CommandName Remove-VMSwitch -Times 1 -Scope 'It' + } + Remove-Variable -Scope 'Global' -Name 'mockedVMSwitch' -ErrorAction 'SilentlyContinue' + } + + # Test Set-TargetResource when the version of Windows doesn't support BandwidthReservationMode + It 'Invalid Operating System Exception' { + Mock -CommandName Get-OSVersion -MockWith { + return [Version]::Parse('6.1.7601') + } + + $errorRecord = Get-InvalidArgumentError ` + -ErrorId 'BandwidthReservationModeError' ` + -ErrorMessage $LocalizedData.BandwidthReservationModeError + {Set-TargetResource -Name 'WeightBRM' -Type 'External' -NetAdapterName 'SomeNIC' -AllowManagementOS $true -BandwidthReservationMode 'Weight' -Ensure 'Present'} | Should Throw $errorRecord + } + } + } +} +finally +{ + Invoke-TestCleanup +} diff --git a/deployment/dsc/azshcihost/xHyper-V/3.17.0.0/Tests/Unit/MSFT_xVMSwitch_EnableEmbeddedTeaming.Tests.ps1 b/deployment/dsc/azshcihost/xHyper-V/3.17.0.0/Tests/Unit/MSFT_xVMSwitch_EnableEmbeddedTeaming.Tests.ps1 new file mode 100644 index 0000000..205f8ed --- /dev/null +++ b/deployment/dsc/azshcihost/xHyper-V/3.17.0.0/Tests/Unit/MSFT_xVMSwitch_EnableEmbeddedTeaming.Tests.ps1 @@ -0,0 +1,550 @@ +#region HEADER + +# Unit Test Template Version: 1.2.0 +$script:moduleRoot = Split-Path -Parent (Split-Path -Parent $PSScriptRoot) +if ( (-not (Test-Path -Path (Join-Path -Path $script:moduleRoot -ChildPath 'DSCResource.Tests'))) -or ` + (-not (Test-Path -Path (Join-Path -Path $script:moduleRoot -ChildPath 'DSCResource.Tests\TestHelper.psm1'))) ) +{ + & git @('clone', 'https://github.com/PowerShell/DscResource.Tests.git', (Join-Path -Path $script:moduleRoot -ChildPath '\DSCResource.Tests\')) +} + +Import-Module -Name (Join-Path -Path $script:moduleRoot -ChildPath (Join-Path -Path 'DSCResource.Tests' -ChildPath 'TestHelper.psm1')) -Force + +$TestEnvironment = Initialize-TestEnvironment ` + -DSCModuleName 'xHyper-V' ` + -DSCResourceName 'MSFT_xVMSwitch' ` + -TestType Unit + +#endregion HEADER + +function Invoke-TestSetup +{ + +} + +function Invoke-TestCleanup +{ + Restore-TestEnvironment -TestEnvironment $TestEnvironment +} + +# Begin Testing +try +{ + Invoke-TestSetup + + InModuleScope 'MSFT_xVMSwitch' { + # Function to create a exception object for testing output exceptions + function Get-InvalidArgumentError + { + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $ErrorId, + + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $ErrorMessage + ) + + $exception = New-Object -TypeName System.ArgumentException ` + -ArgumentList $ErrorMessage + $errorCategory = [System.Management.Automation.ErrorCategory]::InvalidArgument + $errorRecord = New-Object -TypeName System.Management.Automation.ErrorRecord ` + -ArgumentList $exception, $ErrorId, $errorCategory, $null + return $errorRecord + } # end function Get-InvalidArgumentError + + # A helper function to mock a VMSwitch + function New-MockedVMSwitch + { + param ( + [Parameter(Mandatory = $true)] + [string] + $Name, + + [Parameter(Mandatory = $true)] + [ValidateSet('Default', 'Weight', 'Absolute', 'None', 'NA')] + [string] + $BandwidthReservationMode, + + [parameter()] + [ValidateSet('Dynamic','HyperVPort')] + [String] + $LoadBalancingAlgorithm, + + [Parameter()] + [bool] + $AllowManagementOS = $false + ) + + $mockedVMSwitch = @{ + Name = $Name + SwitchType = 'External' + AllowManagementOS = $AllowManagementOS + NetAdapterInterfaceDescription = 'Microsoft Network Adapter Multiplexor Driver' + } + + if ($BandwidthReservationMode -ne 'NA') + { + $mockedVMSwitch['BandwidthReservationMode'] = $BandwidthReservationMode + } + + if($PSBoundParameters.ContainsKey('LoadBalancingAlgorithm')) + { + $mockedVMSwitch['LoadBalancingAlgorithm'] = $LoadBalancingAlgorithm + } + + return [PsObject]$mockedVMSwitch + } + + Describe "MSFT_xVMSwitch" { + # Create empty functions to be able to mock the missing Hyper-V cmdlet + function Get-VMSwitch + { + + } + + function New-VMSwitch + { + + } + + function Set-VMSwitch + { + + } + + function Remove-VMSwitch + { + + } + + function Get-VMSwitchTeam + { + + } + + function Set-VMSwitchTeam + { + + } + + <# + Mocks Get-VMSwitch and will return $global:mockedVMSwitch which is + a variable that is created during most It statements to mock a VMSwitch + #> + Mock -CommandName Get-VMSwitch -MockWith { + param + ( + [string] + $Name, + + [string] + $SwitchType, + + [string] + $ErrorAction + ) + + if ($ErrorAction -eq 'Stop' -and $global:mockedVMSwitch -eq $null) + { + throw [System.Management.Automation.ActionPreferenceStopException]'No switch can be found by given criteria.' + } + + return $global:mockedVMSwitch + } + + <# + Mocks New-VMSwitch and will assign a mocked switch to $global:mockedVMSwitch. This returns $global:mockedVMSwitch + which is a variable that is created during most It statements to mock a VMSwitch + #> + Mock -CommandName New-VMSwitch -MockWith { + param + ( + [string] + $Name, + + [string[]] + $NetAdapterName, + + [string] + $MinimumBandwidthMode = 'NA', + + [bool] + $EnableEmbeddedTeaming, + + [bool] + $AllowManagementOS + ) + + $global:mockedVMSwitch = New-MockedVMSwitch -Name $Name -BandwidthReservationMode $MinimumBandwidthMode -AllowManagementOS $AllowManagementOS + #is SET is enabled mok a VMSwitchTeam + if($EnableEmbeddedTeaming){ + $global:mockedVMSwitchTeam = [PSCustomObject]@{ + Name = "TestSwitch" + Id = [Guid]::NewGuid() + TeamingMode = 'SwitchIndependent' + LoadBalancingAlgorithm = 'Dynamic' + } + } + return $global:mockedVMSwitch + } + + Mock -CommandName Get-OSVersion -MockWith { + return @{ + Major = 10 + } + } + + <# + Mocks Set-VMSwitch and will modify $global:mockedVMSwitch which is + a variable that is created during most It statements to mock a VMSwitch + #> + Mock -CommandName Set-VMSwitch -MockWith { + param + ( + [bool] + $AllowManagementOS + ) + + if ($AllowManagementOS) + { + $global:mockedVMSwitch['AllowManagementOS'] = $AllowManagementOS + } + } + + <# + Mocks Remove-VMSwitch and will remove the variable $global:mockedVMSwitch which is + a variable that is created during most It statements to mock a VMSwitch + #> + Mock -CommandName Remove-VMSwitch -MockWith { + $global:mockedVMSwitch = $null + } + + <# + Mocks Get-VMSwitchTeam and will return a moked VMSwitchTeam + #> + Mock -CommandName Get-VMSwitchTeam -MockWith { + return $global:mockedVMSwitchTeam + } + + <# + Mocks Set-VMSwitchTeam and will return a moked VMSwitchTeam + #> + Mock -CommandName Set-VMSwitchTeam -MockWith { + param + ( + [parameter(Mandatory=$true)] + [ValidateSet('Dynamic','HyperVPort')] + [String] + $LoadBalancingAlgorithm, + + [String] + $Name + ) + + $global:mockedVMSwitchTeam.LoadBalancingAlgorithm = $LoadBalancingAlgorithm + } + + # Mocks Get-NetAdapter which returns a simplified network adapter + Mock -CommandName Get-NetAdapter -MockWith { + return @( + [PSCustomObject]@{ + Name = 'NIC1' + InterfaceDescription = 'Microsoft Network Adapter Multiplexor Driver #1' + } + [PSCustomObject]@{ + Name = 'NIC2' + InterfaceDescription = 'Microsoft Network Adapter Multiplexor Driver #2' + } + ) + } + + # Mocks "Get-Module -Name Hyper-V" so that the DSC resource thinks the Hyper-V module is on the test system + Mock -CommandName Get-Module -ParameterFilter { ($Name -eq 'Hyper-V') -and ($ListAvailable -eq $true) } -MockWith { + return $true + } + + Context "A virtual switch with embedded teaming does not exist but should" { + $global:mockedVMSwitch = $null + + $testParams = @{ + Name = "TestSwitch" + Type = "External" + NetAdapterName = @("NIC1", "NIC2") + AllowManagementOS = $true + EnableEmbeddedTeaming = $true + BandwidthReservationMode = "NA" + Ensure = "Present" + } + + It "Should return absent in the get method" { + (Get-TargetResource -Name $testParams.Name -Type $testParams.Type).Ensure | Should Be "Absent" + } + + It "Should return false in the test method" { + Test-TargetResource @testParams | Should Be $false + } + + It "Should run the set method without exceptions" { + Set-TargetResource @testParams + Assert-MockCalled -CommandName "New-VMSwitch" -Times 1 + } + } + + Context "A virtual switch with embedded teaming exists and should" { + $global:mockedVMSwitch = @{ + Name = "TestSwitch" + SwitchType = "External" + AllowManagementOS = $true + EmbeddedTeamingEnabled = $true + Id = [Guid]::NewGuid() + NetAdapterInterfaceDescriptions = @("Microsoft Network Adapter Multiplexor Driver #1", "Microsoft Network Adapter Multiplexor Driver #2") + } + + $testParams = @{ + Name = "TestSwitch" + Type = "External" + NetAdapterName = @("NIC1", "NIC2") + AllowManagementOS = $true + EnableEmbeddedTeaming = $true + BandwidthReservationMode = "NA" + Ensure = "Present" + } + + It "Should return present in the get method" { + (Get-TargetResource -Name $testParams.Name -Type $testParams.Type).Ensure | Should Be "Present" + } + + It "Should return true in the test method" { + Test-TargetResource @testParams | Should Be $true + } + } + + Context "A virtual switch with embedded teaming exists but does not refer to the correct adapters" { + $global:mockedVMSwitch = @{ + Name = "TestSwitch" + SwitchType = "External" + AllowManagementOS = $true + EmbeddedTeamingEnabled = $true + Id = [Guid]::NewGuid() + NetAdapterInterfaceDescriptions = @("Wrong adapter", "Microsoft Network Adapter Multiplexor Driver #2") + } + + Mock -CommandName Get-NetAdapter -MockWith { + return @( + [PSCustomObject]@{ + Name = 'WrongNic' + InterfaceDescription = 'Wrong adapter' + } + [PSCustomObject]@{ + Name = 'NIC2' + InterfaceDescription = 'Microsoft Network Adapter Multiplexor Driver #2' + } + ) + } + + $testParams = @{ + Name = "TestSwitch" + Type = "External" + NetAdapterName = @("NIC1", "NIC2") + AllowManagementOS = $true + EnableEmbeddedTeaming = $true + BandwidthReservationMode = "NA" + Ensure = "Present" + } + + It "Should return present in the get method" { + (Get-TargetResource -Name $testParams.Name -Type $testParams.Type).Ensure | Should Be "Present" + } + + It "Should return false in the test method" { + Test-TargetResource @testParams | Should Be $false + } + + It "Should run the set method without exceptions" { + Set-TargetResource @testParams + Assert-MockCalled -CommandName "Remove-VMSwitch" -Times 1 + Assert-MockCalled -CommandName "New-VMSwitch" -Times 1 + } + } + + Context "A virtual switch with embedded teaming exists but does not use the correct LB algorithm" { + $global:mockedVMSwitch = @{ + Name = "TestSwitch" + SwitchType = "External" + AllowManagementOS = $true + EmbeddedTeamingEnabled = $true + LoadBalancingAlgorithm = 'Dynamic' + Id = [Guid]::NewGuid() + NetAdapterInterfaceDescriptions = @("Microsoft Network Adapter Multiplexor Driver #1", "Microsoft Network Adapter Multiplexor Driver #2") + } + + Mock -CommandName Get-NetAdapter -MockWith { + return @( + [PSCustomObject]@{ + Name = 'NIC01' + InterfaceDescription = "Microsoft Network Adapter Multiplexor Driver #1" + } + [PSCustomObject]@{ + Name = 'NIC2' + InterfaceDescription = 'Microsoft Network Adapter Multiplexor Driver #2' + } + ) + } + + $testParams = @{ + Name = "TestSwitch" + Type = "External" + NetAdapterName = @("NIC1", "NIC2") + AllowManagementOS = $true + EnableEmbeddedTeaming = $true + LoadBalancingAlgorithm = 'HyperVPort' + Ensure = "Present" + } + + It "Should return present in the get method" { + (Get-TargetResource -Name $testParams.Name -Type $testParams.Type).Ensure | Should Be "Present" + } + + It "Should return false in the test method" { + Test-TargetResource @testParams | Should Be $false + } + + It "Should run the set method without exceptions" { + Set-TargetResource @testParams + Assert-MockCalled -CommandName "Remove-VMSwitch" -Times 1 + Assert-MockCalled -CommandName "New-VMSwitch" -Times 1 + } + } + + Context "A virtual switch without embedded teaming exists but should use embedded teaming" { + $global:mockedVMSwitch = @{ + Name = "TestSwitch" + SwitchType = "External" + AllowManagementOS = $true + EmbeddedTeamingEnabled = $false + Id = [Guid]::NewGuid() + NetAdapterInterfaceDescription = "Microsoft Network Adapter Multiplexor Driver #1" + } + + $testParams = @{ + Name = "TestSwitch" + Type = "External" + NetAdapterName = @("NIC1", "NIC2") + AllowManagementOS = $true + EnableEmbeddedTeaming = $true + BandwidthReservationMode = "NA" + Ensure = "Present" + } + + It "Should return present in the get method" { + (Get-TargetResource -Name $testParams.Name -Type $testParams.Type).Ensure | Should Be "Present" + } + + It "Should return false in the test method" { + Test-TargetResource @testParams | Should Be $false + } + + It "Should run the set method without exceptions" { + Set-TargetResource @testParams + Assert-MockCalled -CommandName "Remove-VMSwitch" -Times 1 + Assert-MockCalled -CommandName "New-VMSwitch" -Times 1 + } + } + + Context "A virtual switch with embedded teaming exists but shouldn't" { + $global:mockedVMSwitch = @{ + Name = "TestSwitch" + SwitchType = "External" + AllowManagementOS = $true + EmbeddedTeamingEnabled = $true + Id = [Guid]::NewGuid() + NetAdapterInterfaceDescriptions = @("Microsoft Network Adapter Multiplexor Driver #1", "Microsoft Network Adapter Multiplexor Driver #2") + } + + $testParams = @{ + Name = "TestSwitch" + Type = "Internal" + Ensure = "Absent" + } + + It "Should return present in the get method" { + (Get-TargetResource -Name $testParams.Name -Type $testParams.Type).Ensure | Should Be "Present" + } + + It "Should return false in the test method" { + Test-TargetResource @testParams | Should Be $false + } + + It "Should run the set method without exceptions" { + Set-TargetResource @testParams + Assert-MockCalled -CommandName "Remove-VMSwitch" -Times 1 + } + } + + Context "A virtual switch with embedded teaming does not exist and shouldn't" { + $global:mockedVMSwitch = $null + + $testParams = @{ + Name = "TestSwitch" + Type = "Internal" + Ensure = "Absent" + } + + It "Should return absent in the get method" { + (Get-TargetResource -Name $testParams.Name -Type $testParams.Type).Ensure | Should Be "Absent" + } + + It "Should return true in the test method" { + Test-TargetResource @testParams | Should Be $true + } + } + + Context "A server is not running Server 2016 and attempts to use embedded teaming" { + $global:mockedVMSwitch = $null + + $testParams = @{ + Name = "TestSwitch" + Type = "External" + NetAdapterName = @("NIC1", "NIC2") + AllowManagementOS = $true + EnableEmbeddedTeaming = $true + BandwidthReservationMode = "NA" + Ensure = "Present" + } + + Mock -CommandName Get-OSVersion -MockWith { + return [Version]::Parse('6.3.9600') + } + + It "Should return absent in the get method" { + (Get-TargetResource -Name $testParams.Name -Type $testParams.Type).Ensure | Should Be "Absent" + } + + It "Should throw an error in the test method" { + $errorRecord = Get-InvalidArgumentError ` + -ErrorId 'SETServer2016Error' ` + -ErrorMessage $LocalizedData.SETServer2016Error + + {Test-TargetResource @testParams} | Should Throw $errorRecord + } + + It "Should throw an error in the set method" { + $errorRecord = Get-InvalidArgumentError ` + -ErrorId 'SETServer2016Error' ` + -ErrorMessage $LocalizedData.SETServer2016Error + + {Set-TargetResource @testParams} | Should Throw $errorRecord + } + } + } + } +} +finally { + Invoke-TestCleanup +} diff --git a/deployment/dsc/azshcihost/xHyper-V/3.17.0.0/Tests/Unit/MSFT_xVMSwitch_Id.Tests.ps1 b/deployment/dsc/azshcihost/xHyper-V/3.17.0.0/Tests/Unit/MSFT_xVMSwitch_Id.Tests.ps1 new file mode 100644 index 0000000..32980a4 --- /dev/null +++ b/deployment/dsc/azshcihost/xHyper-V/3.17.0.0/Tests/Unit/MSFT_xVMSwitch_Id.Tests.ps1 @@ -0,0 +1,421 @@ + +#region HEADER +$script:DSCModuleName = 'xHyper-V' +$script:DSCResourceName = 'MSFT_xVMSwitch' + +# Unit Test Template Version: 1.2.4 +$script:moduleRoot = Split-Path -Parent (Split-Path -Parent $PSScriptRoot) +if ( (-not (Test-Path -Path (Join-Path -Path $script:moduleRoot -ChildPath 'DSCResource.Tests'))) -or ` + (-not (Test-Path -Path (Join-Path -Path $script:moduleRoot -ChildPath 'DSCResource.Tests\TestHelper.psm1'))) ) +{ + & git @('clone', 'https://github.com/PowerShell/DscResource.Tests.git', (Join-Path -Path $script:moduleRoot -ChildPath 'DscResource.Tests')) +} + +Import-Module -Name (Join-Path -Path $script:moduleRoot -ChildPath (Join-Path -Path 'DSCResource.Tests' -ChildPath 'TestHelper.psm1')) -Force + +$TestEnvironment = Initialize-TestEnvironment ` + -DSCModuleName $script:DSCModuleName ` + -DSCResourceName $script:DSCResourceName ` + -ResourceType 'Mof' ` + -TestType Unit + +#endregion HEADER + +function Invoke-TestSetup +{ +} + +function Invoke-TestCleanup +{ + Restore-TestEnvironment -TestEnvironment $TestEnvironment +} + +# Begin Testing +try +{ + Invoke-TestSetup + + InModuleScope $script:DSCResourceName { + + # A helper function to create a exception object for testing output exceptions + function Get-InvalidArgumentError + { + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $ErrorId, + + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $ErrorMessage + ) + + $exception = New-Object -TypeName System.ArgumentException ` + -ArgumentList $ErrorMessage + $errorCategory = [System.Management.Automation.ErrorCategory]::InvalidArgument + $errorRecord = New-Object -TypeName System.Management.Automation.ErrorRecord ` + -ArgumentList $exception, $ErrorId, $errorCategory, $null + return $errorRecord + } + + # A helper function to mock a VMSwitch + function New-MockedVMSwitch + { + param ( + [Parameter(Mandatory = $true)] + [String] + $Name, + + [Parameter()] + [ValidateNotNullOrEmpty()] + [Guid] + $Id + ) + + $mockedVMSwitch = @{ + Name = $Name + SwitchType = 'External' + AllowManagementOS = $true + EmbeddedTeamingEnabled = $true + LoadBalancingAlgorithm = 'HyperVPort' + BandwidthReservationMode = 'Default' + NetAdapterInterfaceDescriptions = @("Microsoft Network Adapter Multiplexor Driver #1", "Microsoft Network Adapter Multiplexor Driver #2") + } + + if ($PSBoundParameters.ContainsKey('Id')) + { + $mockedVMSwitch['Id'] = $Id + } + else + { + $mockedVMSwitch['Id'] = New-Guid + } + return [PsObject]$mockedVMSwitch + } + + # Mocks "Get-Module -Name Hyper-V" so that the DSC resource thinks the Hyper-V module is on the test system + Mock -CommandName Get-Module -ParameterFilter { ($Name -eq 'Hyper-V') -and ($ListAvailable -eq $true) } -MockWith { + return $true + } + + function Get-VMSwitch + { + } + Mock -CommandName Get-VMSwitch -MockWith { + return $Global:MockedVMSwitch + } + + function Get-NetAdapter + { + } + Mock -CommandName Get-NetAdapter -MockWith { + return @( + [PSCustomObject]@{ + Name = 'NIC1' + InterfaceDescription = 'Microsoft Network Adapter Multiplexor Driver #1' + } + [PSCustomObject]@{ + Name = 'NIC2' + InterfaceDescription = 'Microsoft Network Adapter Multiplexor Driver #2' + } + ) + } + + function Get-VMSwitchTeam + { + } + Mock -CommandName Get-VMSwitchTeam -MockWith { + return [PSCustomObject]@{ + Name = 'TestTeam' + Id = New-Guid + NetAdapterInterfaceDescription = @("Microsoft Network Adapter Multiplexor Driver #1", "Microsoft Network Adapter Multiplexor Driver #2") + TeamingMode = 'SwitchIndependent' + LoadBalancingAlgorithm = 'HyperVPort' + } + } + + function Remove-VMSwitch {} + Mock -CommandName Remove-VMSwitch -MockWith { + $Global:mockedVMSwitch = $null + } + + function New-VMSwitch {} + Mock -CommandName New-VMSwitch -MockWith { + Param( + [Parameter()] + [String] + $Name, + + [Parameter()] + [String[]] + $NetAdapterName, + + [Parameter()] + [String] + $MinimumBandwidthMode, + + [Parameter()] + [bool] + $AllowManagementOS, + + [Parameter()] + [String] + $SwitchType, + + [Parameter()] + [bool] + $EnableEmbeddedTeaming, + + [Parameter()] + [Guid] + $Id + ) + + if($PSBoundParameters.ContainsKey('Id')) + { + $Global:MockedVMSwitch = New-MockedVMSwitch -Name $Name -Id $id + } + else + { + $Global:MockedVMSwitch = New-MockedVMSwitch -Name $Name + } + } + + function Get-OSVersion + { + } + Mock -CommandName Get-OSVersion -MockWith { + [PSCustomObject]@{ + Major = 10 + Minor = 0 + Build = 14393 + Revision = 0 + MajorRevision = 0 + MinorRevision = 0 + } + } + + Describe 'MSFT_xVMSwitch\Get-TargetResource' -Tag 'Get' { + + Context 'When the system is in the desired state (VMSwitch has the desired Id)' { + $Global:MockedVMSwitch = New-MockedVMSwitch -Name 'TestSwitch' + + It 'Should return "present"' { + (Get-TargetResource -Name 'TestSwitch' -Type 'External').Ensure | Should Be 'Present' + Assert-MockCalled -CommandName "Get-VMSwitch" -Times 1 + Assert-MockCalled -CommandName "Get-VMSwitchTeam" -Times 1 + } + } + + Context 'When the system is not in the desired state (VMSwitch has not the desired Id)' { + + $Global:mockedVMSwitch = $null + + It 'Should return "absent"' { + (Get-TargetResource -Name 'TestSwitch' -Type 'External').Ensure | Should Be 'Absent' + Assert-MockCalled -CommandName "Get-VMSwitch" -Times 1 + Assert-MockCalled -CommandName "Get-VMSwitchTeam" -Times 0 + } + } + } + + Describe 'MSFT_xVMSwitch\Set-TargetResource' -Tag 'Set' { + + Context 'When the system is in the desired state (VMSwitch has the desired Id)' { + $desiredVMSwitchID = New-Guid + + $Global:mockedVMSwitch = New-MockedVMSwitch -Name 'TestSwitch' -Id $desiredVMSwitchID + + $testParams = @{ + Name = 'TestSwitch' + Type = 'External' + NetAdapterName = @('NIC1', 'NIC2') + AllowManagementOS = $true + EnableEmbeddedTeaming = $true + Ensure = 'Present' + Id = $desiredVMSwitchID + } + + It 'Should run without without exceptions' { + {Set-TargetResource @testParams} | Should -Not -Throw + Assert-MockCalled -CommandName "Get-VMSwitch" -Times 1 + Assert-MockCalled -CommandName 'Get-NetAdapter' -Times 1 + } + } + + Context 'When the system is not in the desired state (VMSwitch has not the desired Id)' { + + $Global:mockedVMSwitch = New-MockedVMSwitch -Name 'TestSwitch' + + $testParams = @{ + Name = 'TestSwitch' + Type = 'External' + NetAdapterName = @('NIC1', 'NIC2') + AllowManagementOS = $true + EnableEmbeddedTeaming = $true + Ensure = 'Present' + Id = New-Guid + } + + It 'Should run without exception while re-creating the VMSwitch' { + {Set-TargetResource @testParams} | Should -Not -Throw + Assert-MockCalled -CommandName "Get-VMSwitch" -Times 1 + Assert-MockCalled -CommandName 'Get-NetAdapter' -Times 1 + Assert-MockCalled -CommandName 'Remove-VMSwitch' -Times 1 + Assert-MockCalled -CommandName 'New-VMSwitch' -Times 1 + } + } + + Context 'When the specified value for Id parameter is not a GUID' { + + $Global:mockedVMSwitch = New-MockedVMSwitch -Name 'TestSwitch' + + $testParams = @{ + Name = 'TestSwitch' + Type = 'External' + NetAdapterName = @('NIC1', 'NIC2') + AllowManagementOS = $true + EnableEmbeddedTeaming = $true + Ensure = 'Present' + Id = '123' + } + + It 'Should throw "The VMSwitch Id must be in GUID format!"' { + {Set-TargetResource @testParams} | Should -Throw 'The VMSwitch Id must be in GUID format!' + } + } + + Context 'When the system is not running Server 2016' { + + $desiredVMSwitchID = New-Guid + + $Global:mockedVMSwitch = New-MockedVMSwitch -Name 'TestSwitch' -Id $desiredVMSwitchID + + $testParams = @{ + Name = 'TestSwitch' + Type = 'External' + NetAdapterName = 'NIC1' + AllowManagementOS = $true + EnableEmbeddedTeaming = $false + Ensure = 'Present' + Id = $desiredVMSwitchID + } + + Mock -CommandName Get-OSVersion -MockWith { + return [Version]::Parse('6.3.9600') + } + + $errorRecord = Get-InvalidArgumentError ` + -ErrorId 'VMSwitchIDServer2016Error' ` + -ErrorMessage $LocalizedData.VMSwitchIDServer2016Error + + It 'Should throw "VMSwitchIDServer2016Error"' { + {Set-TargetResource @testParams} | Should -Throw $errorRecord + } + } + } + + Describe 'MSFT_xVMSwitch\Test-TargetResource' -Tag 'Test' { + Context 'When the system is in the desired state (VMSwitch has the desired Id)' { + + $desiredVMSwitchID = New-Guid + + $Global:mockedVMSwitch = New-MockedVMSwitch -Name 'TestSwitch' -Id $desiredVMSwitchID + + $testParams = @{ + Name = 'TestSwitch' + Type = 'External' + NetAdapterName = @('NIC1', 'NIC2') + AllowManagementOS = $true + EnableEmbeddedTeaming = $true + Ensure = 'Present' + Id = $desiredVMSwitchID + } + + It 'Should return $true' { + {Test-TargetResource @testParams} | Should -Not -Throw + Assert-MockCalled -CommandName "Get-VMSwitch" -Times 1 + Assert-MockCalled -CommandName 'Get-NetAdapter' -Times 1 + } + } + + Context 'When the system is not in the desired state (VMSwitch has not the desired Id)' { + + $Global:mockedVMSwitch = New-MockedVMSwitch -Name 'TestSwitch' + + $testParams = @{ + Name = 'TestSwitch' + Type = 'External' + NetAdapterName = @('NIC1', 'NIC2') + AllowManagementOS = $true + EnableEmbeddedTeaming = $true + Ensure = 'Present' + Id = New-Guid + } + + It 'Should return $false' { + {Test-TargetResource @testParams} | Should -Not -Throw + Assert-MockCalled -CommandName "Get-VMSwitch" -Times 1 + Assert-MockCalled -CommandName 'Get-NetAdapter' -Times 1 + } + } + + Context 'When the specified value for Id parameter is not a GUID' { + + $Global:mockedVMSwitch = New-MockedVMSwitch -Name 'TestSwitch' + + $testParams = @{ + Name = 'TestSwitch' + Type = 'External' + NetAdapterName = @('NIC1', 'NIC2') + AllowManagementOS = $true + EnableEmbeddedTeaming = $true + Ensure = 'Present' + Id = '123' + } + + It 'Should throw "The VMSwitch Id must be in GUID format!"' { + {Test-TargetResource @testParams} | Should -Throw 'The VMSwitch Id must be in GUID format!' + } + } + + Context 'When the system is not running Server 2016' { + + $desiredVMSwitchID = New-Guid + + $Global:mockedVMSwitch = New-MockedVMSwitch -Name 'TestSwitch' -Id $desiredVMSwitchID + + $testParams = @{ + Name = 'TestSwitch' + Type = 'External' + NetAdapterName = 'NIC1' + AllowManagementOS = $true + EnableEmbeddedTeaming = $false + Ensure = 'Present' + Id = $desiredVMSwitchID + } + + Mock -CommandName Get-OSVersion -MockWith { + return [Version]::Parse('6.3.9600') + } + + $errorRecord = Get-InvalidArgumentError ` + -ErrorId 'VMSwitchIDServer2016Error' ` + -ErrorMessage $LocalizedData.VMSwitchIDServer2016Error + + It 'Should throw "VMSwitchIDServer2016Error"' { + {Test-TargetResource @testParams} | Should -Throw $errorRecord + } + } + + } + } +} +finally +{ + Invoke-TestCleanup +} diff --git a/deployment/dsc/azshcihost/xHyper-V/3.17.0.0/xHyper-V.psd1 b/deployment/dsc/azshcihost/xHyper-V/3.17.0.0/xHyper-V.psd1 new file mode 100644 index 0000000..e4809a6 --- /dev/null +++ b/deployment/dsc/azshcihost/xHyper-V/3.17.0.0/xHyper-V.psd1 @@ -0,0 +1,75 @@ +@{ +# Version number of this module. +moduleVersion = '3.17.0.0' + +# ID used to uniquely identify this module +GUID = 'f5a5f169-7026-4053-932a-19a7c37b1ca5' + +# Author of this module +Author = 'Microsoft Corporation' + +# Company or vendor of this module +CompanyName = 'Microsoft Corporation' + +# Copyright statement for this module +Copyright = '(c) 2017 Microsoft Corporation. All rights reserved.' + +# Description of the functionality provided by this module +Description = 'Module with DSC Resources for Hyper-V area' + +# Minimum version of the Windows PowerShell engine required by this module +PowerShellVersion = '4.0' + +# Minimum version of the common language runtime (CLR) required by this module +CLRVersion = '4.0' + +# Functions to export from this module +FunctionsToExport = '*' + +# Cmdlets to export from this module +CmdletsToExport = '*' + +# Private data to pass to the module specified in RootModule/ModuleToProcess. This may also contain a PSData hashtable with additional module metadata used by PowerShell. +PrivateData = @{ + + PSData = @{ + + # Tags applied to this module. These help with module discovery in online galleries. + Tags = @('DesiredStateConfiguration', 'DSC', 'DSCResourceKit', 'DSCResource') + + # A URL to the license for this module. + LicenseUri = 'https://github.com/PowerShell/xHyper-V/blob/master/LICENSE' + + # A URL to the main website for this project. + ProjectUri = 'https://github.com/PowerShell/xHyper-V' + + # A URL to an icon representing this module. + # IconUri = '' + + # ReleaseNotes of this module + ReleaseNotes = '* MSFT_xVMNetworkAdapter: + * Added NetworkSettings to be able to statically set IPAddress. + * Added option for Vlan tagging. You can now setup a Network Adapeter as an access switch on a specific Vlan. + +' + + } # End of PSData hashtable + +} # End of PrivateData hashtable +} + + + + + + + + + + + + + + + + diff --git a/deployment/dsc/azshcihost/xPSDesiredStateConfiguration/9.1.0/DSCResources/DSC_xArchive/DSC_xArchive.psm1 b/deployment/dsc/azshcihost/xPSDesiredStateConfiguration/9.1.0/DSCResources/DSC_xArchive/DSC_xArchive.psm1 new file mode 100644 index 0000000..53199d4 --- /dev/null +++ b/deployment/dsc/azshcihost/xPSDesiredStateConfiguration/9.1.0/DSCResources/DSC_xArchive/DSC_xArchive.psm1 @@ -0,0 +1,1712 @@ +$errorActionPreference = 'Stop' +Set-StrictMode -Version 'Latest' + +$modulePath = Join-Path -Path (Split-Path -Path (Split-Path -Path $PSScriptRoot -Parent) -Parent) -ChildPath 'Modules' + +# Import the shared modules +Import-Module -Name (Join-Path -Path $modulePath ` + -ChildPath (Join-Path -Path 'xPSDesiredStateConfiguration.Common' ` + -ChildPath 'xPSDesiredStateConfiguration.Common.psm1')) + +# Import Localization Strings +$script:localizedData = Get-LocalizedData -ResourceName 'DSC_xArchive' + +Add-Type -AssemblyName 'System.IO.Compression' + +# This resource has not yet been tested on a Nano server. +if (-not (Test-IsNanoServer)) +{ + Add-Type -AssemblyName 'System.IO.Compression.FileSystem' +} + +<# + .SYNOPSIS + Retrieves the current state of the archive resource with the specified path and + destination. + + The returned object provides the following properties: + Path: The specified path. + Destination: The specified destination. + Ensure: Present if the archive at the specified path is expanded at the specified + destination. Absent if the archive at the specified path is not expanded at the + specified destination. + + .PARAMETER Path + The path to the archive file that should or should not be expanded at the specified + destination. + + .PARAMETER Destination + The path where the archive file should or should not be expanded. + + .PARAMETER Validate + Specifies whether or not to validate that a file at the destination with the same name as a + file in the archive actually matches that corresponding file in the archive by the + specified checksum method. + + If a file does not match it will be considered not present. + + The default value is false. + + .PARAMETER Checksum + The Checksum method to use to validate whether or not a file at the destination with the + same name as a file in the archive actually matches that corresponding file in the archive. + + An invalid argument exception will be thrown if Checksum is specified while Validate is + specified as false. + + ModifiedDate will check that the LastWriteTime property of the file at the destination + matches the LastWriteTime property of the file in the archive. + CreatedDate will check that the CreationTime property of the file at the destination + matches the CreationTime property of the file in the archive. + SHA-1, SHA-256, and SHA-512 will check that the hash of the file at the destination by the + specified SHA method matches the hash of the file in the archive by the specified SHA + method. + + The default value is ModifiedDate. + + .PARAMETER Credential + The credential of a user account with permissions to access the specified archive path and + destination if needed. +#> +function Get-TargetResource +{ + [OutputType([System.Collections.Hashtable])] + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $Path, + + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $Destination, + + [Parameter()] + [System.Boolean] + $Validate = $false, + + [Parameter()] + [ValidateSet('SHA-1', 'SHA-256', 'SHA-512', 'CreatedDate', 'ModifiedDate')] + [System.String] + $Checksum = 'ModifiedDate', + + [Parameter()] + [System.Management.Automation.PSCredential] + [System.Management.Automation.Credential()] + $Credential + ) + + if ($PSBoundParameters.ContainsKey('Checksum') -and -not $Validate) + { + $errorMessage = $script:localizedData.ChecksumSpecifiedAndValidateFalse -f $Checksum, $Path, $Destination + New-InvalidArgumentException -ArgumentName 'Checksum or Validate' -Message $errorMessage + } + + $archiveState = @{ + Path = $Path + Destination = $Destination + } + + # In case an error occurs, we assume that the archive is not expanded at the destination + $archiveExpandedAtDestination = $false + + $psDrive = $null + + if ($PSBoundParameters.ContainsKey('Credential')) + { + $psDrive = Mount-PSDriveWithCredential -Path $Path -Credential $Credential + } + + try + { + Assert-PathExistsAsLeaf -Path $Path + Assert-DestinationDoesNotExistAsFile -Destination $Destination + + Write-Verbose -Message ($script:localizedData.RetrievingArchiveState -f $Path, $Destination) + + $testArchiveExistsAtDestinationParameters = @{ + ArchiveSourcePath = $Path + Destination = $Destination + } + + if ($Validate) + { + $testArchiveExistsAtDestinationParameters['Checksum'] = $Checksum + } + + if (Test-Path -LiteralPath $Destination) + { + Write-Verbose -Message ($script:localizedData.DestinationExists -f $Destination) + + $archiveExpandedAtDestination = Test-ArchiveExistsAtDestination @testArchiveExistsAtDestinationParameters + } + else + { + Write-Verbose -Message ($script:localizedData.DestinationDoesNotExist -f $Destination) + } + } + finally + { + if ($null -ne $psDrive) + { + Write-Verbose -Message ($script:localizedData.RemovingPSDrive -f $psDrive.Root) + + $null = Remove-PSDrive -Name $psDrive -Force -ErrorAction 'SilentlyContinue' + } + } + + if ($archiveExpandedAtDestination) + { + $archiveState['Ensure'] = 'Present' + } + else + { + $archiveState['Ensure'] = 'Absent' + } + + return $archiveState +} + +<# + .SYNOPSIS + Expands the archive (.zip) file at the specified path to the specified destination or + removes the expanded archive (.zip) file at the specified path from the specified + destination. + + .PARAMETER Path + The path to the archive file that should be expanded to or removed from the specified + destination. + + .PARAMETER Destination + The path where the specified archive file should be expanded to or removed from. + + .PARAMETER Ensure + Specifies whether or not the expanded content of the archive file at the specified path + should exist at the specified destination. + + To update the specified destination to have the expanded content of the archive file at the + specified path, specify this property as Present. + To remove the expanded content of the archive file at the specified path from the specified + destination, specify this property as Absent. + + The default value is Present. + + .PARAMETER Validate + Specifies whether or not to validate that a file at the destination with the same name as a + file in the archive actually matches that corresponding file in the archive by the + specified checksum method. + + If the file does not match and Ensure is specified as Present and Force is not specified, + the resource will throw an error that the file at the destination cannot be overwritten. + If the file does not match and Ensure is specified as Present and Force is specified, the + file at the destination will be overwritten. + If the file does not match and Ensure is specified as Absent, the file at the destination + will not be removed. + + The default value is false. + + .PARAMETER Checksum + The Checksum method to use to validate whether or not a file at the destination with the + same name as a file in the archive actually matches that corresponding file in the archive. + + An invalid argument exception will be thrown if Checksum is specified while Validate is + specified as false. + + ModifiedDate will check that the LastWriteTime property of the file at the destination + matches the LastWriteTime property of the file in the archive. + CreatedDate will check that the CreationTime property of the file at the destination + matches the CreationTime property of the file in the archive. + SHA-1, SHA-256, and SHA-512 will check that the hash of the file at the destination by the + specified SHA method matches the hash of the file in the archive by the specified SHA + method. + + The default value is ModifiedDate. + + .PARAMETER Credential + The credential of a user account with permissions to access the specified archive path and + destination if needed. + + .PARAMETER Force + Specifies whether or not any existing files or directories at the destination with the same + name as a file or directory in the archive should be overwritten to match the file or + directory in the archive. + + When this property is false, an error will be thrown if an item at the destination needs to + be overwritten. + + The default value is false. +#> +function Set-TargetResource +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $Path, + + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $Destination, + + [Parameter()] + [ValidateSet('Present', 'Absent')] + [System.String] + $Ensure = 'Present', + + [Parameter()] + [System.Boolean] + $Validate = $false, + + [Parameter()] + [ValidateSet('SHA-1', 'SHA-256', 'SHA-512', 'CreatedDate', 'ModifiedDate')] + [System.String] + $Checksum = 'ModifiedDate', + + [Parameter()] + [System.Management.Automation.PSCredential] + [System.Management.Automation.Credential()] + $Credential, + + [Parameter()] + [System.Boolean] + $Force = $false + ) + + if ($PSBoundParameters.ContainsKey('Checksum') -and -not $Validate) + { + $errorMessage = $script:localizedData.ChecksumSpecifiedAndValidateFalse -f $Checksum, $Path, $Destination + New-InvalidArgumentException -ArgumentName 'Checksum or Validate' -Message $errorMessage + } + + $psDrive = $null + + if ($PSBoundParameters.ContainsKey('Credential')) + { + $psDrive = Mount-PSDriveWithCredential -Path $Path -Credential $Credential + } + + try + { + Assert-PathExistsAsLeaf -Path $Path + Assert-DestinationDoesNotExistAsFile -Destination $Destination + + Write-Verbose -Message ($script:localizedData.SettingArchiveState -f $Path, $Destination) + + $expandArchiveToDestinationParameters = @{ + ArchiveSourcePath = $Path + Destination = $Destination + Force = $Force + } + + $removeArchiveFromDestinationParameters = @{ + ArchiveSourcePath = $Path + Destination = $Destination + } + + if ($Validate) + { + $expandArchiveToDestinationParameters['Checksum'] = $Checksum + $removeArchiveFromDestinationParameters['Checksum'] = $Checksum + } + + if (Test-Path -LiteralPath $Destination) + { + Write-Verbose -Message ($script:localizedData.DestinationExists -f $Destination) + + if ($Ensure -eq 'Present') + { + Expand-ArchiveToDestination @expandArchiveToDestinationParameters + } + else + { + Remove-ArchiveFromDestination @removeArchiveFromDestinationParameters + } + } + else + { + Write-Verbose -Message ($script:localizedData.DestinationDoesNotExist -f $Destination) + + if ($Ensure -eq 'Present') + { + Write-Verbose -Message ($script:localizedData.CreatingDirectoryAtDestination -f $Destination) + + $null = New-Item -Path $Destination -ItemType 'Directory' + Expand-ArchiveToDestination @expandArchiveToDestinationParameters + } + } + + Write-Verbose -Message ($script:localizedData.ArchiveStateSet -f $Path, $Destination) + } + finally + { + if ($null -ne $psDrive) + { + Write-Verbose -Message ($script:localizedData.RemovingPSDrive -f $psDrive.Root) + + $null = Remove-PSDrive -Name $psDrive -Force -ErrorAction 'SilentlyContinue' + } + } +} + +<# + .SYNOPSIS + Tests whether or not the archive (.zip) file at the specified path is expanded at the + specified destination. + + .PARAMETER Path + The path to the archive file that should or should not be expanded at the specified + destination. + + .PARAMETER Destination + The path where the archive file should or should not be expanded. + + .PARAMETER Ensure + Specifies whether or not the archive file should be expanded to the specified destination. + + To test whether the archive file is expanded at the specified destination, specify this + property as Present. + To test whether the archive file is not expanded at the specified destination, specify this + property as Absent. + + The default value is Present. + + .PARAMETER Validate + Specifies whether or not to validate that a file at the destination with the same name as a + file in the archive actually matches that corresponding file in the archive by the + specified checksum method. + + If a file does not match it will be considered not present. + + The default value is false. + + .PARAMETER Checksum + The Checksum method to use to validate whether or not a file at the destination with the + same name as a file in the archive actually matches that corresponding file in the archive. + + An invalid argument exception will be thrown if Checksum is specified while Validate is + specified as false. + + ModifiedDate will check that the LastWriteTime property of the file at the destination + matches the LastWriteTime property of the file in the archive. + CreatedDate will check that the CreationTime property of the file at the destination + matches the CreationTime property of the file in the archive. + SHA-1, SHA-256, and SHA-512 will check that the hash of the file at the destination by the + specified SHA method matches the hash of the file in the archive by the specified SHA + method. + + The default value is ModifiedDate. + + .PARAMETER Credential + The credential of a user account with permissions to access the specified archive path and + destination if needed. + + .PARAMETER Force + Not used in Test-TargetResource. +#> +function Test-TargetResource +{ + [OutputType([System.Boolean])] + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $Path, + + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $Destination, + + [Parameter()] + [ValidateSet('Present', 'Absent')] + [System.String] + $Ensure = 'Present', + + [Parameter()] + [System.Boolean] + $Validate = $false, + + [Parameter()] + [ValidateSet('SHA-1', 'SHA-256', 'SHA-512', 'CreatedDate', 'ModifiedDate')] + [System.String] + $Checksum = 'ModifiedDate', + + [Parameter()] + [System.Management.Automation.PSCredential] + [System.Management.Automation.Credential()] + $Credential, + + [Parameter()] + [System.Boolean] + $Force = $false + ) + + $getTargetResourceParameters = @{ + Path = $Path + Destination = $Destination + } + + $optionalGetTargetResourceParameters = @( 'Validate', 'Checksum', 'Credential' ) + + foreach ($optionalGetTargetResourceParameter in $optionalGetTargetResourceParameters) + { + if ($PSBoundParameters.ContainsKey($optionalGetTargetResourceParameter)) + { + $getTargetResourceParameters[$optionalGetTargetResourceParameter] = $PSBoundParameters[$optionalGetTargetResourceParameter] + } + } + + $archiveResourceState = Get-TargetResource @getTargetResourceParameters + + Write-Verbose -Message ($script:localizedData.TestingArchiveState -f $Path, $Destination) + + $archiveInDesiredState = $archiveResourceState.Ensure -ieq $Ensure + + return $archiveInDesiredState +} + +<# + .SYNOPSIS + Creates a new GUID. + This is a wrapper function for unit testing. +#> +function New-Guid +{ + [OutputType([System.Guid])] + [CmdletBinding()] + param () + + return [System.Guid]::NewGuid() +} + +<# + .SYNOPSIS + Invokes the cmdlet New-PSDrive with the specified parameters. + This is a wrapper function for unit testing due to a bug in Pester. + Issue has been filed here: https://github.com/pester/Pester/issues/728 + + .PARAMETER Parameters + A hashtable of parameters to splat to New-PSDrive. +#> +function Invoke-NewPSDrive +{ + [OutputType([System.Management.Automation.PSDriveInfo])] + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.Collections.Hashtable] + $Parameters + ) + + return New-PSDrive @Parameters +} + +<# + .SYNOPSIS + Mounts a PSDrive to access the specified path with the permissions granted by the specified + credential. + + .PARAMETER Path + The path to which to mount a PSDrive. + + .PARAMETER Credential + The credential of the user account with permissions to access the specified path. +#> +function Mount-PSDriveWithCredential +{ + [OutputType([System.Management.Automation.PSDriveInfo])] + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $Path, + + [Parameter(Mandatory = $true)] + [System.Management.Automation.PSCredential] + [System.Management.Automation.Credential()] + $Credential + ) + + $newPSDrive = $null + + if (Test-Path -LiteralPath $Path -ErrorAction 'SilentlyContinue') + { + Write-Verbose -Message ($script:localizedData.PathAccessiblePSDriveNotNeeded -f $Path) + } + else + { + $pathIsADirectory = $Path.EndsWith('\') + + if ($pathIsADirectory) + { + $pathToPSDriveRoot = $Path + } + else + { + $lastIndexOfBackslash = $Path.LastIndexOf('\') + $pathDoesNotContainADirectory = $lastIndexOfBackslash -eq -1 + + if ($pathDoesNotContainADirectory) + { + $errorMessage = $script:localizedData.PathDoesNotContainValidPSDriveRoot -f $Path + New-InvalidArgumentException -ArgumentName 'Path' -Message $errorMessage + } + else + { + $pathToPSDriveRoot = $Path.Substring(0, $lastIndexOfBackslash) + } + } + + $newPSDriveParameters = @{ + Name = New-Guid + PSProvider = 'FileSystem' + Root = $pathToPSDriveRoot + Scope = 'Script' + Credential = $Credential + } + + try + { + Write-Verbose -Message ($script:localizedData.CreatingPSDrive -f $pathToPSDriveRoot, $Credential.UserName) + $newPSDrive = Invoke-NewPSDrive -Parameters $newPSDriveParameters + } + catch + { + $errorMessage = $script:localizedData.ErrorCreatingPSDrive -f $pathToPSDriveRoot, $Credential.UserName + New-InvalidOperationException -Message $errorMessage -ErrorRecord $_ + } + } + + return $newPSDrive +} + +<# + .SYNOPSIS + Throws an invalid argument exception if the specified path does not exist or is not a path + leaf. + + .PARAMETER Path + The path to assert. +#> +function Assert-PathExistsAsLeaf +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $Path + ) + + $pathExistsAsLeaf = Test-Path -LiteralPath $Path -PathType 'Leaf' -ErrorAction 'SilentlyContinue' + + if (-not $pathExistsAsLeaf) + { + $errorMessage = $script:localizedData.PathDoesNotExistAsLeaf -f $Path + New-InvalidArgumentException -ArgumentName 'Path' -Message $errorMessage + } +} + +<# + .SYNOPSIS + Throws an invalid argument exception if the specified destination path already exists as a + file. + + .PARAMETER Destination + The destination path to assert. +#> +function Assert-DestinationDoesNotExistAsFile +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $Destination + ) + + $itemAtDestination = Get-Item -LiteralPath $Destination -ErrorAction 'SilentlyContinue' + + $itemAtDestinationExists = $null -ne $itemAtDestination + $itemAtDestinationIsFile = $itemAtDestination -is [System.IO.FileInfo] + + if ($itemAtDestinationExists -and $itemAtDestinationIsFile) + { + $errorMessage = $script:localizedData.DestinationExistsAsFile -f $Destination + New-InvalidArgumentException -ArgumentName 'Destination' -Message $errorMessage + } +} + +<# + .SYNOPSIS + Opens the archive at the given path. + This is a wrapper function for unit testing. + + .PARAMETER Path + The path to the archive to open. +#> +function Open-Archive +{ + [OutputType([System.IO.Compression.ZipArchive])] + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $Path + ) + + Write-Verbose -Message ($script:localizedData.OpeningArchive -f $Path) + + try + { + $archive = [System.IO.Compression.ZipFile]::OpenRead($Path) + } + catch + { + $errorMessage = $script:localizedData.ErrorOpeningArchive -f $Path + New-InvalidOperationException -Message $errorMessage -ErrorRecord $_ + } + + return $archive +} + +<# + .SYNOPSIS + Closes the specified archive. + This is a wrapper function for unit testing. + + .PARAMETER Archive + The archive to close. +#> +function Close-Archive +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.IO.Compression.ZipArchive] + $Archive + ) + + Write-Verbose -Message ($script:localizedData.ClosingArchive -f $Path) + $null = $Archive.Dispose() +} + +<# + .SYNOPSIS + Retrieves the archive entries from the specified archive. + This is a wrapper function for unit testing. + + .PARAMETER Archive + The archive of which to retrieve the archive entries. +#> +function Get-ArchiveEntries +{ + [OutputType([System.IO.Compression.ZipArchiveEntry[]])] + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.IO.Compression.ZipArchive] + $Archive + ) + + return $Archive.Entries +} + +<# + .SYNOPSIS + Retrieves the full name of the specified archive entry. + This is a wrapper function for unit testing. + + .PARAMETER ArchiveEntry + The archive entry to retrieve the full name of. +#> +function Get-ArchiveEntryFullName +{ + [OutputType([System.String])] + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.IO.Compression.ZipArchiveEntry] + $ArchiveEntry + ) + + return $ArchiveEntry.FullName +} + +<# + .SYNOPSIS + Opens the specified archive entry. + This is a wrapper function for unit testing. + + .PARAMETER ArchiveEntry + The archive entry to open. +#> +function Open-ArchiveEntry +{ + [OutputType([System.IO.Stream])] + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.IO.Compression.ZipArchiveEntry] + $ArchiveEntry + ) + + Write-Verbose -Message ($script:localizedData.OpeningArchiveEntry -f $ArchiveEntry.FullName) + return $ArchiveEntry.Open() +} + +<# + .SYNOPSIS + Closes the specified stream. + This is a wrapper function for unit testing. + + .PARAMETER Stream + The stream to close. +#> +function Close-Stream +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.IO.Stream] + $Stream + ) + + $null = $Stream.Dispose() +} + +<# + .SYNOPSIS + Tests if the given checksum method name is the name of a SHA checksum method. + + .PARAMETER Checksum + The name of the checksum method to test. +#> +function Test-ChecksumIsSha +{ + [OutputType([System.Boolean])] + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $Checksum + ) + + return ($Checksum.Length -ge 'SHA'.Length) -and ($Checksum.Substring(0, 3) -ieq 'SHA') +} + +<# + .SYNOPSIS + Converts the specified DSC hash algorithm name (with a hyphen) to a PowerShell hash + algorithm name (without a hyphen). The in-box PowerShell Get-FileHash cmdlet will only hash + algorithm names without hypens. + + .PARAMETER DscHashAlgorithmName + The DSC hash algorithm name to convert. +#> +function ConvertTo-PowerShellHashAlgorithmName +{ + [OutputType([System.String])] + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $DscHashAlgorithmName + ) + + return $DscHashAlgorithmName.Replace('-', '') +} + +<# + .SYNOPSIS + Tests if the hash of the specified file matches the hash of the specified archive entry + using the specified hash algorithm. + + .PARAMETER FilePath + The path to the file to test the hash of. + + .PARAMETER CacheEntry + The cache entry to test the hash of. + + .PARAMETER HashAlgorithmName + The name of the hash algorithm to use to retrieve the hashes of the file and archive entry. +#> +function Test-FileHashMatchesArchiveEntryHash +{ + [OutputType([System.Boolean])] + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $FilePath, + + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.IO.Compression.ZipArchiveEntry] + $ArchiveEntry, + + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $HashAlgorithmName + ) + + $archiveEntryFullName = Get-ArchiveEntryFullName -ArchiveEntry $ArchiveEntry + + Write-Verbose -Message ($script:localizedData.ComparingHashes -f $FilePath, $archiveEntryFullName, $HashAlgorithmName) + + $fileHashMatchesArchiveEntryHash = $false + + $powerShellHashAlgorithmName = ConvertTo-PowerShellHashAlgorithmName -DscHashAlgorithmName $HashAlgorithmName + + $openStreams = @() + + try + { + $archiveEntryStream = Open-ArchiveEntry -ArchiveEntry $ArchiveEntry + $openStreams += $archiveEntryStream + + # The Open mode will open the file for reading without modifying the file + $fileStreamMode = [System.IO.FileMode]::Open + + $fileStream = New-Object -TypeName 'System.IO.FileStream' -ArgumentList @( $FilePath, $fileStreamMode ) + $openStreams += $fileStream + + $fileHash = Get-FileHash -InputStream $fileStream -Algorithm $powerShellHashAlgorithmName + $archiveEntryHash = Get-FileHash -InputStream $archiveEntryStream -Algorithm $powerShellHashAlgorithmName + + $hashAlgorithmsMatch = $fileHash.Algorithm -eq $archiveEntryHash.Algorithm + $hashesMatch = $fileHash.Hash -eq $archiveEntryHash.Hash + + $fileHashMatchesArchiveEntryHash = $hashAlgorithmsMatch -and $hashesMatch + } + catch + { + $errorMessage = $script:localizedData.ErrorComparingHashes -f $FilePath, $archiveEntryFullName, $HashAlgorithmName + New-InvalidOperationException -Message $errorMessage -ErrorRecord $_ + } + finally + { + foreach ($openStream in $openStreams) + { + Close-Stream -Stream $openStream + } + } + + return $fileHashMatchesArchiveEntryHash +} + +<# + .SYNOPSIS + Retrieves the timestamp of the specified file for the specified checksum method + and returns it as a checksum. + + .PARAMETER File + The file to retrieve the timestamp of. + + .PARAMETER Checksum + The checksum method to retrieve the timestamp checksum for. + + .NOTES + The returned string is file timestamp normalized to the format specified in + ConvertTo-CheckSumFromDateTime. +#> +function Get-ChecksumFromFileTimestamp +{ + [OutputType([System.String])] + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.IO.FileInfo] + $File, + + [Parameter(Mandatory = $true)] + [ValidateSet('CreatedDate', 'ModifiedDate')] + [System.String] + $Checksum + ) + + $timestamp = Get-TimestampForChecksum @PSBoundParameters + + return ConvertTo-ChecksumFromDateTime -Date $timestamp +} + +<# + .SYNOPSIS + Retrieves the timestamp of the specified file for the specified checksum method. + + .PARAMETER File + The file to retrieve the timestamp of. + + .PARAMETER Checksum + The checksum method to retrieve the timestamp for. +#> +function Get-TimestampForChecksum +{ + [OutputType([System.DateTime])] + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.IO.FileInfo] + $File, + + [Parameter(Mandatory = $true)] + [ValidateSet('CreatedDate', 'ModifiedDate')] + [System.String] + $Checksum + ) + + if ($Checksum -ieq 'CreatedDate') + { + $relevantTimestamp = 'CreationTime' + } + elseif ($Checksum -ieq 'ModifiedDate') + { + $relevantTimestamp = 'LastWriteTime' + } + + return Get-TimestampFromFile -File $File -Timestamp $relevantTimestamp +} + +<# + .SYNOPSIS + Retrieves a timestamp of the specified file. + + .PARAMETER File + The file to retrieve the timestamp from. + + .PARAMETER Timestamp + The timestamp attribute to retrieve. +#> +function Get-TimestampFromFile +{ + [OutputType([System.Datetime])] + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.IO.FileInfo] + $File, + + [Parameter(Mandatory = $true)] + [ValidateSet('CreationTime', 'LastWriteTime')] + [System.String] + $Timestamp + ) + + return $File.$Timestamp +} + +<# + .SYNOPSIS + Converts a datetime object into the format used for a + checksum. + + .PARAMETER Date + The date to use to generate the checksum. + + .NOTES + The returned date is normalized to the General (G) date format. + https://technet.microsoft.com/en-us/library/ee692801.aspx + + Because the General (G) is localization specific a non-localization + specific format such as ISO9660 could be used in future. +#> +function ConvertTo-ChecksumFromDateTime +{ + [OutputType([System.String])] + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.DateTime] + $Date + ) + + return Get-Date -Date $Date -Format 'G' +} + +<# + .SYNOPSIS + Retrieves the last write time of the specified archive entry. + This is a wrapper function for unit testing. + + .PARAMETER ArchiveEntry + The archive entry to retrieve the last write time of. +#> +function Get-ArchiveEntryLastWriteTime +{ + [OutputType([System.DateTime])] + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.IO.Compression.ZipArchiveEntry] + $ArchiveEntry + ) + + return $ArchiveEntry.LastWriteTime.DateTime +} + +<# + .SYNOPSIS + Tests if the specified file matches the specified archive entry based on the specified + checksum method. + + .PARAMETER File + The file to test against the specified archive entry. + + .PARAMETER ArchiveEntry + The archive entry to test against the specified file. + + .PARAMETER Checksum + The checksum method to use to determine whether or not the specified file matches the + specified archive entry. +#> +function Test-FileMatchesArchiveEntryByChecksum +{ + [OutputType([System.Boolean])] + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.IO.FileInfo] + $File, + + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.IO.Compression.ZipArchiveEntry] + $ArchiveEntry, + + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $Checksum + ) + + $archiveEntryFullName = Get-ArchiveEntryFullName -ArchiveEntry $ArchiveEntry + + Write-Verbose -Message ($script:localizedData.TestingIfFileMatchesArchiveEntryByChecksum -f $File.FullName, $archiveEntryFullName, $Checksum) + + $fileMatchesArchiveEntry = $false + + if (Test-ChecksumIsSha -Checksum $Checksum) + { + $fileHashMatchesArchiveEntryHash = Test-FileHashMatchesArchiveEntryHash -FilePath $File.FullName -ArchiveEntry $ArchiveEntry -HashAlgorithmName $Checksum + + if ($fileHashMatchesArchiveEntryHash) + { + Write-Verbose -Message ($script:localizedData.FileMatchesArchiveEntryByChecksum -f $File.FullName, $archiveEntryFullName, $Checksum) + + $fileMatchesArchiveEntry = $true + } + else + { + Write-Verbose -Message ($script:localizedData.FileDoesNotMatchArchiveEntryByChecksum -f $File.FullName, $archiveEntryFullName, $Checksum) + } + } + else + { + $fileTimestampForChecksum = Get-ChecksumFromFileTimestamp -File $File -Checksum $Checksum + + $archiveEntryLastWriteTime = Get-ArchiveEntryLastWriteTime -ArchiveEntry $ArchiveEntry + $archiveEntryLastWriteTimeChecksum = ConvertTo-CheckSumFromDateTime -Date $archiveEntryLastWriteTime + + if ($fileTimestampForChecksum.Equals($archiveEntryLastWriteTimeChecksum)) + { + Write-Verbose -Message ($script:localizedData.FileMatchesArchiveEntryByChecksum -f $File.FullName, $archiveEntryFullName, $Checksum) + + $fileMatchesArchiveEntry = $true + } + else + { + Write-Verbose -Message ($script:localizedData.FileDoesNotMatchArchiveEntryByChecksum -f $File.FullName, $archiveEntryFullName, $Checksum) + } + } + + return $fileMatchesArchiveEntry +} + +<# + .SYNOPSIS + Tests if the given archive entry name represents a directory. + + .PARAMETER ArchiveEntryName + The archive entry name to test. +#> +function Test-ArchiveEntryIsDirectory +{ + [OutputType([System.Boolean])] + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $ArchiveEntryName + ) + + return $ArchiveEntryName.EndsWith('\') -or $ArchiveEntryName.EndsWith('/') +} + +<# + .SYNOPSIS + Tests if the specified archive exists in its expanded form at the destination. + + .PARAMETER Archive + The archive to test for existence at the specified destination. + + .PARAMETER Destination + The path to the destination to check for the presence of the expanded form of the specified + archive. + + .PARAMETER Checksum + The checksum method to use to determine whether a file in the archive matches a file at the + destination. + + If not provided, only the existence of the items in the archive will be checked. +#> +function Test-ArchiveExistsAtDestination +{ + [OutputType([System.Boolean])] + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $ArchiveSourcePath, + + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $Destination, + + [Parameter()] + [ValidateSet('SHA-1', 'SHA-256', 'SHA-512', 'CreatedDate', 'ModifiedDate')] + [System.String] + $Checksum + ) + + Write-Verbose -Message ($script:localizedData.TestingIfArchiveExistsAtDestination -f $Destination) + + $archiveExistsAtDestination = $true + + $archive = Open-Archive -Path $ArchiveSourcePath + + try + { + $archiveEntries = Get-ArchiveEntries -Archive $archive + + foreach ($archiveEntry in $archiveEntries) + { + $archiveEntryFullName = Get-ArchiveEntryFullName -ArchiveEntry $archiveEntry + $archiveEntryPathAtDestination = Join-Path -Path $Destination -ChildPath $archiveEntryFullName + + $archiveEntryItemAtDestination = Get-Item -LiteralPath $archiveEntryPathAtDestination -ErrorAction 'SilentlyContinue' + + if ($null -eq $archiveEntryItemAtDestination) + { + Write-Verbose -Message ($script:localizedData.ItemWithArchiveEntryNameDoesNotExist -f $archiveEntryPathAtDestination) + + $archiveExistsAtDestination = $false + break + } + else + { + Write-Verbose -Message ($script:localizedData.ItemWithArchiveEntryNameExists -f $archiveEntryPathAtDestination) + + if (Test-ArchiveEntryIsDirectory -ArchiveEntryName $archiveEntryFullName) + { + if (-not ($archiveEntryItemAtDestination -is [System.IO.DirectoryInfo])) + { + Write-Verbose -Message ($script:localizedData.ItemWithArchiveEntryNameIsNotDirectory -f $archiveEntryPathAtDestination) + + $archiveExistsAtDestination = $false + break + } + } + else + { + if ($archiveEntryItemAtDestination -is [System.IO.FileInfo]) + { + if ($PSBoundParameters.ContainsKey('Checksum')) + { + if (-not (Test-FileMatchesArchiveEntryByChecksum -File $archiveEntryItemAtDestination -ArchiveEntry $archiveEntry -Checksum $Checksum)) + { + $archiveExistsAtDestination = $false + break + } + } + } + else + { + Write-Verbose -Message ($script:localizedData.ItemWithArchiveEntryNameIsNotFile -f $archiveEntryPathAtDestination) + + $archiveExistsAtDestination = $false + break + } + } + } + } + } + finally + { + Close-Archive -Archive $archive + } + + if ($archiveExistsAtDestination) + { + Write-Verbose -Message ($script:localizedData.ArchiveExistsAtDestination -f $ArchiveSourcePath, $Destination) + } + else + { + Write-Verbose -Message ($script:localizedData.ArchiveDoesNotExistAtDestination -f $ArchiveSourcePath, $Destination) + } + + return $archiveExistsAtDestination +} + +<# + .SYNOPSIS + Copies the contents of the specified source stream to the specified destination stream. + This is a wrapper function for unit testing. + + .PARAMETER SourceStream + The stream to copy from. + + .PARAMETER DestinationStream + The stream to copy to. +#> +function Copy-FromStreamToStream +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.IO.Stream] + $SourceStream, + + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.IO.Stream] + $DestinationStream + ) + + $null = $SourceStream.CopyTo($DestinationStream) +} + +<# + .SYNOPSIS + Copies the specified archive entry to the specified destination path. + + .PARAMETER ArchiveEntry + The archive entry to copy to the destination. + + .PARAMETER DestinationPath + The destination file path to copy the archive entry to. +#> +function Copy-ArchiveEntryToDestination +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.IO.Compression.ZipArchiveEntry] + $ArchiveEntry, + + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $DestinationPath + ) + + $archiveEntryFullName = Get-ArchiveEntryFullName -ArchiveEntry $ArchiveEntry + + Write-Verbose -Message ($script:localizedData.CopyingArchiveEntryToDestination -f $archiveEntryFullName, $DestinationPath) + + if (Test-ArchiveEntryIsDirectory -ArchiveEntryName $archiveEntryFullName) + { + Write-Verbose -Message ($script:localizedData.CreatingArchiveEntryDirectory -f $DestinationPath) + + $null = New-Item -Path $DestinationPath -ItemType 'Directory' + } + else + { + $openStreams = @() + + try + { + $archiveEntryStream = Open-ArchiveEntry -ArchiveEntry $ArchiveEntry + $openStreams += $archiveEntryStream + + # The Create mode will create a new file if it does not exist or overwrite the file if it already exists + $destinationStreamMode = [System.IO.FileMode]::Create + + $destinationStream = New-Object -TypeName 'System.IO.FileStream' -ArgumentList @( $DestinationPath, $destinationStreamMode ) + $openStreams += $destinationStream + + Copy-FromStreamToStream -SourceStream $archiveEntryStream -DestinationStream $destinationStream + } + catch + { + $errorMessage = $script:localizedData.ErrorCopyingFromArchiveToDestination -f $DestinationPath + New-InvalidOperationException -Message $errorMessage -ErrorRecord $_ + } + finally + { + foreach ($openStream in $openStreams) + { + Close-Stream -Stream $openStream + } + } + + $null = New-Object -TypeName 'System.IO.FileInfo' -ArgumentList @( $DestinationPath ) + + $updatedTimestamp = Get-ArchiveEntryLastWriteTime -ArchiveEntry $ArchiveEntry + + $null = Set-ItemProperty -LiteralPath $DestinationPath -Name 'LastWriteTime' -Value $updatedTimestamp + $null = Set-ItemProperty -LiteralPath $DestinationPath -Name 'LastAccessTime' -Value $updatedTimestamp + $null = Set-ItemProperty -LiteralPath $DestinationPath -Name 'CreationTime' -Value $updatedTimestamp + } +} + +<# + .SYNOPSIS + Expands the archive at the specified source path to the specified destination path. + + .PARAMETER ArchiveSourcePath + The source path of the archive to expand to the specified destination path. + + .PARAMETER Destination + The destination path at which to expand the archive at the specified source path. + + .PARAMETER Checksum + The checksum method to use to determine if a file at the destination already matches a file + in the archive. + + .PARAMETER Force + Specifies whether or not to overwrite files that exist at the destination but do not match + the file of the same name in the archive based on the specified checksum method. +#> +function Expand-ArchiveToDestination +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $ArchiveSourcePath, + + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $Destination, + + [Parameter()] + [ValidateSet('SHA-1', 'SHA-256', 'SHA-512', 'CreatedDate', 'ModifiedDate')] + [System.String] + $Checksum, + + [Parameter()] + [System.Boolean] + $Force = $false + ) + + Write-Verbose -Message ($script:localizedData.ExpandingArchiveToDestination -f $ArchiveSourcePath, $Destination) + + $archive = Open-Archive -Path $ArchiveSourcePath + + try + { + $archiveEntries = Get-ArchiveEntries -Archive $archive + + foreach ($archiveEntry in $archiveEntries) + { + $archiveEntryFullName = Get-ArchiveEntryFullName -ArchiveEntry $archiveEntry + $archiveEntryPathAtDestination = Join-Path -Path $Destination -ChildPath $archiveEntryFullName + + $archiveEntryIsDirectory = Test-ArchiveEntryIsDirectory -ArchiveEntryName $archiveEntryFullName + + $archiveEntryItemAtDestination = Get-Item -LiteralPath $archiveEntryPathAtDestination -ErrorAction 'SilentlyContinue' + + if ($null -eq $archiveEntryItemAtDestination) + { + Write-Verbose -Message ($script:localizedData.ItemWithArchiveEntryNameDoesNotExist -f $archiveEntryPathAtDestination) + + if (-not $archiveEntryIsDirectory) + { + $parentDirectory = Split-Path -Path $archiveEntryPathAtDestination -Parent + + if (-not (Test-Path -Path $parentDirectory)) + { + Write-Verbose -Message ($script:localizedData.CreatingParentDirectory -f $parentDirectory) + + $null = New-Item -Path $parentDirectory -ItemType 'Directory' + } + } + + Copy-ArchiveEntryToDestination -ArchiveEntry $archiveEntry -DestinationPath $archiveEntryPathAtDestination + } + else + { + Write-Verbose -Message ($script:localizedData.ItemWithArchiveEntryNameExists -f $archiveEntryPathAtDestination) + + $overwriteArchiveEntry = $true + + if ($archiveEntryIsDirectory) + { + $overwriteArchiveEntry = -not ($archiveEntryItemAtDestination -is [System.IO.DirectoryInfo]) + } + elseif ($archiveEntryItemAtDestination -is [System.IO.FileInfo]) + { + if ($PSBoundParameters.ContainsKey('Checksum')) + { + $overwriteArchiveEntry = -not (Test-FileMatchesArchiveEntryByChecksum -File $archiveEntryItemAtDestination -ArchiveEntry $archiveEntry -Checksum $Checksum) + } + else + { + $overwriteArchiveEntry = $false + } + } + + if ($overwriteArchiveEntry) + { + if ($Force) + { + Write-Verbose -Message ($script:localizedData.OverwritingItem -f $archiveEntryPathAtDestination) + + $null = Remove-Item -LiteralPath $archiveEntryPathAtDestination + Copy-ArchiveEntryToDestination -ArchiveEntry $archiveEntry -DestinationPath $archiveEntryPathAtDestination + } + else + { + New-InvalidOperationException -Message ($script:localizedData.ForceNotSpecifiedToOverwriteItem -f $archiveEntryPathAtDestination, $archiveEntryFullName) + } + } + } + } + } + finally + { + Close-Archive -Archive $archive + } +} + +<# + .SYNOPSIS + Removes the specified directory from the specified destination path. + + .PARAMETER Directory + The partial path under the destination path of the directory to remove. + + .PARAMETER Destination + The destination from which to remove the directory. +#> +function Remove-DirectoryFromDestination +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String[]] + $Directory, + + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $Destination + ) + + # Sort-Object requires the use of a pipe to function properly + $Directory = $Directory | Sort-Object -Descending -Unique + + foreach ($directoryToRemove in $Directory) + { + $directoryPathAtDestination = Join-Path -Path $Destination -ChildPath $directoryToRemove + $directoryExists = Test-Path -LiteralPath $directoryPathAtDestination -PathType 'Container' + + if ($directoryExists) + { + $directoryChildItems = Get-ChildItem -LiteralPath $directoryPathAtDestination -ErrorAction 'SilentlyContinue' + $directoryIsEmpty = $null -eq $directoryChildItems + + if ($directoryIsEmpty) + { + Write-Verbose -Message ($script:localizedData.RemovingDirectory -f $directoryPathAtDestination) + + $null = Remove-Item -LiteralPath $directoryPathAtDestination + } + else + { + Write-Verbose -Message ($script:localizedData.DirectoryIsNotEmpty -f $directoryPathAtDestination) + } + } + } +} + +<# + .SYNOPSIS + Removes the specified archive from the specified destination. + + .PARAMETER Archive + The archive to remove from the specified destination. + + .PARAMETER Destination + The path to the destination to remove the specified archive from. + + .PARAMETER Checksum + The checksum method to use to determine whether a file in the archive matches a file at the + destination. + + If not provided, only the existence of the items in the archive will be checked. +#> +function Remove-ArchiveFromDestination +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $ArchiveSourcePath, + + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $Destination, + + [Parameter()] + [ValidateSet('SHA-1', 'SHA-256', 'SHA-512', 'CreatedDate', 'ModifiedDate')] + [System.String] + $Checksum + ) + + Write-Verbose -Message ($script:localizedData.RemovingArchiveFromDestination -f $Destination) + + $archive = Open-Archive -Path $ArchiveSourcePath + + try + { + $directoriesToRemove = @() + + $archiveEntries = Get-ArchiveEntries -Archive $archive + + foreach ($archiveEntry in $archiveEntries) + { + $archiveEntryFullName = Get-ArchiveEntryFullName -ArchiveEntry $archiveEntry + $archiveEntryPathAtDestination = Join-Path -Path $Destination -ChildPath $archiveEntryFullName + + $archiveEntryIsDirectory = Test-ArchiveEntryIsDirectory -ArchiveEntryName $archiveEntryFullName + + $itemAtDestination = Get-Item -LiteralPath $archiveEntryPathAtDestination -ErrorAction 'SilentlyContinue' + + if ($null -eq $itemAtDestination) + { + Write-Verbose -Message ($script:localizedData.ItemWithArchiveEntryNameDoesNotExist -f $archiveEntryPathAtDestination) + } + else + { + Write-Verbose -Message ($script:localizedData.ItemWithArchiveEntryNameExists -f $archiveEntryPathAtDestination) + + $itemAtDestinationIsDirectory = $itemAtDestination -is [System.IO.DirectoryInfo] + $itemAtDestinationIsFile = $itemAtDestination -is [System.IO.FileInfo] + + $removeArchiveEntry = $false + + if ($archiveEntryIsDirectory -and $itemAtDestinationIsDirectory) + { + $removeArchiveEntry = $true + $directoriesToRemove += $archiveEntryFullName + + } + elseif ((-not $archiveEntryIsDirectory) -and $itemAtDestinationIsFile) + { + $removeArchiveEntry = $true + + if ($PSBoundParameters.ContainsKey('Checksum')) + { + $removeArchiveEntry = Test-FileMatchesArchiveEntryByChecksum -File $itemAtDestination -ArchiveEntry $archiveEntry -Checksum $Checksum + } + + if ($removeArchiveEntry) + { + Write-Verbose -Message ($script:localizedData.RemovingFile -f $archiveEntryPathAtDestination) + $null = Remove-Item -LiteralPath $archiveEntryPathAtDestination + } + } + else + { + Write-Verbose -Message ($script:localizedData.CouldNotRemoveItemOfIncorrectType -f $archiveEntryPathAtDestination, $archiveEntryFullName) + } + + if ($removeArchiveEntry) + { + $parentDirectory = Split-Path -Path $archiveEntryFullName -Parent + + while (-not [System.String]::IsNullOrEmpty($parentDirectory)) + { + $directoriesToRemove += $parentDirectory + $parentDirectory = Split-Path -Path $parentDirectory -Parent + } + } + } + } + + if ($directoriesToRemove.Count -gt 0) + { + $null = Remove-DirectoryFromDestination -Directory $directoriesToRemove -Destination $Destination + } + + Write-Verbose -Message ($script:localizedData.ArchiveRemovedFromDestination -f $Destination) + + } + finally + { + Close-Archive -Archive $archive + } +} diff --git a/deployment/dsc/azshcihost/xPSDesiredStateConfiguration/9.1.0/DSCResources/DSC_xArchive/DSC_xArchive.schema.mof b/deployment/dsc/azshcihost/xPSDesiredStateConfiguration/9.1.0/DSCResources/DSC_xArchive/DSC_xArchive.schema.mof new file mode 100644 index 0000000..2d0fdb3 --- /dev/null +++ b/deployment/dsc/azshcihost/xPSDesiredStateConfiguration/9.1.0/DSCResources/DSC_xArchive/DSC_xArchive.schema.mof @@ -0,0 +1,11 @@ +[ClassVersion("1.0.0.0"),FriendlyName("xArchive")] +class DSC_xArchive : OMI_BaseResource +{ + [Key, Description("The path to the archive file that should be expanded to or removed from the specified destination.")] String Path; + [Key, Description("The path where the specified archive file should be expanded to or removed from.")] String Destination; + [Write, Description("Specifies whether or not the expanded content of the archive file at the specified path should exist at the specified destination. To update the specified destination to have the expanded content of the archive file at the specified path, specify this property as Present. To remove the expanded content of the archive file at the specified path from the specified destination, specify this property as Absent. The default value is Present."), ValueMap{"Present", "Absent"}, Values{"Present", "Absent"}] String Ensure; + [Write, Description("Specifies whether or not to validate that a file at the destination with the same name as a file in the archive actually matches that corresponding file in the archive by the specified checksum method. If the file does not match and Ensure is specified as Present and Force is not specified, the resource will throw an error that the file at the desintation cannot be overwritten. If the file does not match and Ensure is specified as Present and Force is specified, the file at the desintation will be overwritten. If the file does not match and Ensure is specified as Absent, the file at the desintation will not be removed. The default value is false.")] Boolean Validate; + [Write, Description("The Checksum method to use to validate whether or not a file at the destination with the same name as a file in the archive actually matches that corresponding file in the archive. An invalid argument exception will be thrown if Checksum is specified while Validate is specified as false. ModifiedDate will check that the LastWriteTime property of the file at the destination matches the LastWriteTime property of the file in the archive. CreatedDate will check that the CreationTime property of the file at the destination matches the CreationTime property of the file in the archive. SHA-1, SHA-256, and SHA-512 will check that the hash of the file at the destination by the specified SHA method matches the hash of the file in the archive by the specified SHA method. The default value is ModifiedDate.") ,ValueMap{"SHA-1", "SHA-256", "SHA-512", "CreatedDate", "ModifiedDate"}, Values{"SHA-1", "SHA-256", "SHA-512", "CreatedDate", "ModifiedDate"}] String Checksum; + [Write, Description("The credential of a user account with permissions to access the specified archive path and destination if needed.") ,EmbeddedInstance("MSFT_Credential")] String Credential; + [Write, Description("Specifies whether or not any existing files or directories at the destination with the same name as a file or directory in the archive should be overwritten to match the file or directory in the archive. When this property is false, an error will be thrown if an item at the destination needs to be overwritten. The default value is false.")] Boolean Force; +}; diff --git a/deployment/dsc/azshcihost/xPSDesiredStateConfiguration/9.1.0/DSCResources/DSC_xArchive/en-US/DSC_xArchive.schema.mfl b/deployment/dsc/azshcihost/xPSDesiredStateConfiguration/9.1.0/DSCResources/DSC_xArchive/en-US/DSC_xArchive.schema.mfl new file mode 100644 index 0000000..42da26b --- /dev/null +++ b/deployment/dsc/azshcihost/xPSDesiredStateConfiguration/9.1.0/DSCResources/DSC_xArchive/en-US/DSC_xArchive.schema.mfl @@ -0,0 +1,11 @@ +[AMENDMENT, LOCALE("MS_409")] +class DSC_xArchive : OMI_BaseResource +{ + [Key,Description("The path to the archive file that should be expanded to or removed from the specified destination.") : Amended] String Path; + [Key,Description("The path where the specified archive file should be expanded to or removed from.") : Amended] String Destination; + [Description("Specifies whether or not the expanded content of the archive file at the specified path should exist at the specified destination. To update the specified destination to have the expanded content of the archive file at the specified path, specify this property as Present. To remove the expanded content of the archive file at the specified path from the specified destination, specify this property as Absent. The default value is Present.") : Amended] String Ensure; + [Description("Specifies whether or not to validate that a file at the destination with the same name as a file in the archive actually matches that corresponding file in the archive by the specified checksum method. If the file does not match and Ensure is specified as Present and Force is not specified, the resource will throw an error that the file at the desintation cannot be overwritten. If the file does not match and Ensure is specified as Present and Force is specified, the file at the desintation will be overwritten. If the file does not match and Ensure is specified as Absent, the file at the desintation will not be removed. The default value is false.") : Amended] Boolean Validate; + [Description("The Checksum method to use to validate whether or not a file at the destination with the same name as a file in the archive actually matches that corresponding file in the archive. An invalid argument exception will be thrown if Checksum is specified while Validate is specified as false. ModifiedDate will check that the LastWriteTime property of the file at the destination matches the LastWriteTime property of the file in the archive. CreatedDate will check that the CreationTime property of the file at the destination matches the CreationTime property of the file in the archive. SHA-1, SHA-256, and SHA-512 will check that the hash of the file at the destination by the specified SHA method matches the hash of the file in the archive by the specified SHA method. The default value is ModifiedDate.") : Amended] String Checksum; + [Description("The credential of a user account with permissions to access the specified archive path and destination if needed.") : Amended] String Credential; + [Description("Specifies whether or not any existing files or directories at the destination with the same name as a file or directory in the archive should be overwritten to match the file or directory in the archive. When this property is false, an error will be thrown if an item at the destination needs to be overwritten. The default value is false.") : Amended] Boolean Force; +}; diff --git a/deployment/dsc/azshcihost/xPSDesiredStateConfiguration/9.1.0/DSCResources/DSC_xArchive/en-US/DSC_xArchive.strings.psd1 b/deployment/dsc/azshcihost/xPSDesiredStateConfiguration/9.1.0/DSCResources/DSC_xArchive/en-US/DSC_xArchive.strings.psd1 new file mode 100644 index 0000000..55f0912 --- /dev/null +++ b/deployment/dsc/azshcihost/xPSDesiredStateConfiguration/9.1.0/DSCResources/DSC_xArchive/en-US/DSC_xArchive.strings.psd1 @@ -0,0 +1,58 @@ +# Localized resources for DSC_xArchive + +ConvertFrom-StringData @' + RetrievingArchiveState = Retrieving the state of the archive with path "{0}" and destination "{1}"... + SettingArchiveState = Setting the state of the archive with path "{0}" and destination "{1}"... + ArchiveStateSet = The state of the archive with path "{0}" and destination "{1}" has been set. + TestingArchiveState = Testing whether or not the state of the archive with path "{0}" and destination "{1}" matches the desired state... + + PathAccessiblePSDriveNotNeeded = The path "{0}" is accessible. A new PSDrive is not needed. + CreatingPSDrive = Creating a PSDrive to access the path "{0}" with permissions from the user "{1}"... + RemovingPSDrive = Removing the mounted PSDrive "{0}"... + + DestinationExists = A directory already exists at the destination path "{0}". + DestinationDoesNotExist = A directory does not exist at the destination path "{0}". + CreatingDirectoryAtDestination = Creating the root directory at the destination path "{0}"... + + TestingIfArchiveExistsAtDestination = Testing if the archive at the destination path "{0}" exists... + ArchiveExistsAtDestination = The archive at path "{0}" exists at the destination "{1}". + ArchiveDoesNotExistAtDestination = The archive at path "{0}" does not exist at the destination "{1}". + + OpeningArchive = Opening the archive at path "{0}"... + ClosingArchive = Closing the archive at path "{0}"... + + OpeningArchiveEntry = Opening the archive entry "{0}"... + + ItemWithArchiveEntryNameExists = An item with the same name as the archive entry exists at the destination path "{0}". + ItemWithArchiveEntryNameDoesNotExist = An item with the same name as the archive entry does not exist at the destination path "{0}". + ItemWithArchiveEntryNameIsNotDirectory = The item at the destination path "{0}" has the same name as a directory archive entry but is not a directory. + ItemWithArchiveEntryNameIsNotFile = The item at the destination path "{0}" has the same name as a file archive entry but is not a file. + + TestingIfFileMatchesArchiveEntryByChecksum = Testing if the file at "{0}" matches the archive entry at "{1}" by the checksum method "{2}"... + ComparingHashes = Comparing the hash of the file at "{0}" to the hash of the archive entry at "{1}" with the hash algorithm "{2}"... + FileMatchesArchiveEntryByChecksum = The file at "{0}" matches the archive entry at "{1}" by the checksum method "{2}". + FileDoesNotMatchArchiveEntryByChecksum = The file at "{0}" does not match the archive entry at "{1}" by the checksum method "{2}". + + ExpandingArchiveToDestination = Expanding the archive at "{0}" to the destination "{1}"... + CreatingParentDirectory = Creating an archive entry parent directory at the path "{0}"... + OverwritingItem = Overwriting the item at the path "{0}"... + CreatingArchiveEntryDirectory = Creating an archive entry directory "{0}"... + CopyingArchiveEntryToDestination = Copying the archive entry "{0}" to the destination path "{1}"... + + RemovingArchiveFromDestination = Removing archive from the destination path "{0}"... + RemovingDirectory = Removing the directory at path "{0}"... + RemovingFile = Removing the file at path "{0}"... + CouldNotRemoveItemOfIncorrectType = The file at "{0}" does not match the item type (file, directory, or other) or the archive entry at "{1}", so it will not be removed. + ArchiveRemovedFromDestination = Archive removed from the destination path "{0}". + + ChecksumSpecifiedAndValidateFalse = The Checksum parameter was specified as "{0}" but the Validate parameter is set to false for the archive with path "{1}" and destination "{2}". Please specify the Validate parameter as true to use the Checksum parameter. + PathDoesNotContainValidPSDriveRoot = The path "{0}" cannot be accessed because it does not contain any directories to use as the root of a PSDrive. + ErrorCreatingPSDrive = An error occurred while attempting to create a PSDrive to access the path "{0}" under the user "{1}". + PathDoesNotExistAsLeaf = The path "{0}" does not exist or is not a path leaf. + DestinationExistsAsFile = A file exists at the desintation path "{0}". + ErrorOpeningArchive = An error occurred while attempting to open the archive at path "{0}". + ErrorCopyingFromArchiveToDestination = An error occurred while attempting copy from the archive to the destination path "{0}". + DirectoryIsNotEmpty = The directory at path "{0}" is not empty, so it will not be removed. + ErrorComparingHashes = An error occurred while comparing the hash of the file at "{0}" to the archive entry "{1}" with the hash algorithm "{2}". + ForceNotSpecifiedToOverwriteItem = An item already exists at "{0}" that does not match the item in the archive at "{1}", but the Force parameter has not been specified to overwrite this item. +'@ diff --git a/deployment/dsc/azshcihost/xPSDesiredStateConfiguration/9.1.0/DSCResources/DSC_xDSCWebService/DSC_xDSCWebService.psm1 b/deployment/dsc/azshcihost/xPSDesiredStateConfiguration/9.1.0/DSCResources/DSC_xDSCWebService/DSC_xDSCWebService.psm1 new file mode 100644 index 0000000..c136aab --- /dev/null +++ b/deployment/dsc/azshcihost/xPSDesiredStateConfiguration/9.1.0/DSCResources/DSC_xDSCWebService/DSC_xDSCWebService.psm1 @@ -0,0 +1,1759 @@ +$modulePath = Join-Path -Path (Split-Path -Path (Split-Path -Path $PSScriptRoot -Parent) -Parent) -ChildPath 'Modules' + +# Import the shared modules +Import-Module -Name (Join-Path -Path $modulePath ` + -ChildPath (Join-Path -Path 'xPSDesiredStateConfiguration.Common' ` + -ChildPath 'xPSDesiredStateConfiguration.Common.psm1')) +Import-Module -Name (Join-Path -Path $modulePath ` + -ChildPath (Join-Path -Path 'xPSDesiredStateConfiguration.PSWSIIS' ` + -ChildPath 'xPSDesiredStateConfiguration.PSWSIIS.psm1')) +Import-Module -Name (Join-Path -Path $modulePath ` + -ChildPath (Join-Path -Path 'xPSDesiredStateConfiguration.Firewall' ` + -ChildPath 'xPSDesiredStateConfiguration.Firewall.psm1')) +Import-Module -Name (Join-Path -Path $modulePath ` + -ChildPath (Join-Path -Path 'xPSDesiredStateConfiguration.Security' ` + -ChildPath 'xPSDesiredStateConfiguration.Security.psm1')) + +# Import Localization Strings +$script:localizedData = Get-LocalizedData -ResourceName 'DSC_xDSCWebService' + +<# + .SYNOPSIS + Get the state of the DSC Web Service. + + .PARAMETER EndpointName + Prefix of the WCF SVC file. + + .PARAMETER ApplicationPoolName + The IIS ApplicationPool to use for the Pull Server. If not specified a + pool with name 'PSWS' will be created. + + .PARAMETER CertificateSubject + The subject of the Certificate in CERT:\LocalMachine\MY\ for Pull Server. + + .PARAMETER CertificateTemplateName + The certificate Template Name of the Certificate in CERT:\LocalMachine\MY\ + for Pull Server. + + .PARAMETER CertificateThumbPrint + The thumbprint of the Certificate in CERT:\LocalMachine\MY\ for Pull Server. + + .PARAMETER ConfigureFirewall + Enable incomming firewall exceptions for the configured DSC Pull Server + port. Defaults to true. + + .PARAMETER DisableSecurityBestPractices + A list of exceptions to the security best practices to apply. + + .PARAMETER Enable32BitAppOnWin64 + Enable the DSC Pull Server to run in a 32-bit process on a 64-bit operating + system. + + .PARAMETER UseSecurityBestPractices + Ensure that the DSC Pull Server is created using security best practices. +#> +function Get-TargetResource +{ + [CmdletBinding(DefaultParameterSetName = 'CertificateThumbPrint')] + [OutputType([System.Collections.Hashtable])] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $EndpointName, + + [Parameter()] + [ValidateNotNullOrEmpty()] + [System.String] + $ApplicationPoolName = $DscWebServiceDefaultAppPoolName, + + [Parameter(ParameterSetName = 'CertificateSubject')] + [ValidateNotNullOrEmpty()] + [System.String] + $CertificateSubject, + + [Parameter(ParameterSetName = 'CertificateSubject')] + [ValidateNotNullOrEmpty()] + [System.String] + $CertificateTemplateName = 'WebServer', + + [Parameter(ParameterSetName = 'CertificateThumbPrint')] + [ValidateNotNullOrEmpty()] + [System.String] + $CertificateThumbPrint, + + [Parameter()] + [System.Boolean] + $ConfigureFirewall = $true, + + [Parameter()] + [ValidateSet('SecureTLSProtocols')] + [System.String[]] + $DisableSecurityBestPractices, + + [Parameter()] + [System.Boolean] + $Enable32BitAppOnWin64 = $false, + + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.Boolean] + $UseSecurityBestPractices + ) + + <# + If Certificate Subject is not specified then a value for + CertificateThumbprint must be explicitly set instead. The + Mof schema doesn't allow for a mandatory parameter in a parameter set. + #> + if ($PScmdlet.ParameterSetName -eq 'CertificateThumbPrint' ` + -and $PSBoundParameters.ContainsKey('CertificateThumbPrint') -ne $true) + { + throw $script:localizedData.ThrowCertificateThumbprint + } + + $webSite = Get-Website -Name $EndpointName + + if ($webSite) + { + Write-Verbose -Message "PullServer is deployed at '$EndpointName'." + + $Ensure = 'Present' + $acceptSelfSignedCertificates = $false + + # Get Full Path for Web.config file + $webConfigFullPath = Join-Path -Path $website.physicalPath -ChildPath 'web.config' + + # Get module and configuration path + $modulePath = Get-WebConfigAppSetting -WebConfigFullPath $webConfigFullPath -AppSettingName 'ModulePath' + $configurationPath = Get-WebConfigAppSetting -WebConfigFullPath $webConfigFullPath -AppSettingName 'ConfigurationPath' + $registrationKeyPath = Get-WebConfigAppSetting -WebConfigFullPath $webConfigFullPath -AppSettingName 'RegistrationKeyPath' + + # Get database path + switch ((Get-WebConfigAppSetting -WebConfigFullPath $webConfigFullPath -AppSettingName 'dbprovider')) + { + 'ESENT' + { + $databasePath = Get-WebConfigAppSetting -WebConfigFullPath $webConfigFullPath -AppSettingName 'dbconnectionstr' | Split-Path -Parent + } + + 'System.Data.OleDb' + { + $connectionString = Get-WebConfigAppSetting -WebConfigFullPath $webConfigFullPath -AppSettingName 'dbconnectionstr' + + if ($connectionString -match 'Data Source=(.*)\\Devices\.mdb') + { + $databasePath = $Matches[0] + } + else + { + $databasePath = $connectionString + } + } + } + + $urlPrefix = $website.bindings.Collection[0].protocol + '://' + + $ipProperties = [System.Net.NetworkInformation.IPGlobalProperties]::GetIPGlobalProperties() + + if ($ipProperties.DomainName) + { + $fqdn = '{0}.{1}' -f $ipProperties.HostName, $ipProperties.DomainName + } + else + { + $fqdn = $ipProperties.HostName + } + + $iisPort = $website.bindings.Collection[0].bindingInformation.Split(':')[1] + + $svcFileName = (Get-ChildItem -Path $website.physicalPath -Filter '*.svc').Name + + $serverUrl = $urlPrefix + $fqdn + ':' + $iisPort + '/' + $svcFileName + + $webBinding = Get-WebBinding -Name $EndpointName + + if ((Test-IISSelfSignedModuleEnabled -EndpointName $EndpointName)) + { + $acceptSelfSignedCertificates = $true + } + + $ConfigureFirewall = Test-PullServerFirewallConfiguration -Port $iisPort + $ApplicationPoolName = $webSite.applicationPool + $physicalPath = $website.physicalPath + $state = $webSite.state + } + else + { + Write-Verbose -Message "No website found with name '$EndpointName'." + $Ensure = 'Absent' + } + + $output = @{ + EndpointName = $EndpointName + ApplicationPoolName = $ApplicationPoolName + Port = $iisPort + PhysicalPath = $physicalPath + State = $state + DatabasePath = $databasePath + ModulePath = $modulePath + ConfigurationPath = $configurationPath + DSCServerUrl = $serverUrl + Ensure = $Ensure + RegistrationKeyPath = $registrationKeyPath + AcceptSelfSignedCertificates = $acceptSelfSignedCertificates + UseSecurityBestPractices = $UseSecurityBestPractices + DisableSecurityBestPractices = $DisableSecurityBestPractices + Enable32BitAppOnWin64 = $Enable32BitAppOnWin64 + ConfigureFirewall = $ConfigureFirewall + } + + if ($CertificateThumbPrint -eq 'AllowUnencryptedTraffic') + { + Write-Verbose -Message 'Current PullServer configuration allows unencrypted traffic.' + $output.Add('CertificateThumbPrint', $certificateThumbPrint) + } + else + { + $certificate = ([System.Array] (Get-ChildItem -Path 'Cert:\LocalMachine\My\')) | + Where-Object -FilterScript { + $_.Thumbprint -eq $webBinding.CertificateHash + } + + # Try to parse the Certificate Template Name. The property is not available on all Certificates. + $actualCertificateTemplateName = '' + $certificateTemplateProperty = $certificate.Extensions | Where-Object -FilterScript { + $_.Oid.FriendlyName -eq 'Certificate Template Name' + } + + if ($null -ne $certificateTemplateProperty) + { + $actualCertificateTemplateName = $certificateTemplateProperty.Format($false) + } + + $output.Add('CertificateThumbPrint', $webBinding.CertificateHash) + $output.Add('CertificateSubject', $certificate.Subject) + $output.Add('CertificateTemplateName', $actualCertificateTemplateName) + } + + return $output +} + +<# + .SYNOPSIS + Set the state of the DSC Web Service. + + .PARAMETER EndpointName + Prefix of the WCF SVC file. + + .PARAMETER AcceptSelfSignedCertificates + Specifies is self-signed certs will be accepted for client authentication. + + .PARAMETER ApplicationPoolName + The IIS ApplicationPool to use for the Pull Server. If not specified a + pool with name 'PSWS' will be created. + + .PARAMETER CertificateSubject + The subject of the Certificate in CERT:\LocalMachine\MY\ for Pull Server. + + .PARAMETER CertificateTemplateName + The certificate Template Name of the Certificate in CERT:\LocalMachine\MY\ + for Pull Server. + + .PARAMETER CertificateThumbPrint + The thumbprint of the Certificate in CERT:\LocalMachine\MY\ for Pull Server. + + .PARAMETER ConfigurationPath + The location on the disk where the Configuration is stored. + + .PARAMETER ConfigureFirewall + Enable incomming firewall exceptions for the configured DSC Pull Server + port. Defaults to true. + + .PARAMETER DatabasePath + The location on the disk where the database is stored. + + .PARAMETER DisableSecurityBestPractices + A list of exceptions to the security best practices to apply. + + .PARAMETER Enable32BitAppOnWin64 + Enable the DSC Pull Server to run in a 32-bit process on a 64-bit operating + system. + + .PARAMETER Ensure + Specifies if the DSC Web Service should be installed. + + .PARAMETER PhysicalPath + The physical path for the IIS Endpoint on the machine (usually under inetpub). + + .PARAMETER Port + The port number of the DSC Pull Server IIS Endpoint. + + .PARAMETER ModulePath + The location on the disk where the Modules are stored. + + .PARAMETER RegistrationKeyPath + The location on the disk where the RegistrationKeys file is stored. + + .PARAMETER SqlConnectionString + The connection string to use to connect to the SQL server backend database. + Required if SqlProvider is true. + + .PARAMETER SqlProvider + Enable DSC Pull Server to use SQL server as the backend database. + + .PARAMETER State + Specifies the state of the DSC Web Service. + + .PARAMETER UseSecurityBestPractices + Ensure that the DSC Pull Server is created using security best practices. +#> +function Set-TargetResource +{ + [CmdletBinding(DefaultParameterSetName = 'CertificateThumbPrint')] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $EndpointName, + + [Parameter()] + [System.Boolean] + $AcceptSelfSignedCertificates = $true, + + [Parameter()] + [ValidateNotNullOrEmpty()] + [System.String] + $ApplicationPoolName = $DscWebServiceDefaultAppPoolName, + + [Parameter(ParameterSetName = 'CertificateSubject')] + [ValidateNotNullOrEmpty()] + [System.String] + $CertificateSubject, + + [Parameter(ParameterSetName = 'CertificateSubject')] + [ValidateNotNullOrEmpty()] + [System.String] + $CertificateTemplateName = 'WebServer', + + [Parameter(ParameterSetName = 'CertificateThumbPrint')] + [ValidateNotNullOrEmpty()] + [System.String] + $CertificateThumbPrint, + + [Parameter()] + [System.String] + $ConfigurationPath = "$env:PROGRAMFILES\WindowsPowerShell\DscService\Configuration", + + [Parameter()] + [System.Boolean] + $ConfigureFirewall = $true, + + [Parameter()] + [ValidateNotNullOrEmpty()] + [System.String] + $DatabasePath = "$env:PROGRAMFILES\WindowsPowerShell\DscService", + + [Parameter()] + [ValidateSet('SecureTLSProtocols')] + [System.String[]] + $DisableSecurityBestPractices, + + [Parameter()] + [System.Boolean] + $Enable32BitAppOnWin64 = $false, + + [Parameter()] + [ValidateSet('Present', 'Absent')] + [System.String] + $Ensure = 'Present', + + [Parameter()] + [System.String] + $ModulePath = "$env:PROGRAMFILES\WindowsPowerShell\DscService\Modules", + + [Parameter()] + [System.String] + $PhysicalPath = "$env:SystemDrive\inetpub\$EndpointName", + + [Parameter()] + [ValidateRange(1, 65535)] + [System.UInt32] + $Port = 8080, + + [Parameter()] + [System.String] + $RegistrationKeyPath = "$env:PROGRAMFILES\WindowsPowerShell\DscService", + + [Parameter()] + [System.String] + $SqlConnectionString, + + [Parameter()] + [System.Boolean] + $SqlProvider = $false, + + [Parameter()] + [ValidateSet('Started', 'Stopped')] + [System.String] + $State = 'Started', + + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.Boolean] + $UseSecurityBestPractices + ) + + <# + If Certificate Subject is not specified then a value for CertificateThumbprint + must be explicitly set instead. The Mof schema doesn't allow for a mandatory parameter + in a parameter set. + #> + if ($PScmdlet.ParameterSetName -eq 'CertificateThumbPrint' -and $PSBoundParameters.ContainsKey('CertificateThumbPrint') -ne $true) + { + throw $script:localizedData.ThrowCertificateThumbprint + } + + # Find a certificate that matches the Subject and Template Name + if ($PSCmdlet.ParameterSetName -eq 'CertificateSubject') + { + $certificateThumbPrint = Find-CertificateThumbprintWithSubjectAndTemplateName -Subject $CertificateSubject -TemplateName $CertificateTemplateName + } + + # Check parameter values + if ($UseSecurityBestPractices -and ($CertificateThumbPrint -eq 'AllowUnencryptedTraffic')) + { + throw $script:localizedData.ThrowUseSecurityBestPractice + } + + if ($ConfigureFirewall) + { + Write-Warning -Message $script:localizedData.ConfigFirewallDeprecated + } + + <# + If the Pull Server Site should be bound to the non default AppPool + ensure that the AppPool already exists + #> + if ('Present' -eq $Ensure ` + -and $ApplicationPoolName -ne $DscWebServiceDefaultAppPoolName ` + -and (-not (Test-Path -Path "IIS:\AppPools\$ApplicationPoolName"))) + { + throw ($script:localizedData.ThrowApplicationPoolNotFound -f $ApplicationPoolName) + } + + # Initialize with default values + $pathPullServer = "$pshome\modules\PSDesiredStateConfiguration\PullServer" + $jet4provider = 'System.Data.OleDb' + $jet4database = 'Provider=Microsoft.Jet.OLEDB.4.0;Data Source=$DatabasePath\Devices.mdb;' + $eseprovider = 'ESENT' + $esedatabase = "$DatabasePath\Devices.edb" + + $cultureInfo = Get-Culture + $languagePath = $cultureInfo.IetfLanguageTag + $language = $cultureInfo.TwoLetterISOLanguageName + + # The two letter iso languagename is not actually implemented in the source path, it's always 'en' + if (-not (Test-Path -Path "$pathPullServer\$languagePath\Microsoft.Powershell.DesiredStateConfiguration.Service.Resources.dll")) + { + $languagePath = 'en' + } + + $os = Get-OSVersion + + $isBlue = $false + + if ($os.Major -eq 6 -and $os.Minor -eq 3) + { + $isBlue = $true + } + + $isDownlevelOfBlue = $false + + if ($os.Major -eq 6 -and $os.Minor -lt 3) + { + $isDownlevelOfBlue = $true + } + + # Use Pull Server values for defaults + $webConfigFileName = "$pathPullServer\PSDSCPullServer.config" + $svcFileName = "$pathPullServer\PSDSCPullServer.svc" + $pswsMofFileName = "$pathPullServer\PSDSCPullServer.mof" + $pswsDispatchFileName = "$pathPullServer\PSDSCPullServer.xml" + + if (($Ensure -eq 'Absent')) + { + if (Test-Path -LiteralPath "IIS:\Sites\$EndpointName") + { + # Get the port number for the Firewall rule + Write-Verbose -Message "Processing bindings for '$EndpointName'." + $portList = Get-WebBinding -Name $EndpointName | ForEach-Object -Process { + [System.Text.RegularExpressions.Regex]::Match($_.bindingInformation, ':(\d+):').Groups[1].Value + } + + # There is a web site, but there shouldn't be one + Write-Verbose -Message "Removing web site '$EndpointName'." + Remove-PSWSEndpoint -SiteName $EndpointName + + $portList | ForEach-Object -Process { Remove-PullServerFirewallConfiguration -Port $_ } + } + + # We are done here, all stuff below is for 'Present' + return + } + + Write-Verbose -Message 'Create the IIS endpoint' + New-PSWSEndpoint ` + -site $EndpointName ` + -Path $PhysicalPath ` + -cfgfile $webConfigFileName ` + -port $Port ` + -appPool $ApplicationPoolName ` + -applicationPoolIdentityType LocalSystem ` + -app $EndpointName ` + -svc $svcFileName ` + -mof $pswsMofFileName ` + -dispatch $pswsDispatchFileName ` + -asax "$pathPullServer\Global.asax" ` + -dependentBinaries "$pathPullServer\Microsoft.Powershell.DesiredStateConfiguration.Service.dll" ` + -language $language ` + -dependentMUIFiles "$pathPullServer\$languagePath\Microsoft.Powershell.DesiredStateConfiguration.Service.Resources.dll" ` + -certificateThumbPrint $certificateThumbPrint ` + -Enable32BitAppOnWin64 $Enable32BitAppOnWin64 ` + + switch ($Ensure) + { + 'Present' + { + if ($ConfigureFirewall) + { + Write-Verbose -Message "Enabling firewall exception for port $port." + Add-PullServerFirewallConfiguration -Port $port + } + } + + 'Absent' + { + Write-Verbose -Message "Disabling firewall exception for port $port." + Remove-PullServerFirewallConfiguration -Port $port + } + } + + Update-LocationTagInApplicationHostConfigForAuthentication -WebSite $EndpointName -Authentication 'anonymous' + Update-LocationTagInApplicationHostConfigForAuthentication -WebSite $EndpointName -Authentication 'basic' + Update-LocationTagInApplicationHostConfigForAuthentication -WebSite $EndpointName -Authentication 'windows' + + if ($SqlProvider) + { + Write-Verbose -Message 'Set values into the web.config that define the SQL Connection' + Set-AppSettingsInWebconfig -Path $PhysicalPath -Key 'dbprovider' -Value $jet4provider + Set-AppSettingsInWebconfig -Path $PhysicalPath -Key 'dbconnectionstr' -Value $SqlConnectionString + + if ($isBlue) + { + Set-BindingRedirectSettingInWebConfig -Path $PhysicalPath + } + } + elseif ($isBlue) + { + Write-Verbose -Message 'Set values into the web.config that define the repository for BLUE OS' + Set-AppSettingsInWebconfig -Path $PhysicalPath -Key 'dbprovider' -Value $eseprovider + Set-AppSettingsInWebconfig -Path $PhysicalPath -Key 'dbconnectionstr' -Value $esedatabase + + Set-BindingRedirectSettingInWebConfig -Path $PhysicalPath + } + else + { + if ($isDownlevelOfBlue) + { + Write-Verbose -Message 'Set values into the web.config that define the repository for non-BLUE Downlevel OS' + $repository = Join-Path -Path $DatabasePath -ChildPath 'Devices.mdb' + Copy-Item -Path "$pathPullServer\Devices.mdb" -Destination $repository -Force + + Set-AppSettingsInWebconfig -Path $PhysicalPath -Key 'dbprovider' -Value $jet4provider + Set-AppSettingsInWebconfig -Path $PhysicalPath -Key 'dbconnectionstr' -Value $jet4database + } + else + { + Write-Verbose -Message 'Set values into the web.config that define the repository later than BLUE OS' + Write-Verbose -Message 'Only ESENT is supported on Windows Server 2016' + + Set-AppSettingsInWebconfig -Path $PhysicalPath -Key 'dbprovider' -Value $eseprovider + Set-AppSettingsInWebconfig -Path $PhysicalPath -Key 'dbconnectionstr' -Value $esedatabase + } + } + + Write-Verbose -Message 'Pull Server: Set values into the web.config that indicate the location of repository, configuration, modules' + + # Create the application data directory calculated above + $null = New-Item -Path $DatabasePath -ItemType 'directory' -Force + $null = New-Item -Path $ConfigurationPath -ItemType 'directory' -Force + + Set-AppSettingsInWebconfig -Path $PhysicalPath -Key 'ConfigurationPath' -Value $configurationPath + + $null = New-Item -Path $ModulePath -ItemType 'directory' -Force + + Set-AppSettingsInWebconfig -Path $PhysicalPath -Key 'ModulePath' -Value $ModulePath + + $null = New-Item -Path $RegistrationKeyPath -ItemType 'directory' -Force + + Set-AppSettingsInWebconfig -Path $PhysicalPath -Key 'RegistrationKeyPath' -Value $registrationKeyPath + + if ($AcceptSelfSignedCertificates) + { + Write-Verbose -Message 'Accepting self signed certificates from incoming hosts' + Enable-IISSelfSignedModule -EndpointName $EndpointName -Enable32BitAppOnWin64:$Enable32BitAppOnWin64 + } + else + { + Disable-IISSelfSignedModule -EndpointName $EndpointName + } + + if ($UseSecurityBestPractices) + { + Set-UseSecurityBestPractice -DisableSecurityBestPractices $DisableSecurityBestPractices + } +} + +<# + .SYNOPSIS + Test the state of the DSC Web Service. + + .PARAMETER EndpointName + Prefix of the WCF SVC file. + + .PARAMETER AcceptSelfSignedCertificates + Specifies is self-signed certs will be accepted for client authentication. + + .PARAMETER ApplicationPoolName + The IIS ApplicationPool to use for the Pull Server. If not specified a + pool with name 'PSWS' will be created. + + .PARAMETER CertificateSubject + The subject of the Certificate in CERT:\LocalMachine\MY\ for Pull Server. + + .PARAMETER CertificateTemplateName + The certificate Template Name of the Certificate in CERT:\LocalMachine\MY\ + for Pull Server. + + .PARAMETER CertificateThumbPrint + The thumbprint of the Certificate in CERT:\LocalMachine\MY\ for Pull Server. + + .PARAMETER ConfigurationPath + The location on the disk where the Configuration is stored. + + .PARAMETER ConfigureFirewall + Enable incomming firewall exceptions for the configured DSC Pull Server + port. Defaults to true. + + .PARAMETER DatabasePath + The location on the disk where the database is stored. + + .PARAMETER DisableSecurityBestPractices + A list of exceptions to the security best practices to apply. + + .PARAMETER Enable32BitAppOnWin64 + Enable the DSC Pull Server to run in a 32-bit process on a 64-bit operating + system. + + .PARAMETER Ensure + Specifies if the DSC Web Service should be installed. + + .PARAMETER PhysicalPath + The physical path for the IIS Endpoint on the machine (usually under inetpub). + + .PARAMETER Port + The port number of the DSC Pull Server IIS Endpoint. + + .PARAMETER ModulePath + The location on the disk where the Modules are stored. + + .PARAMETER RegistrationKeyPath + The location on the disk where the RegistrationKeys file is stored. + + .PARAMETER SqlConnectionString + The connection string to use to connect to the SQL server backend database. + Required if SqlProvider is true. + + .PARAMETER SqlProvider + Enable DSC Pull Server to use SQL server as the backend database. + + .PARAMETER State + Specifies the state of the DSC Web Service. + + .PARAMETER UseSecurityBestPractices + Ensure that the DSC Pull Server is created using security best practices. +#> +function Test-TargetResource +{ + [CmdletBinding(DefaultParameterSetName = 'CertificateThumbPrint')] + [OutputType([System.Boolean])] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $EndpointName, + + [Parameter()] + [System.Boolean] + $AcceptSelfSignedCertificates, + + [Parameter()] + [ValidateNotNullOrEmpty()] + [System.String] + $ApplicationPoolName = $DscWebServiceDefaultAppPoolName, + + [Parameter(ParameterSetName = 'CertificateSubject')] + [ValidateNotNullOrEmpty()] + [System.String] + $CertificateSubject, + + [Parameter(ParameterSetName = 'CertificateSubject')] + [ValidateNotNullOrEmpty()] + [System.String] + $CertificateTemplateName = 'WebServer', + + [Parameter(ParameterSetName = 'CertificateThumbPrint')] + [ValidateNotNullOrEmpty()] + [System.String] + $CertificateThumbPrint, + + [Parameter()] + [System.String] + $ConfigurationPath = "$env:PROGRAMFILES\WindowsPowerShell\DscService\Configuration", + + [Parameter()] + [System.Boolean] + $ConfigureFirewall = $true, + + [Parameter()] + [ValidateNotNullOrEmpty()] + [System.String] + $DatabasePath = "$env:PROGRAMFILES\WindowsPowerShell\DscService", + + [Parameter()] + [ValidateSet('SecureTLSProtocols')] + [System.String[]] + $DisableSecurityBestPractices, + + [Parameter()] + [System.Boolean] + $Enable32BitAppOnWin64 = $false, + + [Parameter()] + [ValidateSet('Present', 'Absent')] + [System.String] + $Ensure = 'Present', + + [Parameter()] + [System.String] + $ModulePath = "$env:PROGRAMFILES\WindowsPowerShell\DscService\Modules", + + [Parameter()] + [System.String] + $PhysicalPath = "$env:SystemDrive\inetpub\$EndpointName", + + [Parameter()] + [ValidateRange(1, 65535)] + [System.UInt32] + $Port = 8080, + + [Parameter()] + [System.String] + $RegistrationKeyPath, + + [Parameter()] + [System.String] + $SqlConnectionString, + + [Parameter()] + [System.Boolean] + $SqlProvider = $false, + + [Parameter()] + [ValidateSet('Started', 'Stopped')] + [System.String] + $State = 'Started', + + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.Boolean] + $UseSecurityBestPractices + ) + + <# + If Certificate Subject is not specified then a value for CertificateThumbprint + must be explicitly set instead. The Mof schema doesn't allow for a mandatory + parameter in a parameter set. + #> + if ($PScmdlet.ParameterSetName -eq 'CertificateThumbPrint' -and $PSBoundParameters.ContainsKey('CertificateThumbPrint') -ne $true) + { + throw $script:localizedData.ThrowCertificateThumbprint + } + + $desiredConfigurationMatch = $true + + $website = Get-Website -Name $EndpointName + $stop = $true + + :WebSiteTests do + { + Write-Verbose -Message 'Check Ensure.' + + if (($Ensure -eq 'Present' -and $null -eq $website)) + { + $desiredConfigurationMatch = $false + Write-Verbose -Message "The Website '$EndpointName' is not present." + break + } + + if (($Ensure -eq 'Absent' -and $null -ne $website)) + { + $desiredConfigurationMatch = $false + Write-Verbose -Message "The Website '$EndpointName' is present but should not be." + break + } + + if (($Ensure -eq 'Absent' -and $null -eq $website)) + { + $desiredConfigurationMatch = $true + Write-Verbose -Message "The Website '$EndpointName' is not present as requested." + break + } + + # The other case is: Ensure and exist, we continue with more checks + Write-Verbose -Message 'Check Port.' + $actualPort = $website.bindings.Collection[0].bindingInformation.Split(':')[1] + + if ($Port -ne $actualPort) + { + $desiredConfigurationMatch = $false + Write-Verbose -Message "Port for the Website '$EndpointName' does not match the desired state." + break + } + + Write-Verbose -Message 'Check Application Pool.' + + if ($ApplicationPoolName -ne $website.applicationPool) + { + $desiredConfigurationMatch = $false + Write-Verbose -Message "Currently bound application pool '$($website.applicationPool)' does not match the desired state '$ApplicationPoolName'." + break + } + + Write-Verbose -Message 'Check Binding.' + $actualCertificateHash = $website.bindings.Collection[0].certificateHash + $websiteProtocol = $website.bindings.collection[0].Protocol + + Write-Verbose -Message 'Checking firewall rule settings.' + $ruleExists = Test-PullServerFirewallConfiguration -Port $Port + + if ($ruleExists -and -not $ConfigureFirewall) + { + $desiredConfigurationMatch = $false + Write-Verbose -Message "Firewall rule exists for $Port and should not. Configuration does not match the desired state." + break + } + elseif (-not $ruleExists -and $ConfigureFirewall) + { + $desiredConfigurationMatch = $false + Write-Verbose -Message "Firewall rule does not exist for $Port and should. Configuration does not match the desired state." + break + } + + switch ($PSCmdlet.ParameterSetName) + { + 'CertificateThumbprint' + { + if ($CertificateThumbPrint -eq 'AllowUnencryptedTraffic' -and $websiteProtocol -ne 'http') + { + $desiredConfigurationMatch = $false + Write-Verbose -Message "Website '$EndpointName' is not configured for http and does not match the desired state." + break WebSiteTests + } + + if ($CertificateThumbPrint -ne 'AllowUnencryptedTraffic' -and $websiteProtocol -ne 'https') + { + $desiredConfigurationMatch = $false + Write-Verbose -Message "Website '$EndpointName' is not configured for https and does not match the desired state." + break WebSiteTests + } + } + + 'CertificateSubject' + { + $certificateThumbPrint = Find-CertificateThumbprintWithSubjectAndTemplateName -Subject $CertificateSubject -TemplateName $CertificateTemplateName + + if ($CertificateThumbPrint -ne $actualCertificateHash) + { + $desiredConfigurationMatch = $false + Write-Verbose -Message "Certificate Hash for the Website '$EndpointName' does not match the desired state." + break WebSiteTests + } + } + } + + Write-Verbose -Message 'Check Physical Path property.' + + if (Test-WebsitePath -EndpointName $EndpointName -PhysicalPath $PhysicalPath) + { + $desiredConfigurationMatch = $false + Write-Verbose -Message "Physical Path of Website '$EndpointName' does not match the desired state." + break + } + + Write-Verbose -Message 'Check State.' + + if ($website.state -ne $State -and $null -ne $State) + { + $desiredConfigurationMatch = $false + Write-Verbose -Message "The state of Website '$EndpointName' does not match the desired state." + break + } + + Write-Verbose -Message 'Get Full Path for Web.config file.' + $webConfigFullPath = Join-Path -Path $website.physicalPath -ChildPath 'web.config' + + # Changed from -eq $false to -ne $true as $IsComplianceServer is never set. This section was always being skipped + if ($IsComplianceServer -ne $true) + { + Write-Verbose -Message 'Check DatabasePath.' + + switch ((Get-WebConfigAppSetting -WebConfigFullPath $webConfigFullPath -AppSettingName 'dbprovider')) + { + 'ESENT' + { + $expectedConnectionString = "$DatabasePath\Devices.edb" + } + + 'System.Data.OleDb' + { + $expectedConnectionString = "Provider=Microsoft.Jet.OLEDB.4.0;Data Source=$DatabasePath\Devices.mdb;" + } + + default + { + $expectedConnectionString = [System.String]::Empty + } + } + + if ($SqlProvider) + { + $expectedConnectionString = $SqlConnectionString + } + + if (([System.String]::IsNullOrEmpty($expectedConnectionString))) + { + $desiredConfigurationMatch = $false + Write-Verbose -Message "The DB provider does not have a valid value: 'ESENT' or 'System.Data.OleDb'." + break + } + + if (-not (Test-WebConfigAppSetting ` + -WebConfigFullPath $webConfigFullPath ` + -AppSettingName 'dbconnectionstr' ` + -ExpectedAppSettingValue $expectedConnectionString)) + { + $desiredConfigurationMatch = $false + break + } + + Write-Verbose -Message 'Check ModulePath.' + + if ($ModulePath) + { + if (-not (Test-WebConfigAppSetting ` + -WebConfigFullPath $webConfigFullPath ` + -AppSettingName 'ModulePath' ` + -ExpectedAppSettingValue $ModulePath)) + { + $desiredConfigurationMatch = $false + break + } + } + + Write-Verbose -Message 'Check ConfigurationPath.' + + if ($ConfigurationPath) + { + if (-not (Test-WebConfigAppSetting ` + -WebConfigFullPath $webConfigFullPath ` + -AppSettingName 'ConfigurationPath' ` + -ExpectedAppSettingValue $configurationPath)) + { + $desiredConfigurationMatch = $false + break + } + } + + Write-Verbose -Message 'Check RegistrationKeyPath.' + + if ($RegistrationKeyPath) + { + if (-not (Test-WebConfigAppSetting ` + -WebConfigFullPath $webConfigFullPath ` + -AppSettingName 'RegistrationKeyPath' ` + -ExpectedAppSettingValue $registrationKeyPath)) + { + $desiredConfigurationMatch = $false + break + } + } + + Write-Verbose -Message 'Check AcceptSelfSignedCertificates.' + + if ($AcceptSelfSignedCertificates) + { + Write-Verbose -Message "AcceptSelfSignedCertificates is enabled. Checking if module Selfsigned IIS module is configured for web site at '$webConfigFullPath'." + + if (Test-IISSelfSignedModuleInstalled) + { + if (-not (Test-IISSelfSignedModuleEnabled -EndpointName $EndpointName)) + { + Write-Verbose -Message 'Module not enabled in web site. Current configuration does not match the desired state.' + $desiredConfigurationMatch = $false + break + } + else + { + Write-Verbose -Message 'Module present in web site. Current configuration match the desired state.' + } + } + else + { + Write-Verbose -Message 'Selfsigned module not installed in IIS. Current configuration does not match the desired state.' + $desiredConfigurationMatch = $false + } + } + else + { + Write-Verbose -Message "AcceptSelfSignedCertificates is disabled. Checking if module Selfsigned IIS module is NOT configured for web site at '$webConfigFullPath'." + + if (Test-IISSelfSignedModuleInstalled) + { + if (-not (Test-IISSelfSignedModuleEnabled -EndpointName $EndpointName)) + { + Write-Verbose -Message 'Module not enabled in web site. Current configuration does match the desired state.' + } + else + { + Write-Verbose -Message 'Module present in web site. Current configuration does not match the desired state.' + $desiredConfigurationMatch = $false + break + } + } + else + { + Write-Verbose -Message 'Selfsigned module not installed in IIS. Current configuration does match the desired state.' + } + } + } + + Write-Verbose -Message 'Check UseSecurityBestPractices.' + + if ($UseSecurityBestPractices) + { + if (-not (Test-UseSecurityBestPractice -DisableSecurityBestPractices $DisableSecurityBestPractices)) + { + $desiredConfigurationMatch = $false + Write-Verbose -Message 'The state of security settings does not match the desired state.' + break + } + } + + $stop = $false + } + while ($stop) + + return $desiredConfigurationMatch +} + +<# + .SYNOPSIS + The function returns the OS version string detected by .NET. + + .DESCRIPTION + The function returns the OS version which ahs been detected + by .NET. The function is added so that the dectection of the OS + is mockable in Pester tests. + + .OUTPUTS + System.String. The operating system version. +#> +function Get-OSVersion +{ + [CmdletBinding()] + [OutputType([System.String])] + param () + + # Moved to a function to allow for the behaviour to be mocked. + return [System.Environment]::OSVersion.Version +} + +#region IIS Utils + +<# + .SYNOPSIS + Returns the configuration value for a module settings from + web.config. + + .PARAMETER WebConfigFullPath + The full path to the web.config. + + .PARAMETER ModuleName + The name of the IIS module. + + .OUTPUTS + System.String. The configured value. +#> +function Get-WebConfigModulesSetting +{ + [CmdletBinding()] + [OutputType([System.String])] + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $WebConfigFullPath, + + [Parameter(Mandatory = $true)] + [System.String] + $ModuleName + ) + + $moduleValue = '' + + if (Test-Path -Path $WebConfigFullPath) + { + $webConfigXml = [Xml] (Get-Content -Path $WebConfigFullPath) + $root = $webConfigXml.get_DocumentElement() + + foreach ($item in $root.'system.webServer'.modules.add) + { + if ($item.name -eq $ModuleName) + { + $moduleValue = $item.name + break + } + } + } + + return $moduleValue +} + +<# + .SYNOPSIS + Unlocks a specifc authentication configuration section for a IIS website. + + .PARAMETER WebSite + The name of the website. + + .PARAMETER Authentication + The authentication section which should be unlocked. + + .OUTPUTS + System.String. The configured value. +#> +function Update-LocationTagInApplicationHostConfigForAuthentication +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $WebSite, + + [Parameter(Mandatory = $true)] + [ValidateSet('anonymous', 'basic', 'windows')] + [System.String] + $Authentication + ) + + $webAdminSrvMgr = Get-IISServerManager + $appHostConfig = $webAdminSrvMgr.GetApplicationHostConfiguration() + + $authenticationType = $Authentication + 'Authentication' + $appHostConfigSection = $appHostConfig.GetSection("system.webServer/security/authentication/$authenticationType", $WebSite) + $appHostConfigSection.OverrideMode = 'Allow' + $webAdminSrvMgr.CommitChanges() +} + +<# + .SYNOPSIS + Returns an instance of the Microsoft.Web.Administration.ServerManager. + + .OUTPUTS + The server manager as Microsoft.Web.Administration.ServerManager. +#> +function Get-IISServerManager +{ + [CmdletBinding()] + [OutputType([System.Object])] + param () + + $iisInstallPath = (Get-ItemProperty -Path 'HKLM:\SOFTWARE\Microsoft\INetStp' -Name InstallPath).InstallPath + + if (-not $iisInstallPath) + { + throw ($script:localizedData.IISInstallationPathNotFound) + } + + $assyPath = Join-Path -Path $iisInstallPath -ChildPath 'Microsoft.Web.Administration.dll' -Resolve -ErrorAction:SilentlyContinue + + if (-not $assyPath) + { + throw ($script:localizedData.IISWebAdministrationAssemblyNotFound) + } + + $assy = [System.Reflection.Assembly]::LoadFrom($assyPath) + return [System.Activator]::CreateInstance($assy.FullName, 'Microsoft.Web.Administration.ServerManager').Unwrap() +} + +<# + .SYNOPSIS + Tests if a module installation status is equal to an expected status. + + .PARAMETER WebConfigFullPath + The full path to the web.config. + + .PARAMETER ModuleName + The name of the IIS module for which the state should be checked. + + .PARAMETER ExpectedInstallationStatus + Test if the module is installed ($true) or absent ($false). + + .OUTPUTS + Returns true if the current installation status is equal to the expected + installation status. +#> +function Test-WebConfigModulesSetting +{ + [CmdletBinding()] + [OutputType([System.Boolean])] + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $WebConfigFullPath, + + [Parameter(Mandatory = $true)] + [System.String] + $ModuleName, + + [Parameter(Mandatory = $true)] + [Boolean] + $ExpectedInstallationStatus + ) + + if (Test-Path -Path $WebConfigFullPath) + { + $webConfigXml = [Xml] (Get-Content -Path $WebConfigFullPath) + $root = $webConfigXml.get_DocumentElement() + + foreach ($item in $root.'system.webServer'.modules.add) + { + if ( $item.name -eq $ModuleName ) + { + return $ExpectedInstallationStatus -eq $true + } + } + } + else + { + Write-Warning -Message "Test-WebConfigModulesSetting: web.config file not found at '$WebConfigFullPath'" + } + + return $ExpectedInstallationStatus -eq $false +} + +<# + .SYNOPSIS + Tests if a the currently configured path for a website is equal to a given + path. + + .PARAMETER EndpointName + The endpoint name (website name) to test. + + .PARAMETER PhysicalPath + The full physical path to check. + + .OUTPUTS + Returns true if the current installation status is equal to the expected + installation status. +#> +function Test-WebsitePath +{ + [CmdletBinding()] + [OutputType([System.Boolean])] + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $EndpointName, + + [Parameter(Mandatory = $true)] + [System.String] + $PhysicalPath + ) + + $pathNeedsUpdating = $false + + if ((Get-ItemProperty -Path "IIS:\Sites\$EndpointName" -Name physicalPath) -ne $PhysicalPath) + { + $pathNeedsUpdating = $true + } + + return $pathNeedsUpdating +} + +<# + .SYNOPSIS + Test if a currently configured app setting is equal to a given value. + + .PARAMETER WebConfigFullPath + The full path to the web.config. + + .PARAMETER AppSettingName + The app setting name to check. + + .PARAMETER ExpectedAppSettingValue + The expected value. + + .OUTPUTS + Returns true if the current value is equal to the expected value. +#> +function Test-WebConfigAppSetting +{ + [CmdletBinding()] + [OutputType([System.Boolean])] + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $WebConfigFullPath, + + [Parameter(Mandatory = $true)] + [System.String] + $AppSettingName, + + [Parameter(Mandatory = $true)] + [System.String] + $ExpectedAppSettingValue + ) + + $returnValue = $true + + if (Test-Path -Path $WebConfigFullPath) + { + $webConfigXml = [System.Xml.XmlDocument] (Get-Content -Path $WebConfigFullPath) + $root = $webConfigXml.get_DocumentElement() + + foreach ($item in $root.appSettings.add) + { + if ( $item.key -eq $AppSettingName ) + { + break + } + } + + if ($item.value -ne $ExpectedAppSettingValue) + { + $returnValue = $false + Write-Verbose -Message "The state of Web.Config AppSetting '$AppSettingName' does not match the desired state." + } + } + + return $returnValue +} + +<# + .SYNOPSIS + Helper function to Get the specified Web.Config App Setting. + + .PARAMETER WebConfigFullPath + The full path to the web.config. + + .PARAMETER AppSettingName + The app settings name to get the value for. + + .OUTPUTS + The current app settings value. +#> +function Get-WebConfigAppSetting +{ + [CmdletBinding()] + [OutputType([System.String])] + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $WebConfigFullPath, + + [Parameter(Mandatory = $true)] + [System.String] + $AppSettingName + ) + + $appSettingValue = '' + + if (Test-Path -Path $WebConfigFullPath) + { + $webConfigXml = [System.Xml.XmlDocument] (Get-Content -Path $WebConfigFullPath) + $root = $webConfigXml.get_DocumentElement() + + foreach ($item in $root.appSettings.add) + { + if ($item.key -eq $AppSettingName) + { + $appSettingValue = $item.value + break + } + } + } + + return $appSettingValue +} + +#endregion + +#region IIS Selfsigned Certficate Module + +New-Variable -Name iisSelfSignedModuleAssemblyName -Value 'IISSelfSignedCertModule.dll' -Option ReadOnly -Scope Script +New-Variable -Name iisSelfSignedModuleName -Value 'IISSelfSignedCertModule(32bit)' -Option ReadOnly -Scope Script + +<# + .SYNOPSIS + Get a powershell command instance for appcmd.exe. + + .OUTPUTS + The appcmd.exe as System.Management.Automation.CommandInfo. +#> +function Get-IISAppCmd +{ + [CmdletBinding()] + [OutputType([System.Management.Automation.CommandInfo])] + param () + + Push-Location -Path "$env:windir\system32\inetsrv" + $appCmd = Get-Command -Name '.\appcmd.exe' -CommandType 'Application' -ErrorAction 'Stop' + Pop-Location + $appCmd +} + +<# + .SYNOPSIS + Tests if two files differ. + + .PARAMETER SourceFilePath + Path to the source file. + + .PARAMETER DestinationFilePath + Path to the destination file. + + .OUTPUTS + Returns true if the two files differ. +#> +function Test-FilesDiffer +{ + [CmdletBinding()] + [OutputType([System.Boolean])] + param + ( + [Parameter(Mandatory = $true)] + [ValidateScript( { Test-Path -PathType Leaf -LiteralPath $_ })] + [System.String] + $SourceFilePath, + + [Parameter()] + [System.String] + $DestinationFilePath + ) + + Write-Verbose -Message "Testing for file difference between '$SourceFilePath' and '$DestinationFilePath'." + + if (Test-Path -LiteralPath $DestinationFilePath) + { + if (Test-Path -LiteralPath $DestinationFilePath -PathType Container) + { + throw "$DestinationFilePath is a container (Directory) not a leaf (File)" + } + + Write-Verbose -Message "Destination file already exists at '$DestinationFilePath'." + $md5Dest = Get-FileHash -LiteralPath $destinationFilePath -Algorithm MD5 + $md5Src = Get-FileHash -LiteralPath $sourceFilePath -Algorithm MD5 + return $md5Src.Hash -ne $md5Dest.Hash + } + else + { + Write-Verbose -Message "Destination file does not exist at '$DestinationFilePath'." + return $true + } +} + +<# + .SYNOPSIS + Tests if the IISSelfSignedModule module is installed. + + .OUTPUTS + Returns true if the module is installed. +#> +function Test-IISSelfSignedModuleInstalled +{ + [CmdletBinding()] + [OutputType([System.Boolean])] + param () + + ('' -ne ((& (Get-IISAppCmd) list config -section:system.webServer/globalModules) -like "*$iisSelfSignedModuleName*")) +} + +<# + .SYNOPSIS + Install the IISSelfSignedModule module. + + .PARAMETER Enable32BitAppOnWin64 + If set install the module as 32bit module. +#> +function Install-IISSelfSignedModule +{ + [CmdletBinding()] + param + ( + [Parameter()] + [Switch] + $Enable32BitAppOnWin64 + ) + + if ($Enable32BitAppOnWin64) + { + Write-Verbose -Message "Install-IISSelfSignedModule: Providing '$iisSelfSignedModuleAssemblyName' to run in a 32 bit process." + + $sourceFilePath = Join-Path -Path "$env:windir\SysWOW64\WindowsPowerShell\v1.0\Modules\PSDesiredStateConfiguration\PullServer" ` + -ChildPath $iisSelfSignedModuleAssemblyName + $destinationFolderPath = "$env:windir\SysWOW64\inetsrv" + + Copy-Item -Path $sourceFilePath -Destination $destinationFolderPath -Force + } + + if (Test-IISSelfSignedModuleInstalled) + { + Write-Verbose -Message "Install-IISSelfSignedModule: module '$iisSelfSignedModuleName' already installed." + } + else + { + Write-Verbose -Message "Install-IISSelfSignedModule: Installing module '$iisSelfSignedModuleName'." + $sourceFilePath = Join-Path -Path "$env:windir\System32\WindowsPowerShell\v1.0\Modules\PSDesiredStateConfiguration\PullServer" ` + -ChildPath $iisSelfSignedModuleAssemblyName + $destinationFolderPath = "$env:windir\System32\inetsrv" + $destinationFilePath = Join-Path -Path $destinationFolderPath ` + -ChildPath $iisSelfSignedModuleAssemblyName + + if (Test-FilesDiffer -SourceFilePath $sourceFilePath -DestinationFilePath $destinationFilePath) + { + # Might fail if the DLL has already been loaded by the IIS from a former PullServer Deployment + Copy-Item -Path $sourceFilePath -Destination $destinationFolderPath -Force + } + else + { + Write-Verbose -Message "Install-IISSelfSignedModule: module '$iisSelfSignedModuleName' already installed at '$destinationFilePath' with the correct version." + } + + Write-Verbose -Message "Install-IISSelfSignedModule: globally activating module '$iisSelfSignedModuleName'." + + & (Get-IISAppCmd) install module /name:$iisSelfSignedModuleName /image:$destinationFilePath /add:false /lock:false + } +} + +<# + .SYNOPSIS + Enable the IISSelfSignedModule module for a specific website (endpoint). + + .PARAMETER EndpointName + The endpoint (website) for which the module should be enabled. + + .PARAMETER Enable32BitAppOnWin64 + If set enable the module as a 32bit module. +#> +function Enable-IISSelfSignedModule +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $EndpointName, + + [Parameter()] + [Switch] + $Enable32BitAppOnWin64 + ) + + Write-Verbose -Message "Enable-IISSelfSignedModule: EndpointName '$EndpointName' and Enable32BitAppOnWin64 '$Enable32BitAppOnWin64'" + + Install-IISSelfSignedModule -Enable32BitAppOnWin64:$Enable32BitAppOnWin64 + $preConditionBitnessArgumentFor32BitInstall = '' + + if ($Enable32BitAppOnWin64) + { + $preConditionBitnessArgumentFor32BitInstall = '/preCondition:bitness32' + } + + & (Get-IISAppCmd) add module /name:$iisSelfSignedModuleName /app.name:"$EndpointName/" $preConditionBitnessArgumentFor32BitInstall +} + +<# + .SYNOPSIS + Disable the IISSelfSignedModule module for a specific website (endpoint). + + .PARAMETER EndpointName + The endpoint (website) for which the module should be disabled. +#> +function Disable-IISSelfSignedModule +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String]$EndpointName + ) + + Write-Verbose -Message "Disable-IISSelfSignedModule: EndpointName '$EndpointName'" + + & (Get-IISAppCmd) delete module /name:$iisSelfSignedModuleName /app.name:"$EndpointName/" +} + +<# + .SYNOPSIS + Tests if the IISSelfSignedModule module is enabled for a website (endpoint). + + .PARAMETER EndpointName + The endpoint (website) for which the status should be checked. + + .OUTPUTS + Returns true if the module is enabled. +#> +function Test-IISSelfSignedModuleEnabled +{ + [CmdletBinding()] + [OutputType([System.Boolean])] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $EndpointName + ) + + Write-Verbose -Message "Test-IISSelfSignedModuleEnabled: EndpointName '$EndpointName'" + + $webSite = Get-Website -Name $EndpointName + + if ($webSite) + { + $webConfigFullPath = Join-Path -Path $website.physicalPath -ChildPath 'web.config' + Write-Verbose -Message "Test-IISSelfSignedModuleEnabled: web.confg path '$webConfigFullPath'" + Test-WebConfigModulesSetting -WebConfigFullPath $webConfigFullPath -ModuleName $iisSelfSignedModuleName -ExpectedInstallationStatus $true + } + else + { + throw "Website '$EndpointName' not found" + } +} + +#endregion + +#region Certificate Utils + +<# + .SYNOPSIS + Returns a certificate thumbprint from a certificate with a matching subject. + + .DESCRIPTION + Retreives a list of certificates from the a certificate store. + From this list all certificates will be checked to see if they match the supplied Subject and Template. + If one certificate is found the thumbrpint is returned. Otherwise an error is thrown. + + .PARAMETER Subject + The subject of the certificate to find the thumbprint of. + + .PARAMETER TemplateName + The template used to create the certificate to find the subject of. + + .PARAMETER Store + The certificate store to retrieve certificates from. + + .NOTES + Uses certificate Oid mapping: + 1.3.6.1.4.1.311.20.2 = Certificate Template Name + 1.3.6.1.4.1.311.21.7 = Certificate Template Information +#> +function Find-CertificateThumbprintWithSubjectAndTemplateName +{ + [CmdletBinding()] + [OutputType([System.String])] + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $Subject, + + [Parameter(Mandatory = $true)] + [System.String] + $TemplateName, + + [Parameter()] + [System.String] + $Store = 'Cert:\LocalMachine\My' + ) + + $filteredCertificates = @() + + foreach ($oidFriendlyName in 'Certificate Template Name', 'Certificate Template Information') + { + # Only get certificates created from a template otherwise filtering by subject and template name will cause errors + [System.Array] $certificatesFromTemplates = (Get-ChildItem -Path $Store).Where{ + $_.Extensions.Oid.FriendlyName -contains $oidFriendlyName + } + + switch ($oidFriendlyName) + { + 'Certificate Template Name' + { + $templateMatchString = $TemplateName + } + + 'Certificate Template Information' + { + $templateMatchString = '^Template={0}' -f $TemplateName + } + } + + $filteredCertificates += $certificatesFromTemplates.Where{ + $_.Subject -eq $Subject -and + $_.Extensions.Where{ + $_.Oid.FriendlyName -eq $oidFriendlyName + }.Format($false) -match $templateMatchString + } + } + + if ($filteredCertificates.Count -eq 1) + { + return $filteredCertificates.Thumbprint + } + elseif ($filteredCertificates.Count -gt 1) + { + throw ($script:localizedData.FindCertificateBySubjectMultiple -f $Subject, $TemplateName) + } + else + { + throw ($script:localizedData.FindCertificateBySubjectNotFound -f $Subject, $TemplateName) + } +} + +#endregion + +Export-ModuleMember -Function *-TargetResource diff --git a/deployment/dsc/azshcihost/xPSDesiredStateConfiguration/9.1.0/DSCResources/DSC_xDSCWebService/DSC_xDSCWebService.schema.mof b/deployment/dsc/azshcihost/xPSDesiredStateConfiguration/9.1.0/DSCResources/DSC_xDSCWebService/DSC_xDSCWebService.schema.mof new file mode 100644 index 0000000..28be58d --- /dev/null +++ b/deployment/dsc/azshcihost/xPSDesiredStateConfiguration/9.1.0/DSCResources/DSC_xDSCWebService/DSC_xDSCWebService.schema.mof @@ -0,0 +1,25 @@ +[ClassVersion("1.0.0"), FriendlyName("xDSCWebService")] +class DSC_xDSCWebService : OMI_BaseResource +{ + [Key, Description("Prefix of the WCF SVC file.")] string EndpointName; + [Write, Description("Specifies is self-signed certs will be accepted for client authentication.")] boolean AcceptSelfSignedCertificates; + [Write, Description("The IIS ApplicationPool to use for the Pull Server. If not specified a pool with name 'PSWS' will be created.")] string ApplicationPoolName; + [Write, Description("The subject of the Certificate in CERT:\\LocalMachine\\MY\\ for Pull Server.")] string CertificateSubject; + [Write, Description("The certificate Template Name of the Certificate in CERT:\\LocalMachine\\MY\\ for Pull Server.")] string CertificateTemplateName; + [Write, Description("The thumbprint of the Certificate in CERT:\\LocalMachine\\MY\\ for Pull Server.")] string CertificateThumbPrint; + [Write, Description("The location on the disk where the Configuration is stored.")] string ConfigurationPath; + [Write, Description("Enable incomming firewall exceptions for the configured DSC Pull Server port. Defaults to true.")] boolean ConfigureFirewall; + [Write, Description("The location on the disk where the database is stored.")] string DatabasePath; + [Write, Description("A list of exceptions to the security best practices to apply."), ValueMap{"SecureTLSProtocols"},Values{"SecureTLSProtocols"}] string DisableSecurityBestPractices []; + [Write, Description("Enable the DSC Pull Server to run in a 32-bit process on a 64-bit operating system.")] boolean Enable32BitAppOnWin64; + [Write, Description("Specifies if the DSC Web Service should be installed."), ValueMap{"Present","Absent"},Values{"Present","Absent"}] string Ensure; + [Write, Description("The location on the disk where the Modules are stored.")] string ModulePath; + [Write, Description("The physical path for the IIS Endpoint on the machine (usually under inetpub).")] string PhysicalPath; + [Write, Description("The port number of the DSC Pull Server IIS Endpoint.")] uint32 Port; + [Write, Description("The location on the disk where the RegistrationKeys file is stored.")] string RegistrationKeyPath; + [Write, Description("The connection string to use to connect to the SQL server backend database. Required if SqlProvider is true.")] string SqlConnectionString; + [Write, Description("Enable DSC Pull Server to use SQL server as the backend database.")] boolean SqlProvider; + [Write, Description("Specifies the state of the DSC Web Service."), ValueMap{"Started","Stopped"},Values{"Started","Stopped"}] string State; + [Required, Description("This property will ensure that the Pull Server is created with the most secure practices.")] boolean UseSecurityBestPractices; + [Read, Description("The URL of the DSC Pull Server.")] string DSCServerUrl; +}; diff --git a/deployment/dsc/azshcihost/xPSDesiredStateConfiguration/9.1.0/DSCResources/DSC_xDSCWebService/en-US/DSC_xDSCWebService.strings.psd1 b/deployment/dsc/azshcihost/xPSDesiredStateConfiguration/9.1.0/DSCResources/DSC_xDSCWebService/en-US/DSC_xDSCWebService.strings.psd1 new file mode 100644 index 0000000..28cdc70 --- /dev/null +++ b/deployment/dsc/azshcihost/xPSDesiredStateConfiguration/9.1.0/DSCResources/DSC_xDSCWebService/en-US/DSC_xDSCWebService.strings.psd1 @@ -0,0 +1,11 @@ +# culture="en-US" +ConvertFrom-StringData -StringData @' + ThrowCertificateThumbprint = CertificateThumbprint must contain a certificate thumbprint, or "AllowUnencryptedTraffic" to opt-out from being secure. + ThrowUseSecurityBestPractice = Error: Cannot use best practice security settings with unencrypted traffic. Please set UseSecurityBestPractices to $false or use a certificate to encrypt pull server traffic. + FindCertificateBySubjectMultiple = More than one certificate found with subject containing {0} and using template "{1}". + FindCertificateBySubjectNotFound = Certificate not found with subject containing {0} and using template "{1}". + IISInstallationPathNotFound = IIS installation path not found + IISWebAdministrationAssemblyNotFound = IIS version of Microsoft.Web.Administration.dll not found + ConfigFirewallDeprecated = The support for configuring firewall rules is deprecated. Please set ConfigureFirewall to false and use the Firewall resource from NetworkingDSC module to configure required firewall rules. + ThrowApplicationPoolNotFound = IIS Application pool "{0}" not found. +'@ diff --git a/deployment/dsc/azshcihost/xPSDesiredStateConfiguration/9.1.0/DSCResources/DSC_xEnvironmentResource/DSC_xEnvironmentResource.psm1 b/deployment/dsc/azshcihost/xPSDesiredStateConfiguration/9.1.0/DSCResources/DSC_xEnvironmentResource/DSC_xEnvironmentResource.psm1 new file mode 100644 index 0000000..90f6261 --- /dev/null +++ b/deployment/dsc/azshcihost/xPSDesiredStateConfiguration/9.1.0/DSCResources/DSC_xEnvironmentResource/DSC_xEnvironmentResource.psm1 @@ -0,0 +1,1156 @@ +$errorActionPreference = 'Stop' +Set-StrictMode -Version 'Latest' + +$modulePath = Join-Path -Path (Split-Path -Path (Split-Path -Path $PSScriptRoot -Parent) -Parent) -ChildPath 'Modules' + +# Import the shared modules +Import-Module -Name (Join-Path -Path $modulePath ` + -ChildPath (Join-Path -Path 'xPSDesiredStateConfiguration.Common' ` + -ChildPath 'xPSDesiredStateConfiguration.Common.psm1')) + +# Import Localization Strings +$script:localizedData = Get-LocalizedData -ResourceName 'DSC_xEnvironmentResource' + +$script:envVarRegPathMachine = 'HKLM:\System\CurrentControlSet\Control\Session Manager\Environment' +$script:envVarRegPathUser = 'HKCU:\Environment' + +$script:maxSystemEnvVariableLength = 1024 +$script:maxUserEnvVariableLength = 255 + +<# + .SYNOPSIS + Retrieves the state of the environment variable. If both Machine and Process Target are + specified, only the machine value will be returned. + + .PARAMETER Name + The name of the environment variable to retrieve. + + .PARAMETER Target + Indicates where to retrieve the variable: The machine or the process. If both are indicated + then only the value from the machine is returned. + The default is both since that is the default for the rest of the resource. +#> +function Get-TargetResource +{ + [CmdletBinding()] + [OutputType([System.Collections.Hashtable])] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $Name, + + [Parameter()] + [ValidateSet('Process', 'Machine')] + [ValidateNotNullOrEmpty()] + [System.String[]] + $Target = ('Process', 'Machine') + ) + + $valueToReturn = $null + + if ($Target -contains 'Machine') + { + $environmentVaraible = Get-EnvironmentVariableWithoutExpanding -Name $Name -ErrorAction 'SilentlyContinue' + + if ($null -ne $environmentVaraible) + { + $valueToReturn = $environmentVaraible.$Name + } + } + else + { + $valueToReturn = Get-ProcessEnvironmentVariable -Name $Name + } + + $environmentResource = @{ + Name = $Name + Value = $null + Ensure = 'Absent' + } + + if ($null -eq $valueToReturn) + { + Write-Verbose -Message ($script:localizedData.EnvVarNotFound -f $Name) + } + else + { + Write-Verbose -Message ($script:localizedData.EnvVarFound -f $Name, $valueToReturn) + $environmentResource.Ensure = 'Present' + $environmentResource.Value = $valueToReturn + } + + return $environmentResource +} + +<# + .SYNOPSIS + Creates, modifies, or removes an environment variable. + + .PARAMETER Name + The name of the environment variable to create, modify, or remove. + + .PARAMETER Value + The value to set the environment variable to. + If a value is not provided, the variable cannot be created. + If Ensure is set to Present, the variable does not already exist, and a value is not + specified, an error will be thrown indicating that the variable cannot be created without + a specified value. If Ensure is set to Present, the variable already exists, and no value + is specified, nothing will be changed. + + .PARAMETER Ensure + Specifies whether the variable should exist or not. + To ensure that the variable or value does exist, set this property to Present. + To ensure that the variable or value does not exist, set this property to Absent. + The default value is Present. + + .PARAMETER Path + Indicates whether or not this is a path variable. If this property is set to True, + the value provided through the Value property will be appended to (or removed from if + Ensure is set to Absent) the existing value. + If this property is set to False, the existing value will be replaced by the new Value. + The default value is False. + + .PARAMETER Target + Indicates where to set the environment variable: The machine, the process, or both. + The default is both: ('Process', 'Machine') +#> +function Set-TargetResource +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $Name, + + [Parameter()] + [ValidateNotNull()] + [System.String] + $Value = [System.String]::Empty, + + [Parameter()] + [ValidateSet('Present', 'Absent')] + [System.String] + $Ensure = 'Present', + + [Parameter()] + [System.Boolean] + $Path = $false, + + [Parameter()] + [ValidateSet('Process', 'Machine')] + [ValidateNotNullOrEmpty()] + [System.String[]] + $Target = ('Process', 'Machine') + ) + + $valueSpecified = ($Value -ne [System.String]::Empty) + $currentValueFromMachine = $null + $currentValueFromProcess = $null + $currentPropertiesFromMachine = $null + + $setMachineVariable = ($Target -contains 'Machine') + $setProcessVariable = ($Target -contains 'Process') + + if ($setMachineVariable) + { + if ($Path) + { + $currentPropertiesFromMachine = Get-EnvironmentVariableWithoutExpanding -Name $Name -ErrorAction 'SilentlyContinue' + + if ($null -ne $currentPropertiesFromMachine) + { + $currentValueFromMachine = $currentPropertiesFromMachine.$Name + } + } + else + { + $currentPropertiesFromMachine = Get-ItemProperty -Path $script:envVarRegPathMachine -Name $Name -ErrorAction 'SilentlyContinue' + $currentValueFromMachine = Get-EnvironmentVariable -Name $Name -Target 'Machine' + } + } + + if ($setProcessVariable) + { + $currentValueFromProcess = Get-EnvironmentVariable -Name $Name -Target 'Process' + } + + # A different value of the environment variable needs to be displayed depending on the Target + $currentValueToDisplay = '' + if ($setMachineVariable -and $setProcessVariable) + { + $currentValueToDisplay = "Machine: $currentValueFromMachine, Process: $currentValueFromProcess" + } + elseif ($setMachineVariable) + { + $currentValueToDisplay = $currentValueFromMachine + } + else + { + $currentValueToDisplay = $currentValueFromProcess + } + + if ($Ensure -eq 'Present') + { + $createMachineVariable = ((-not $setMachineVariable) -or ($null -eq $currentPropertiesFromMachine) -or ($currentValueFromMachine -eq [System.String]::Empty)) + $createProcessVariable = ((-not $setProcessVariable) -or ($null -eq $currentValueFromProcess) -or ($currentValueFromProcess -eq [System.String]::Empty)) + + if ($createMachineVariable -and $createProcessVariable) + { + if (-not $valueSpecified) + { + <# + If the environment variable doesn't exist and no value is passed in + then there is nothing to set - so throw an error. + #> + + New-InvalidOperationException -Message ($script:localizedData.CannotSetValueToEmpty -f $Name) + } + + <# + Given the specified $Name environment variable hasn't been created or set + simply create one with the specified value and return. + Both path and non-path cases are covered by this. + #> + + Set-EnvironmentVariable -Name $Name -Value $Value -Target $Target + + Write-Verbose -Message ($script:localizedData.EnvVarCreated -f $Name, $Value) + return + } + + if (-not $valueSpecified) + { + <# + Given no $Value was specified to be set and the variable exists, + we'll leave the existing variable as is. + This covers both path and non-path variables. + #> + + Write-Verbose -Message ($script:localizedData.EnvVarUnchanged -f $Name, $currentValueToDisplay) + return + } + + # Check if an empty, whitespace or semi-colon only string has been specified. If yes, return unchanged. + $trimmedValue = $Value.Trim(';').Trim() + + if ([System.String]::IsNullOrEmpty($trimmedValue)) + { + Write-Verbose -Message ($script:localizedData.EnvVarPathUnchanged -f $Name, $currentValueToDisplay) + return + } + + if (-not $Path) + { + # For non-path variables, simply set the specified $Value as the new value of the specified + # variable $Name for the given $Target + + if (($setMachineVariable -and ($Value -cne $currentValueFromMachine)) -or ` + ($setProcessVariable -and ($Value -cne $currentValueFromProcess))) + { + Set-EnvironmentVariable -Name $Name -Value $Value -Target $Target + Write-Verbose -Message ($script:localizedData.EnvVarUpdated -f $Name, $currentValueToDisplay, $Value) + } + else + { + Write-Verbose -Message ($script:localizedData.EnvVarUnchanged -f $Name, $currentValueToDisplay) + } + + return + } + + # If the control reaches here, the specified variable exists, it is a path variable, and a value has been specified to be set. + + if ($setMachineVariable) + { + $valueUnchanged = Test-PathsInValue -ExistingPaths $currentValueFromMachine -QueryPaths $trimmedValue -FindCriteria 'All' + + if ($currentValueFromMachine -and -not $valueUnchanged) + { + $updatedValue = Add-PathsToValue -CurrentValue $currentValueFromMachine -NewValue $trimmedValue + Set-EnvironmentVariable -Name $Name -Value $updatedValue -Target @('Machine') + Write-Verbose -Message ($script:localizedData.EnvVarPathUpdated -f $Name, $currentValueFromMachine, $updatedValue) + } + else + { + Write-Verbose -Message ($script:localizedData.EnvVarPathUnchanged -f $Name, $currentValueFromMachine) + } + } + + if ($setProcessVariable) + { + $valueUnchanged = Test-PathsInValue -ExistingPaths $currentValueFromProcess -QueryPaths $trimmedValue -FindCriteria 'All' + + if ($currentValueFromProcess -and -not $valueUnchanged) + { + $updatedValue = Add-PathsToValue -CurrentValue $currentValueFromProcess -NewValue $trimmedValue + Set-EnvironmentVariable -Name $Name -Value $updatedValue -Target @('Process') + Write-Verbose -Message ($script:localizedData.EnvVarPathUpdated -f $Name, $currentValueFromProcess, $updatedValue) + } + else + { + Write-Verbose -Message ($script:localizedData.EnvVarPathUnchanged -f $Name, $currentValueFromProcess) + } + } + } + + # Ensure = 'Absent' + else + { + $machineVariableRemoved = ((-not $setMachineVariable) -or ($null -eq $currentPropertiesFromMachine)) + $processVariableRemoved = ((-not $setProcessVariable) -or ($null -eq $currentValueFromProcess)) + + if ($machineVariableRemoved -and $processVariableRemoved) + { + # Variable not found, condition is satisfied and there is nothing to set/remove, return + Write-Verbose -Message ($script:localizedData.EnvVarNotFound -f $Name) + return + } + + if ((-not $ValueSpecified) -or (-not $Path)) + { + <# + If $Value is not specified or if $Value is a non-path variable, + simply remove the environment variable. + #> + + Remove-EnvironmentVariable -Name $Name -Target $Target + + Write-Verbose -Message ($script:localizedData.EnvVarRemoved -f $Name) + return + } + + # Check if an empty string or semi-colon only string has been specified as $Value. If yes, return unchanged as we don't need to remove anything. + $trimmedValue = $Value.Trim(';').Trim() + + if ([System.String]::IsNullOrEmpty($trimmedValue)) + { + Write-Verbose -Message ($script:localizedData.EnvVarPathUnchanged -f $Name, $currentValueToDisplay) + return + } + + # If the control reaches here: target variable is an existing environment path-variable and a specified $Value needs be removed from it + + if ($setMachineVariable) + { + $finalPath = $null + + if ($currentValueFromMachine) + { + <# + If this value returns $null or an empty string, than the entire path should be removed. + If it returns the same value as the path that was passed in, than nothing needs to be + updated, otherwise, only the specified paths were removed but there are still others + that need to be left in, so the path variable is updated to remove only the specified paths. + #> + $finalPath = Remove-PathsFromValue -CurrentValue $currentValueFromMachine -PathsToRemove $trimmedValue + } + + if ([System.String]::IsNullOrEmpty($finalPath)) + { + Remove-EnvironmentVariable -Name $Name -Target @('Machine') + Write-Verbose -Message ($script:localizedData.EnvVarRemoved -f $Name) + } + elseif ($finalPath -ceq $currentValueFromMachine) + { + Write-Verbose -Message ($script:localizedData.EnvVarPathUnchanged -f $Name, $currentValueFromMachine) + } + else + { + Set-EnvironmentVariable -Name $Name -Value $finalPath -Target @('Machine') + Write-Verbose -Message ($script:localizedData.EnvVarPathUpdated -f $Name, $currentValueFromMachine, $finalPath) + } + } + + if ($setProcessVariable) + { + $finalPath = $null + + if ($currentValueFromProcess) + { + <# + If this value returns $null or an empty string, than the entire path should be removed. + If it returns the same value as the path that was passed in, than nothing needs to be + updated, otherwise, only the specified paths were removed but there are still others + that need to be left in, so the path variable is updated to remove only the specified paths. + #> + $finalPath = Remove-PathsFromValue -CurrentValue $currentValueFromProcess -PathsToRemove $trimmedValue + } + + if ([System.String]::IsNullOrEmpty($finalPath)) + { + Remove-EnvironmentVariable -Name $Name -Target @('Process') + Write-Verbose -Message ($script:localizedData.EnvVarRemoved -f $Name) + } + elseif ($finalPath -ceq $currentValueFromProcess) + { + Write-Verbose -Message ($script:localizedData.EnvVarPathUnchanged -f $Name, $currentValueFromProcess) + } + else + { + Set-EnvironmentVariable -Name $Name -Value $finalPath -Target @('Process') + Write-Verbose -Message ($script:localizedData.EnvVarPathUpdated -f $Name, $currentValueFromProcess, $finalPath) + } + } + } +} + +<# + .SYNOPSIS + Tests if the environment variable is in the desired state. + + .PARAMETER Name + The name of the environment variable to test. + + .PARAMETER Value + The value of the environment variable to test. If no value is specified then only the + existence of the variable will be checked. + + .PARAMETER Ensure + Specifies whether the variable should exist or not. + To test that the variable does exist, set this property to Present. + To test that the variable does not exist, set this property to Absent. + The default value is Present. + + .PARAMETER Path + Indicates whether or not this is a path variable. If this property is set to True, + the value(s) provided through the Value property will be checked against all existing + values already set in this variable. + If this property is set to False, the value will be compared directly to the existing value. + The default value is False. + + .PARAMETER Target + Indicates where to test the environment variable: The machine, the process, or both. + The default is both: ('Process', 'Machine') +#> +function Test-TargetResource +{ + [CmdletBinding()] + [OutputType([System.Boolean])] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $Name, + + [Parameter()] + [ValidateNotNull()] + [System.String] + $Value, + + [Parameter()] + [ValidateSet('Present', 'Absent')] + [System.String] + $Ensure = 'Present', + + [Parameter()] + [System.Boolean] + $Path = $false, + + [Parameter()] + [ValidateSet('Process', 'Machine')] + [ValidateNotNullOrEmpty()] + [System.String[]] + $Target = ('Process', 'Machine') + ) + + $valueSpecified = $PSBoundParameters.ContainsKey('Value') -and ($Value -ne [System.String]::Empty) + $currentValueFromMachine = $null + $currentValueFromProcess = $null + $currentPropertiesFromMachine = $null + + $checkMachineTarget = ($Target -contains 'Machine') + $checkProcessTarget = ($Target -contains 'Process') + + if ($checkMachineTarget) + { + if ($Path) + { + $currentPropertiesFromMachine = Get-EnvironmentVariableWithoutExpanding -Name $Name -ErrorAction 'SilentlyContinue' + + if ($null -ne $currentPropertiesFromMachine) + { + $currentValueFromMachine = $currentPropertiesFromMachine.$Name + } + } + else + { + $currentPropertiesFromMachine = Get-ItemProperty -Path $script:envVarRegPathMachine -Name $Name -ErrorAction 'SilentlyContinue' + $currentValueFromMachine = Get-EnvironmentVariable -Name $Name -Target 'Machine' + } + } + + if ($checkProcessTarget) + { + $currentValueFromProcess = Get-EnvironmentVariable -Name $Name -Target 'Process' + } + + # A different value of the environment variable needs to be displayed depending on the Target + $currentValueToDisplay = '' + if ($checkMachineTarget -and $checkProcessTarget) + { + $currentValueToDisplay = "Machine: $currentValueFromMachine, Process: $currentValueFromProcess" + } + elseif ($checkMachineTarget) + { + $currentValueToDisplay = $currentValueFromMachine + } + else + { + $currentValueToDisplay = $currentValueFromProcess + } + + if (($checkMachineTarget -and ($null -eq $currentPropertiesFromMachine)) -or ($checkProcessTarget -and ($null -eq $currentValueFromProcess))) + { + # Variable not found + Write-Verbose ($script:localizedData.EnvVarNotFound -f $Name) + return ($Ensure -eq 'Absent') + } + + if (-not $valueSpecified) + { + Write-Verbose ($script:localizedData.EnvVarFound -f $Name, $currentValueToDisplay) + return ($Ensure -eq 'Present') + } + + if (-not $Path) + { + # For this non-path variable, make sure that the specified $Value matches the current value. + + if (($checkMachineTarget -and ($Value -cne $currentValueFromMachine)) -or ` + ($checkProcessTarget -and ($Value -cne $currentValueFromProcess))) + { + Write-Verbose ($script:localizedData.EnvVarFoundWithMisMatchingValue -f $Name, $currentValueToDisplay, $Value) + return ($Ensure -eq 'Absent') + } + else + { + Write-Verbose ($script:localizedData.EnvVarFound -f $Name, $currentValueToDisplay) + return ($Ensure -eq 'Present') + } + } + + # If the control reaches here, the expected environment variable exists, it is a path variable and a $Value is specified to test against + if ($Ensure -eq 'Present') + { + if ($checkMachineTarget) + { + if (-not (Test-PathsInValue -ExistingPaths $currentValueFromMachine -QueryPaths $Value -FindCriteria 'All')) + { + # If the control reached here some part of the specified path ($Value) was not found in the existing variable, return failure + Write-Verbose ($script:localizedData.EnvVarFoundWithMisMatchingValue -f $Name, $currentValueToDisplay, $Value) + return $false + } + } + + if ($checkProcessTarget) + { + if (-not (Test-PathsInValue -ExistingPaths $currentValueFromProcess -QueryPaths $Value -FindCriteria 'All')) + { + # If the control reached here some part of the specified path ($Value) was not found in the existing variable, return failure + Write-Verbose ($script:localizedData.EnvVarFoundWithMisMatchingValue -f $Name, $currentValueToDisplay, $Value) + return $false + } + } + + # The specified path was completely present in the existing environment variable, return success + Write-Verbose ($script:localizedData.EnvVarFound -f $Name, $currentValueToDisplay) + return $true + } + # Ensure = 'Absent' + else + { + if ($checkMachineTarget) + { + if (Test-PathsInValue -ExistingPaths $currentValueFromMachine -QueryPaths $Value -FindCriteria 'Any') + { + # One of the specified paths in $Value exists in the environment variable path, thus the test fails + Write-Verbose ($script:localizedData.EnvVarFound -f $Name, $currentValueFromMachine) + return $false + } + } + + if ($checkProcessTarget) + { + if (Test-PathsInValue -ExistingPaths $currentValueFromProcess -QueryPaths $Value -FindCriteria 'Any') + { + # One of the specified paths in $Value exists in the environment variable path, thus the test fails + Write-Verbose ($script:localizedData.EnvVarFound -f $Name, $currentValueFromProcess) + return $false + } + } + + # If the control reached here, none of the specified paths were found in the existing path-variable, return success + Write-Verbose ($script:localizedData.EnvVarFoundWithMisMatchingValue -f $Name, $currentValueToDisplay, $Value) + return $true + } +} + +<# + .SYNOPSIS + Retrieves the value of the environment variable from the given Target. + + .PARAMETER Name + The name of the environment variable to retrieve the value from. + + .PARAMETER Target + Indicates where to retrieve the environment variable from. Currently, only + Process and Machine are being used, but User is included for future extension + of this resource. +#> +function Get-EnvironmentVariable +{ + [CmdletBinding()] + [OutputType([System.String])] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $Name, + + [Parameter(Mandatory = $true)] + [ValidateSet('Process', 'Machine')] + [System.String] + $Target + ) + + $valueToReturn = $null + + if ($Target -eq 'Process') + { + $valueToReturn = Get-ProcessEnvironmentVariable -Name $Name + } + elseif ($Target -eq 'Machine') + { + $retrievedProperty = Get-ItemProperty -Path $script:envVarRegPathMachine -Name $Name -ErrorAction 'SilentlyContinue' + + if ($null -ne $retrievedProperty) + { + $valueToReturn = $retrievedProperty.$Name + } + } + elseif ($Target -eq 'User') + { + $retrievedProperty = Get-ItemProperty -Path $script:envVarRegPathUser -Name $Name -ErrorAction 'SilentlyContinue' + + if ($null -ne $retrievedProperty) + { + $valueToReturn = $retrievedProperty.$Name + } + } + + return $valueToReturn +} + +<# + .SYNOPSIS + Wrapper function to retrieve an environment variable from the current process. + + .PARAMETER Name + The name of the variable to retrieve +#> +function Get-ProcessEnvironmentVariable +{ + [CmdletBinding()] + [OutputType([System.String])] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $Name + ) + + return [System.Environment]::GetEnvironmentVariable($Name) +} + +<# + .SYNOPSIS + If there are any paths in NewPaths that aren't in CurrentValue they will be added + to the current paths value and a String will be returned containing all old paths + and new paths. Otherwise the original value will be returned unchanged. + + .PARAMETER CurrentValue + A semicolon-separated String containing the current path values. + + .PARAMETER NewPaths + A semicolon-separated String containing any paths that should be added to + the current value. If CurrentValue already contains a path, it will not be added. +#> +function Add-PathsToValue +{ + [CmdletBinding()] + [OutputType([System.String])] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $CurrentValue, + + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $NewValue + ) + + $finalValue = $CurrentValue + ';' + $currentPaths = $CurrentValue -split ';' + $newPaths = $NewValue -split ';' + + foreach ($path in $newPaths) + { + if ($currentPaths -notcontains $path) + { + <# + If the control reached here, we didn't find this $specifiedPath in the $currentPaths, + so add it. + #> + + $finalValue += ($path + ';') + } + } + + # Remove any extraneous ';' at the end (and potentially start - as a side-effect) of the value to be set + return $finalValue.Trim(';') +} + +<# + .SYNOPSIS + If there are any paths in PathsToRemove that aren't in CurrentValue they will be removed + from the current paths value and either the new value will be returned if there are still + paths that remain, or an empty string will be returned if all paths were removed. + If none of the paths in PathsToRemove are in CurrentValue then this function will + return CurrentValue since nothing needs to be changed. + + .PARAMETER CurrentValue + A semicolon-separated String containing the current path values. + + .PARAMETER PathsToRemove + A semicolon-separated String containing any paths that should be removed from + the current value. +#> +function Remove-PathsFromValue +{ + [OutputType([System.String])] + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $CurrentValue, + + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $PathsToRemove + ) + + $finalPath = '' + $specifiedPaths = $PathsToRemove -split ';' + $currentPaths = $CurrentValue -split ';' + $varAltered = $false + + foreach ($subpath in $currentPaths) + { + if ($specifiedPaths -contains $subpath) + { + <# + Found this $subpath as one of the $specifiedPaths, skip adding this to the final + value/path of this variable and mark the variable as altered. + #> + $varAltered = $true + } + else + { + # the current $subpath was not part of the $specifiedPaths (to be removed) so keep this $subpath in the finalPath + $finalPath += $subpath + ';' + } + } + + # Remove any extraneous ';' at the end (and potentially start - as a side-effect) of the $finalPath + $finalPath = $finalPath.Trim(';') + + if ($varAltered) + { + return $finalPath + } + else + { + return $CurrentValue + } +} + +<# + .SYNOPSIS + Sets the value of the environment variable with the given name if a value is specified. + If no value is specified, then the environment variable will be removed. + + .PARAMETER Name + The name of the environment variable to set or remove. + + .PARAMETER Value + The value to set the environment variable to. If not provided, then the variable will + be removed. + + .PARAMETER Target + Indicates where to set or remove the environment variable: The machine, the process, or both. + The logic for User is also included here for future expansion of this resource. +#> +function Set-EnvironmentVariable +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $Name, + + [Parameter()] + [System.String] + $Value, + + [Parameter(Mandatory = $true)] + [ValidateSet('Process', 'Machine')] + [System.String[]] + $Target + ) + + $valueSpecified = $PSBoundParameters.ContainsKey('Value') + + try + { + # If the Value is set to [System.String]::Empty then nothing should be updated for the process + if (($Target -contains 'Process') -and (-not $valueSpecified -or ($Value -ne [System.String]::Empty))) + { + if (-not $valueSpecified) + { + Set-ProcessEnvironmentVariable -Name $Name -Value $null + } + else + { + Set-ProcessEnvironmentVariable -Name $Name -Value $Value + } + } + + if ($Target -contains 'Machine') + { + if ($Name.Length -ge $script:maxSystemEnvVariableLength) + { + New-InvalidArgumentException -Message $script:localizedData.ArgumentTooLong -ArgumentName $Name + } + + $path = $script:envVarRegPathMachine + + if (-not $valueSpecified) + { + $environmentKey = Get-ItemProperty -Path $path -Name $Name -ErrorAction 'SilentlyContinue' + + if ($environmentKey) + { + Remove-ItemProperty -Path $path -Name $Name + } + else + { + $message = ($script:localizedData.RemoveNonExistentVarError -f $Name) + New-InvalidArgumentException -Message $message -ArgumentName $Name + } + } + else + { + Set-ItemProperty -Path $path -Name $Name -Value $Value + $environmentKey = Get-ItemProperty -Path $path -Name $Name -ErrorAction 'SilentlyContinue' + + if ($null -eq $environmentKey) + { + $message = ($script:localizedData.GetItemPropertyFailure -f $Name, $path) + New-InvalidArgumentException -Message $message -ArgumentName $Name + } + } + } + + # The User feature of this resource is not yet implemented. + if ($Target -contains 'User') + { + if ($Name.Length -ge $script:maxUserEnvVariableLength) + { + New-InvalidArgumentException -Message $script:localizedData.ArgumentTooLong -ArgumentName $Name + } + + $path = $script:envVarRegPathUser + + if (-not $valueSpecified) + { + $environmentKey = Get-ItemProperty -Path $path -Name $Name -ErrorAction 'SilentlyContinue' + + if ($environmentKey) + { + Remove-ItemProperty -Path $path -Name $Name + } + else + { + $message = ($script:localizedData.RemoveNonExistentVarError -f $Name) + New-InvalidArgumentException -Message $message -ArgumentName $Name + } + } + else + { + Set-ItemProperty -Path $path -Name $Name -Value $Value + $environmentKey = Get-ItemProperty -Path $path -Name $Name -ErrorAction 'SilentlyContinue' + + if ($null -eq $environmentKey) + { + $message = ($script:localizedData.GetItemPropertyFailure -f $Name, $path) + New-InvalidArgumentException -Message $message -ArgumentName $Name + } + } + } + } + catch + { + New-InvalidOperationException -Message ($script:localizedData.EnvVarSetError -f $Name, $Value) ` + -ErrorRecord $_ + } + +} + +<# + .SYNOPSIS + Wrapper function to set an environment variable for the current process. + + .PARAMETER Name + The name of the environment variable to set. + + .PARAMETER Value + The value to set the environment variable to. +#> +function Set-ProcessEnvironmentVariable +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $Name, + + [Parameter()] + [System.String] + $Value = [System.String]::Empty + ) + + [System.Environment]::SetEnvironmentVariable($Name, $Value) +} + +<# + .SYNOPSIS + Removes an environment variable from the given target(s) by calling Set-EnvironmentVariable + with no Value specified. + + .PARAMETER Name + The name of the environment variable to remove. + + .PARAMETER Target + Indicates where to remove the environment variable from: The machine, the process, or both. +#> +function Remove-EnvironmentVariable +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $Name, + + [Parameter(Mandatory = $true)] + [ValidateSet('Process', 'Machine')] + [System.String[]] + $Target + ) + + try + { + Set-EnvironmentVariable -Name $Name -Target $Target + } + catch + { + New-InvalidOperationException -Message ($script:localizedData.EnvVarRemoveError -f $Name) ` + -ErrorRecord $_ + } +} + +<# + .SYNOPSIS + Tests all of the paths in QueryPaths against those in ExistingPaths. + If FindCriteria is set to 'All' then it will only return True if all of the + paths in QueryPaths are in ExistingPaths, otherwise it will return False. + If FindCriteria is set to 'Any' then it will return True if any of the paths + in QueryPaths are in ExistingPaths, otherwise it will return False. + + .PARAMETER ExistingPaths + A semicolon-separated String containing the path values to test against. + + .PARAMETER QueryPaths + A semicolon-separated String containing the path values to ensure are either + included or not included in ExistingPaths. + + .PARAMETER FindCriteria + Set to either 'All' or 'Any' to indicate whether all of the paths in QueryPaths + should be included in ExistingPaths or any of them. +#> +function Test-PathsInValue +{ + [OutputType([System.Boolean])] + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $ExistingPaths, + + [Parameter(Mandatory = $true)] + [System.String] + $QueryPaths, + + [Parameter(Mandatory = $true)] + [ValidateSet('Any', 'All')] + [System.String] + $FindCriteria + ) + + $existingPathList = $ExistingPaths -split ';' + $queryPathList = $QueryPaths -split ';' + + switch ($FindCriteria) + { + 'Any' + { + foreach ($queryPath in $queryPathList) + { + if ($existingPathList -contains $queryPath) + { + # Found this $queryPath in the existing paths, return $true + return $true + } + } + + # If the control reached here, none of the QueryPaths were found in ExistingPaths + return $false + } + + 'All' + { + foreach ($queryPath in $queryPathList) + { + if ($queryPath) + { + if ($existingPathList -notcontains $queryPath) + { + # The current $queryPath wasn't found in any of the $existingPathList, return false + return $false + } + } + } + + # If the control reached here, all of the QueryPaths were found in ExistingPaths + return $true + } + } +} + +<# + .SYNOPSIS + Retrieves the Environment variable with the given name from the registry on the machine. + It returns the result as an object containing a Hashtable with the environment variable + name and its current value on the machine. This is to most closely represent what the + actual API call returns. If an environment variable with the given name is not found, then + $null will be returned. + + .PARAMETER Name + The name of the environment variable to retrieve the value of. +#> +function Get-EnvironmentVariableWithoutExpanding +{ + [OutputType([System.Management.Automation.PSObject])] + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNull()] + [System.String] + $Name + ) + + $path = $script:envVarRegPathMachine + $pathTokens = $path.Split('\',[System.StringSplitOptions]::RemoveEmptyEntries) + $entry = $pathTokens[1..($pathTokens.Count - 1)] -join '\' + + # Since the target registry path coming to this function is hardcoded for local machine + $hive = [Microsoft.Win32.Registry]::LocalMachine + + $noteProperties = @{} + + try + { + $key = $hive.OpenSubKey($entry) + + $valueNames = $key.GetValueNames() + if ($valueNames -inotcontains $Name) + { + return $null + } + + [System.String] $value = Get-KeyValue -Name $Name -Key $key + $noteProperties.Add($Name, $value) + } + finally + { + if ($key) + { + $key.Close() + } + } + + [System.Management.Automation.PSObject] $propertyResults = New-Object -TypeName System.Management.Automation.PSObject -Property $noteProperties + + return $propertyResults +} + +<# + .SYNOPSIS + Wrapper function to get the value of the environment variable with the given name + from the specified registry key. + + .PARAMETER Name + The name of the environment variable to retrieve the value of. + + .PARAMETER Key + The key to retrieve the environment variable from. +#> +function Get-KeyValue +{ + [OutputType([System.String])] + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNull()] + [System.String] + $Name, + + [Parameter(Mandatory = $true)] + [ValidateNotNull()] + [Microsoft.Win32.RegistryKey] + $Key + ) + + return $Key.GetValue($Name, $null, [Microsoft.Win32.RegistryValueOptions]::DoNotExpandEnvironmentNames) +} diff --git a/deployment/dsc/azshcihost/xPSDesiredStateConfiguration/9.1.0/DSCResources/DSC_xEnvironmentResource/DSC_xEnvironmentResource.schema.mof b/deployment/dsc/azshcihost/xPSDesiredStateConfiguration/9.1.0/DSCResources/DSC_xEnvironmentResource/DSC_xEnvironmentResource.schema.mof new file mode 100644 index 0000000..dc3c093 --- /dev/null +++ b/deployment/dsc/azshcihost/xPSDesiredStateConfiguration/9.1.0/DSCResources/DSC_xEnvironmentResource/DSC_xEnvironmentResource.schema.mof @@ -0,0 +1,9 @@ +[ClassVersion("1.0.0"), FriendlyName("xEnvironment")] +class DSC_xEnvironmentResource : OMI_BaseResource +{ + [Key, Description("The name of the environment variable for which you want to ensure a specific state.")] String Name; + [Write, Description("The desired value for the environment variable.")] String Value; + [Write, Description("Specifies if the environment variable should exist."), ValueMap{"Present", "Absent"}, Values{"Present", "Absent"}] String Ensure; + [Write, Description("Indicates whether or not the environment variable is the Path variable.")] Boolean Path; + [Write, Description("Indicates the target where the environment variable should be set."), ValueMap{"Process", "Machine"}, Values{"Process", "Machine"}] String Target[]; +}; diff --git a/deployment/dsc/azshcihost/xPSDesiredStateConfiguration/9.1.0/DSCResources/DSC_xEnvironmentResource/en-US/DSC_xEnvironmentResource.schema.mfl b/deployment/dsc/azshcihost/xPSDesiredStateConfiguration/9.1.0/DSCResources/DSC_xEnvironmentResource/en-US/DSC_xEnvironmentResource.schema.mfl new file mode 100644 index 0000000..d6dff21 --- /dev/null +++ b/deployment/dsc/azshcihost/xPSDesiredStateConfiguration/9.1.0/DSCResources/DSC_xEnvironmentResource/en-US/DSC_xEnvironmentResource.schema.mfl @@ -0,0 +1,9 @@ +[Description("The xEnvironment resource provides a mechanism to manage machine-wide environment variables.") : Amended,AMENDMENT, LOCALE("MS_409")] +class DSC_xEnvironmentResource : OMI_BaseResource +{ + [Key, Description("The name of the environment variable for which you want to ensure a specific state.") : Amended] String Name; + [Description("The desired value for the environment variable.") : Amended] String Value; + [Description("Specifies if the environment varaible should exist.") : Amended] String Ensure; + [Description("Indicates whether or not the environment variable is the Path variable.") : Amended] Boolean Path; + [Description("Indicates the target where the environment variable should be set.") : Amended] String Target[]; +}; diff --git a/deployment/dsc/azshcihost/xPSDesiredStateConfiguration/9.1.0/DSCResources/DSC_xEnvironmentResource/en-US/DSC_xEnvironmentResource.strings.psd1 b/deployment/dsc/azshcihost/xPSDesiredStateConfiguration/9.1.0/DSCResources/DSC_xEnvironmentResource/en-US/DSC_xEnvironmentResource.strings.psd1 new file mode 100644 index 0000000..13b7bf3 --- /dev/null +++ b/deployment/dsc/azshcihost/xPSDesiredStateConfiguration/9.1.0/DSCResources/DSC_xEnvironmentResource/en-US/DSC_xEnvironmentResource.strings.psd1 @@ -0,0 +1,19 @@ +# Localized resources for DSC_xEnvironmentResource + +ConvertFrom-StringData @' + ArgumentTooLong = Argument is too long. + CannotSetValueToEmpty = Cannot create environment variable with an empty value. Set Ensure = Absent to remove environment variable '{0}'. + EnvVarCreated = Environment variable '{0}' created with value '{1}'. + EnvVarSetError = Failed to set environment variable '{0}' to value '{1}'. + EnvVarRemoveError = Failed to remove environment variable '{0}' holding value '{1}'. + EnvVarUnchanged = Environment variable '{0}' with value '{1}' was not updated. + EnvVarUpdated = Environment variable '{0}' updated from value '{1}' to value '{2}'. + EnvVarPathUnchanged = Path environment variable '{0}' with value '{1}' was not updated. + EnvVarPathUpdated = Environment variable '{0}' updated from value '{1}' to value '{2}'. + EnvVarNotFound = Environment variable '{0}' does not exist. + EnvVarFound = Environment variable '{0}' with value '{1}' was successfully found. + EnvVarFoundWithMisMatchingValue = Environment variable '{0}' with value '{1}' mismatched the specified value '{2}'. + EnvVarRemoved = Environment variable '{0}' removed. + GetItemPropertyFailure = Failed to get the item property for variable '{0}' with path '{1}'. + RemoveNonExistentVarError = Environment variable '{0}' cannot be removed because it does not exist. +'@ diff --git a/deployment/dsc/azshcihost/xPSDesiredStateConfiguration/9.1.0/DSCResources/DSC_xGroupResource/DSC_xGroupResource.psm1 b/deployment/dsc/azshcihost/xPSDesiredStateConfiguration/9.1.0/DSCResources/DSC_xGroupResource/DSC_xGroupResource.psm1 new file mode 100644 index 0000000..c4c0e06 --- /dev/null +++ b/deployment/dsc/azshcihost/xPSDesiredStateConfiguration/9.1.0/DSCResources/DSC_xGroupResource/DSC_xGroupResource.psm1 @@ -0,0 +1,2620 @@ +<# + Implementatation Notes + + Managing Disposable Objects + The types PrincipalContext, Principal, and DirectoryEntry are used througout the code and + all are disposable. However, in many cases, disposing the object immediately causes + subsequent operations to fail or duplicate disposes calls to occur. + + To simplify management of these disposables, each public entry point defines a $disposables + ArrayList variable and passes it to secondary functions that may need to create disposable + objects. The public entry point is then required to dispose the contents of the list in a + finally block. + + Managing PrincipalContext Instances + To use the AccountManagement APIs to connect to the local machine or a domain, a + PrincipalContext is needed. + + For the local groups and users, a PrincipalContext reflecting the current user can be + created. + + For the default domain, the domain where the machine is joined, explicit credentials are + needed since the default user context is SYSTEM which has no rights to the domain. + + Additional PrincipalContext instances may be needed when the machine is in a domain that is + part of a multi-domain forest. For example, Microsoft uses a multi-domain forest that + includes domains such as ntdev, redmond, wingroup and a group may have members that + span multiple domains. Unless the enterprise implements the Global Catalog, + something that Microsoft does not do, a unique PrincipalContext is needed to resolve + accounts in each of the domains. + + To manage the use of PrincipalContext across domains, public entry points define a + $principalContextCache hashtable and pass it to support functions that need to resolve a group + or group member. Consumers of a PrincipalContext call Get-PrincipalContext with a scope + (domain name or machine name). Get-PrincipalContext returns an existing hashtable entry or + creates a new entry. Note that a PrincipalContext to a target domain requires connecting + to the domain. The hashtable avoids subsequent connection calls. Also note that + Get-PrincipalContext takes a Credential parameter for the case where a new PrincipalContext + is needed. The implicit assumption is that the credential provided for the primary domain + also has rights to resolve accounts in any of the other domains. + + Resolving Group Members + The original implementation assumed that group members could be resolved using the machine + PrincipalContext or the logged on user. In practice this is not reliable since the resource + is typically run under the SYSTEM account and this account is not guaranteed to have rights + to resolve domain accounts. Additionally, the APIs for enumerating group members do not + provide a facility for passing additional credentials resulting in domain members failing + to resolve. + + To address this, group members are enumerated by first converting the GroupPrincipal to a + DirectoryEntry and enumerating its child members. The returned DirectoryEntry instances are + then resolved to Principal objects using a PrincipalContext appropriate for the target + domain. + + Handling Stale Group Members + A group may have stale members if the machine was moved from one domain to a another + foreign domain or when accounts are deleted (domain or local). At this point, members that + were defined in the original domain or were deleted are now stale and cannot be resolved + using Principal::FindByIdentity. The original implementation failed at this point + preventing any operations against the group. The current implementation calls Write-Warning + with the associated SID of the member that cannot be resolved then continues the operation. +#> + +$errorActionPreference = 'Stop' +Set-StrictMode -Version 'Latest' + +$modulePath = Join-Path -Path (Split-Path -Path (Split-Path -Path $PSScriptRoot -Parent) -Parent) -ChildPath 'Modules' + +# Import the shared modules +Import-Module -Name (Join-Path -Path $modulePath ` + -ChildPath (Join-Path -Path 'xPSDesiredStateConfiguration.Common' ` + -ChildPath 'xPSDesiredStateConfiguration.Common.psm1')) + +# Import Localization Strings +$script:localizedData = Get-LocalizedData -ResourceName 'DSC_xGroupResource' + +if (-not (Test-IsNanoServer)) +{ + Add-Type -AssemblyName 'System.DirectoryServices.AccountManagement' +} + +<# + .SYNOPSIS + Retrieves the current state of the group with the specified name. + + .PARAMETER GroupName + The name of the group to retrieve the current state of. + + .PARAMETER Credential + A credential to resolve non-local group members. +#> +function Get-TargetResource +{ + [OutputType([System.Collections.Hashtable])] + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $GroupName, + + [Parameter()] + [System.Management.Automation.PSCredential] + [System.Management.Automation.Credential()] + $Credential + ) + + Assert-GroupNameValid -GroupName $GroupName + + if (Test-IsNanoServer) + { + Write-Verbose -Message ($script:localizedData.InvokingFunctionForGroup -f 'Get-TargetResourceOnNanoServer', $GroupName) + return Get-TargetResourceOnNanoServer @PSBoundParameters + } + else + { + Write-Verbose -Message ($script:localizedData.InvokingFunctionForGroup -f 'Get-TargetResourceOnFullSKU', $GroupName) + return Get-TargetResourceOnFullSKU @PSBoundParameters + } +} + +<# + .SYNOPSIS + Creates, modifies, or removes a group. + + .PARAMETER GroupName + The name of the group to create, modify, or remove. + + .PARAMETER Ensure + Specifies whether the group should exist or not. + + To ensure that the group does exist, set this property to present. + To ensure that the group does not exist, set this property to Absent. + + The default value is Present. + + .PARAMETER Description + The description the group should have. + + .PARAMETER Members + The members the group should have. + + This property will replace all the current group members with the specified members. + + Members should be specified as strings in the format of their domain qualified name + (domain\username), their UPN (username@domainname), their distinguished name (CN=username,DC=...), + or their username (for local machine accounts). + + Using either the MembersToExclude or MembersToInclude properties in the same configuration + as this property will generate an error. + + .PARAMETER MembersToInclude + The members the group should include. + + This property will only add members to a group. + + Members should be specified as strings in the format of their domain qualified name + (domain\username), their UPN (username@domainname), their distinguished name (CN=username,DC=...), + or their username (for local machine accounts). + + Using the Members property in the same configuration as this property will generate an error. + + .PARAMETER MembersToExclude + The members the group should exclude. + + This property will only remove members from a group. + + Members should be specified as strings in the format of their domain qualified name + (domain\username), their UPN (username@domainname), their distinguished name (CN=username,DC=...), + or their username (for local machine accounts). + + Using the Members property in the same configuration as this property will generate an error. + + .PARAMETER Credential + A credential to resolve and add non-local group members. + + An error will occur if this account does not have the appropriate Active Directory permissions to add all + non-local accounts to the group. + + .NOTES + ShouldProcess PSSA rule is suppressed because Set-TargetResourceOnFullSKU and + Set-TargetResourceOnNanoServer call ShouldProcess. +#> +function Set-TargetResource +{ + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSShouldProcess', '')] + [CmdletBinding(SupportsShouldProcess = $true)] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $GroupName, + + [Parameter()] + [ValidateSet('Present', 'Absent')] + [System.String] + $Ensure = 'Present', + + [Parameter()] + [System.String] + $Description, + + [Parameter()] + [System.String[]] + $Members, + + [Parameter()] + [System.String[]] + $MembersToInclude, + + [Parameter()] + [System.String[]] + $MembersToExclude, + + [Parameter()] + [ValidateNotNullOrEmpty()] + [System.Management.Automation.PSCredential] + [System.Management.Automation.Credential()] + $Credential + ) + + Write-Verbose ($script:localizedData.SetTargetResourceStartMessage -f $GroupName) + + Assert-GroupNameValid -GroupName $GroupName + + if (Test-IsNanoServer) + { + Set-TargetResourceOnNanoServer @PSBoundParameters + } + else + { + Set-TargetResourceOnFullSKU @PSBoundParameters + } + + Write-Verbose ($script:localizedData.SetTargetResourceEndMessage -f $GroupName) +} + +<# + .SYNOPSIS + Tests if the group with the specified name is in the desired state. + + .PARAMETER GroupName + The name of the group to test the state of. + + .PARAMETER Ensure + Indicates if the group should exist or not. + + Set this property to "Absent" to test that the group does not exist. + Setting it to "Present" (the default value) tests that the group exists. + + .PARAMETER Description + The description of the group to test for. + + .PARAMETER Members + The list of members the group should have. + + The value of this property is an array of strings of the formats domain qualified name + (domain\username), UPN (username@domainname), distinguished name (CN=username,DC=...) and/or + a unqualified (username) for local machine accounts. + + If you set this property in a configuration, do not use either the MembersToExclude or + MembersToInclude property. Doing so will generate an error. + + .PARAMETER MembersToInclude + A list of members that should be in the group. + + The value of this property is an array of strings of the formats domain qualified name + (domain\username), UPN (username@domainname), distinguished name (CN=username,DC=...) and/or + a unqualified (username) for local machine accounts. + + If you set this property in a configuration, do not use the Members property. + Doing so will generate an error. + + .PARAMETER MembersToExclude + A list of members that should not be in the group. + + The value of this property is an array of strings of the formats domain qualified name + (domain\username), UPN (username@domainname), distinguished name (CN=username,DC=...) and/or + a unqualified (username) for local machine accounts. + + If you set this property in a configuration, do not use the Members property. + Doing so will generate an error. + + .PARAMETER Credential + The credentials required to resolve non-local group members +#> +function Test-TargetResource +{ + [OutputType([System.Boolean])] + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $GroupName, + + [Parameter()] + [ValidateSet('Present', 'Absent')] + [System.String] + $Ensure = 'Present', + + [Parameter()] + [System.String] + $Description, + + [Parameter()] + [System.String[]] + $Members, + + [Parameter()] + [System.String[]] + $MembersToInclude, + + [Parameter()] + [System.String[]] + $MembersToExclude, + + [Parameter()] + [ValidateNotNullOrEmpty()] + [System.Management.Automation.PSCredential] + [System.Management.Automation.Credential()] + $Credential + ) + + Assert-GroupNameValid -GroupName $GroupName + + if (Test-IsNanoServer) + { + Write-Verbose ($script:localizedData.InvokingFunctionForGroup -f 'Test-TargetResourceOnNanoServer', $GroupName) + return Test-TargetResourceOnNanoServer @PSBoundParameters + } + else + { + Write-Verbose ($script:localizedData.InvokingFunctionForGroup -f 'Test-TargetResourceOnFullSKU', $GroupName) + return Test-TargetResourceOnFullSKU @PSBoundParameters + } +} + +<# + .SYNOPSIS + Retrieves the current state of the group with the specified name on a full server. + + .PARAMETER GroupName + The name of the group to retrieve the current state of. + + .PARAMETER Credential + A credential to resolve non-local group members. +#> +function Get-TargetResourceOnFullSKU +{ + [OutputType([System.Collections.Hashtable])] + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $GroupName, + + [Parameter()] + [System.Management.Automation.PSCredential] + [System.Management.Automation.Credential()] + $Credential + ) + + $principalContextCache = @{} + $disposables = New-Object -TypeName 'System.Collections.ArrayList' + + try + { + $principalContext = Get-PrincipalContext ` + -PrincipalContextCache $principalContextCache ` + -Disposables $Disposables ` + -Scope $env:COMPUTERNAME + + $group = Get-Group -GroupName $GroupName -PrincipalContext $principalContext + + if ($null -ne $group) + { + $null = $disposables.Add($group) + + # The group was found. Find the group members. + $members = Get-MembersOnFullSKU -Group $group -PrincipalContextCache $principalContextCache ` + -Credential $Credential -Disposables $disposables + + return @{ + GroupName = $group.Name + Ensure = 'Present' + Description = $group.Description + Members = $members + } + } + else + { + # The group was not found. + return @{ + GroupName = $GroupName + Ensure = 'Absent' + } + } + } + finally + { + Remove-DisposableObject -Disposables $disposables + } +} + +<# + .SYNOPSIS + Retrieves the current state of the group with the specified name on Nano Server. + + .PARAMETER GroupName + The name of the group to retrieve the current state of. + + .PARAMETER Credential + A credential to resolve non-local group members. +#> +function Get-TargetResourceOnNanoServer +{ + [OutputType([System.Collections.Hashtable])] + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $GroupName, + + [Parameter()] + [System.Management.Automation.PSCredential] + [System.Management.Automation.Credential()] + $Credential + ) + + try + { + $group = Get-LocalGroup -Name $GroupName -ErrorAction 'Stop' + } + catch + { + if ($_.CategoryInfo.Reason -eq 'GroupNotFoundException') + { + # The group was not found. + return @{ + GroupName = $GroupName + Ensure = 'Absent' + } + } + + New-InvalidOperationException -ErrorRecord $_ + } + + # The group was found. Find the group members. + $members = Get-MembersOnNanoServer -Group $group + + return @{ + GroupName = $group.Name + Ensure = 'Present' + Description = $group.Description + Members = $members + } +} + +<# + .SYNOPSIS + The Set-TargetResource cmdlet on a full server. + + .PARAMETER GroupName + The name of the group for which you want to ensure a specific state. + + .PARAMETER Ensure + Indicates if the group should exist or not. + + Set this property to Present to ensure that the group exists. + Set this property to Absent to ensure that the group does not exist. + + The default value is Present. + + .PARAMETER Description + The description of the group. + + .PARAMETER Members + Use this property to replace the current group membership with the specified members. + + The value of this property is an array of strings of the formats domain qualified name + (domain\username), UPN (username@domainname), distinguished name (CN=username,DC=...) and/or + an unqualified (username) for local machine accounts. + + If you set this property in a configuration, do not use either the MembersToExclude or + MembersToInclude property. Doing so will generate an error. + + .PARAMETER MembersToInclude + Use this property to add members to the existing membership of the group. + + The value of this property is an array of strings of the formats domain qualified name + (domain\username), UPN (username@domainname), distinguished name (CN=username,DC=...) and/or + a unqualified (username) for local machine accounts. + + If you set this property in a configuration, do not use the Members property. + Doing so will generate an error. + + .PARAMETER MembersToExclude + Use this property to remove members from the existing membership of the group. + + The value of this property is an array of strings of the formats domain qualified name + (domain\username), UPN (username@domainname), distinguished name (CN=username,DC=...) and/or + a unqualified (username) for local machine accounts. + + If you set this property in a configuration, do not use the Members property. + Doing so will generate an error. + + .PARAMETER Credential + The credentials required to access remote resources. Note: This account must have the + appropriate Active Directory permissions to add all non-local accounts to the group. + Otherwise, an error will occur. +#> +function Set-TargetResourceOnFullSKU +{ + [CmdletBinding(SupportsShouldProcess = $true)] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $GroupName, + + [Parameter()] + [ValidateSet('Present', 'Absent')] + [System.String] + $Ensure = 'Present', + + [Parameter()] + [System.String] + $Description, + + [Parameter()] + [System.String[]] + $Members, + + [Parameter()] + [System.String[]] + $MembersToInclude, + + [Parameter()] + [System.String[]] + $MembersToExclude, + + [Parameter()] + [ValidateNotNullOrEmpty()] + [System.Management.Automation.PSCredential] + [System.Management.Automation.Credential()] + $Credential + ) + + $principalContextCache = @{} + $disposables = New-Object -TypeName 'System.Collections.ArrayList' + + try + { + $principalContext = Get-PrincipalContext ` + -PrincipalContextCache $principalContextCache ` + -Disposables $disposables ` + -Scope $env:computerName + + # Try to find a group by its name. + $group = Get-Group -GroupName $GroupName -PrincipalContext $principalContext + $groupOriginallyExists = $null -ne $group + + if ($Ensure -eq 'Present') + { + $shouldProcessTarget = $script:localizedData.GroupWithName -f $GroupName + if ($groupOriginallyExists) + { + $null = $disposables.Add($group) + $whatIfShouldProcess = $PSCmdlet.ShouldProcess($shouldProcessTarget, $script:localizedData.SetOperation) + } + else + { + $whatIfShouldProcess = $PSCmdlet.ShouldProcess($shouldProcessTarget, $script:localizedData.AddOperation) + } + + if ($whatIfShouldProcess) + { + $saveChanges = $false + + if (-not $groupOriginallyExists) + { + $localPrincipalContext = Get-PrincipalContext -PrincipalContextCache $principalContextCache ` + -Disposables $disposables -Scope $env:COMPUTERNAME + + $group = New-Object -TypeName 'System.DirectoryServices.AccountManagement.GroupPrincipal' ` + -ArgumentList @( $localPrincipalContext ) + $null = $disposables.Add($group) + + $group.Name = $GroupName + $saveChanges = $true + } + + # Set group properties. + + if ($PSBoundParameters.ContainsKey('Description') -and $Description -ne $group.Description) + { + $group.Description = $Description + $saveChanges = $true + } + + $actualMembersAsPrincipals = $null + + <# + Group members can be updated in two ways: + 1. Supplying the Members parameter - this causes the membership to be replaced + with the members defined in Members. + + NOTE: If Members is empty, the group membership is cleared. + + 2. Providing MembersToInclude and/or MembersToExclude + - this adds/removes members from the list. + + If Members is mutually exclusive with MembersToInclude and MembersToExclude + If Members is not defined then MembersToInclude or MembersToExclude + must contain at least one entry. + #> + if ($PSBoundParameters.ContainsKey('Members')) + { + foreach ($incompatibleParameterName in @( 'MembersToInclude', 'MembersToExclude' )) + { + if ($PSBoundParameters.ContainsKey($incompatibleParameterName)) + { + New-InvalidArgumentException -ArgumentName $incompatibleParameterName ` + -Message ($script:localizedData.MembersAndIncludeExcludeConflict -f 'Members', $incompatibleParameterName) + } + } + + if ($groupOriginallyExists) + { + $actualMembersAsPrincipals = @( Get-MembersAsPrincipalsList ` + -Group $group ` + -PrincipalContextCache $principalContextCache ` + -Disposables $disposables ` + -Credential $Credential + ) + } + + if ($Members.Count -eq 0 -and $null -ne $actualMembersAsPrincipals -and $actualMembersAsPrincipals.Count -ne 0) + { + Clear-GroupMember -Group $group + $saveChanges = $true + } + elseif ($Members.Count -ne 0) + { + # Remove duplicate names as strings. + $uniqueMembers = $Members | Select-Object -Unique + + # Resolve the names to actual principal objects. + $membersAsPrincipals = @( ConvertTo-UniquePrincipalsList ` + -MemberNames $uniqueMembers ` + -PrincipalContextCache $principalContextCache ` + -Disposables $disposables ` + -Credential $Credential ) + + if ($null -ne $actualMembersAsPrincipals -and $actualMembersAsPrincipals.Count -gt 0) + { + foreach ($memberAsPrincipal in $membersAsPrincipals) + { + if ($actualMembersAsPrincipals -notcontains $memberAsPrincipal) + { + Add-GroupMember -Group $group -MemberAsPrincipal $memberAsPrincipal + $saveChanges = $true + } + } + + foreach ($actualMemberAsPrincipal in $actualMembersAsPrincipals) + { + if ($membersAsPrincipals -notcontains $actualMemberAsPrincipal) + { + Remove-GroupMember -Group $group -MemberAsPrincipal $actualMemberAsPrincipal + $saveChanges = $true + } + } + } + else + { + # Set the members of the group + foreach ($memberAsPrincipal in $membersAsPrincipals) + { + Add-GroupMember -Group $group -MemberAsPrincipal $memberAsPrincipal + } + + $saveChanges = $true + } + } + else + { + Write-Verbose -Message ($script:localizedData.GroupAndMembersEmpty -f $GroupName) + } + } + elseif ($PSBoundParameters.ContainsKey('MembersToInclude') -or $PSBoundParameters.ContainsKey('MembersToExclude')) + { + if ($groupOriginallyExists) + { + $actualMembersAsPrincipals = @( Get-MembersAsPrincipalsList ` + -Group $group ` + -PrincipalContextCache $principalContextCache ` + -Disposables $disposables ` + -Credential $Credential + ) + } + + $membersToIncludeAsPrincipals = $null + $uniqueMembersToInclude = $MembersToInclude | Select-Object -Unique + + if ($null -eq $uniqueMembersToInclude) + { + Write-Verbose -Message $script:localizedData.MembersToIncludeEmpty + } + else + { + # Resolve the names to actual principal objects. + $membersToIncludeAsPrincipals = @( ConvertTo-UniquePrincipalsList ` + -MemberNames $uniqueMembersToInclude ` + -PrincipalContextCache $principalContextCache ` + -Disposables $disposables ` + -Credential $Credential + ) + } + + $membersToExcludeAsPrincipals = $null + $uniqueMembersToExclude = $MembersToExclude | Select-Object -Unique + + if ($null -eq $uniqueMembersToExclude) + { + Write-Verbose -Message $script:localizedData.MembersToExcludeEmpty + } + else + { + # Resolve the names to actual principal objects. + $membersToExcludeAsPrincipals = @( ConvertTo-UniquePrincipalsList ` + -MemberNames $uniqueMembersToExclude ` + -PrincipalContextCache $principalContextCache ` + -Disposables $disposables ` + -Credential $Credential + ) + } + + foreach ($includedPrincipal in $membersToIncludeAsPrincipals) + { + <# + Throw an error if any common principals were provided in MembersToInclude + and MembersToExclude. + #> + if ($membersToExcludeAsPrincipals -contains $includedPrincipal) + { + New-InvalidArgumentException -ArgumentName 'MembersToInclude and MembersToExclude' ` + -Message ($script:localizedData.IncludeAndExcludeConflict -f $includedPrincipal.SamAccountName, + 'MembersToInclude', 'MembersToExclude') + } + + if ($actualMembersAsPrincipals -notcontains $includedPrincipal) + { + Add-GroupMember -Group $group -MemberAsPrincipal $includedPrincipal + $saveChanges = $true + } + } + + foreach ($excludedPrincipal in $membersToExcludeAsPrincipals) + { + if ($actualMembersAsPrincipals -contains $excludedPrincipal) + { + Remove-GroupMember -Group $group -MemberAsPrincipal $excludedPrincipal + $saveChanges = $true + } + } + } + + if ($saveChanges) + { + Save-Group -Group $group + + # Send an operation success verbose message. + if ($groupOriginallyExists) + { + Write-Verbose -Message ($script:localizedData.GroupUpdated -f $GroupName) + } + else + { + Write-Verbose -Message ($script:localizedData.GroupCreated -f $GroupName) + } + } + else + { + Write-Verbose -Message ($script:localizedData.NoConfigurationRequired -f $GroupName) + } + } + } + else + { + if ($groupOriginallyExists) + { + if ($PSCmdlet.ShouldProcess(($script:localizedData.GroupWithName -f $GroupName), $script:localizedData.RemoveOperation)) + { + # Don't add group to $disposables since Delete also disposes. + Remove-Group -Group $group + Write-Verbose -Message ($script:localizedData.GroupRemoved -f $GroupName) + } + else + { + $null = $disposables.Add($group) + } + } + else + { + Write-Verbose -Message ($script:localizedData.NoConfigurationRequiredGroupDoesNotExist -f $GroupName) + } + } + } + finally + { + Remove-DisposableObject -Disposables $disposables + } +} + +<# + .SYNOPSIS + The Set-TargetResource cmdlet on Nano Server. + + .PARAMETER GroupName + The name of the group for which you want to ensure a specific state. + + .PARAMETER Ensure + Indicates if the group should exist or not. + + Set this property to Present to ensure that the group exists. + Set this property to Absent to ensure that the group does not exist. + + The default value is Present. + + .PARAMETER Description + The description of the group. + + .PARAMETER Members + Use this property to replace the current group membership with the specified members. + + The value of this property is an array of strings of the formats domain qualified name + (domain\username), UPN (username@domainname), distinguished name (CN=username,DC=...) and/or + a unqualified (username) for local machine accounts. + + If you set this property in a configuration, do not use either the MembersToExclude or + MembersToInclude property. Doing so will generate an error. + + .PARAMETER MembersToInclude + Use this property to add members to the existing membership of the group. + + The value of this property is an array of strings of the formats domain qualified name + (domain\username), UPN (username@domainname), distinguished name (CN=username,DC=...) and/or + a unqualified (username) for local machine accounts. + + If you set this property in a configuration, do not use the Members property. + Doing so will generate an error. + + .PARAMETER MembersToExclude + Use this property to remove members from the existing membership of the group. + + The value of this property is an array of strings of the formats domain qualified name + (domain\username), UPN (username@domainname), distinguished name (CN=username,DC=...) and/or + a unqualified (username) for local machine accounts. + + If you set this property in a configuration, do not use the Members property. + Doing so will generate an error. + + .PARAMETER Credential + Not used on Nano Server. + Only local users are accessible from the resource. +#> +function Set-TargetResourceOnNanoServer +{ + [CmdletBinding(SupportsShouldProcess = $true)] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $GroupName, + + [Parameter()] + [ValidateSet('Present', 'Absent')] + [System.String] + $Ensure = 'Present', + + [Parameter()] + [System.String] + $Description, + + [Parameter()] + [System.String[]] + $Members, + + [Parameter()] + [System.String[]] + $MembersToInclude, + + [Parameter()] + [System.String[]] + $MembersToExclude, + + [Parameter()] + [ValidateNotNullOrEmpty()] + [System.Management.Automation.PSCredential] + [System.Management.Automation.Credential()] + $Credential + ) + + try + { + $group = Get-LocalGroup -Name $GroupName -ErrorAction 'Stop' + $groupOriginallyExists = $true + } + catch [System.Exception] + { + if ($_.CategoryInfo.Reason -eq 'GroupNotFoundException') + { + # A group with the provided name does not exist. + Write-Verbose -Message ($script:localizedData.GroupDoesNotExist -f $GroupName) + $groupOriginallyExists = $false + } + else + { + New-InvalidOperationException -ErrorRecord $_ + } + } + + if ($Ensure -eq 'Present') + { + $whatIfShouldProcess = + if ($groupOriginallyExists) + { + $PSCmdlet.ShouldProcess(($script:localizedData.GroupWithName -f $GroupName), + $script:localizedData.SetOperation) + } + else + { + $PSCmdlet.ShouldProcess(($script:localizedData.GroupWithName -f $GroupName), + $script:localizedData.AddOperation) + } + + if ($whatIfShouldProcess) + { + if (-not $groupOriginallyExists) + { + $group = New-LocalGroup -Name $GroupName + Write-Verbose -Message ($script:localizedData.GroupCreated -f $GroupName) + } + + # Set the group properties. + if ($PSBoundParameters.ContainsKey('Description') -and + ((-not $groupOriginallyExists) -or ($Description -ne $group.Description))) + { + Set-LocalGroup -Name $GroupName -Description $Description + } + + if ($PSBoundParameters.ContainsKey('Members')) + { + foreach ($incompatibleParameterName in @( 'MembersToInclude', 'MembersToExclude' )) + { + if ($PSBoundParameters.ContainsKey($incompatibleParameterName)) + { + New-InvalidArgumentException -ArgumentName $incompatibleParameterName ` + -Message ($script:localizedData.MembersAndIncludeExcludeConflict -f 'Members', $incompatibleParameterName) + } + } + + $groupMembers = Get-MembersOnNanoServer -Group $group + + # Remove duplicate names as strings. + $uniqueMembers = $Members | Select-Object -Unique + + # Remove unspecified members + foreach ($groupMember in $groupMembers) + { + if ($uniqueMembers -notcontains $groupMember) + { + Remove-LocalGroupMember -Group $GroupName -Member $groupMember + } + } + + # Add specified missing members + foreach ($uniqueMember in $uniqueMembers) + { + if ($groupMembers -notcontains $uniqueMember) + { + Add-LocalGroupMember -Group $GroupName -Member $uniqueMember + } + } + } + elseif ($PSBoundParameters.ContainsKey('MembersToInclude') -or $PSBoundParameters.ContainsKey('MembersToExclude')) + { + [System.Array] $groupMembers = Get-MembersOnNanoServer -Group $group + + $uniqueMembersToInclude = $MembersToInclude | Select-Object -Unique + $uniqueMembersToExclude = $MembersToExclude | Select-Object -Unique + + <# + Both MembersToInclude and MembersToExclude were provided. + Check if they have common principals. + #> + foreach ($includedMember in $uniqueMembersToInclude) + { + foreach ($excludedMember in $uniqueMembersToExclude) + { + if ($includedMember -eq $excludedMember) + { + New-InvalidArgumentException -ArgumentName 'MembersToInclude and MembersToExclude' ` + -Message ($script:localizedData.IncludeAndExcludeConflict -f $includedMember, 'MembersToInclude', + 'MembersToExclude') + } + } + } + + foreach ($includedMember in $uniqueMembersToInclude) + { + if ($groupMembers -notcontains $includedMember) + { + Add-LocalGroupMember -Group $GroupName -Member $includedMember + } + } + + foreach ($excludedMember in $uniqueMembersToExclude) + { + if ($groupMembers -contains $excludedMember) + { + Remove-LocalGroupMember -Group $GroupName -Member $excludedMember + } + } + } + } + } + else + { + # Ensure is set to "Absent". + if ($groupOriginallyExists) + { + $whatIfShouldProcess = $PSCmdlet.ShouldProcess( + ($script:localizedData.GroupWithName -f $GroupName), $script:localizedData.RemoveOperation) + if ($whatIfShouldProcess) + { + # The group exists. Remove the group by the provided name. + Remove-LocalGroup -Name $GroupName + Write-Verbose -Message ($script:localizedData.GroupRemoved -f $GroupName) + } + } + else + { + Write-Verbose -Message ($script:localizedData.NoConfigurationRequiredGroupDoesNotExist -f $GroupName) + } + } +} + +<# + .SYNOPSIS + The Test-TargetResource cmdlet on a full server. + Tests if the group being managed is in the desired state. + + .PARAMETER GroupName + The name of the group for which you want to test a specific state. + + .PARAMETER Ensure + Indicates if the group should exist or not. + + Set this property to Present to ensure that the group exists. + Set this property to Absent to ensure that the group does not exist. + + The default value is Present. + + .PARAMETER Description + The description of the group to test for. + + .PARAMETER Members + Use this property to test if the existing membership of the group matches + the list provided. + + The value of this property is an array of strings of the formats domain qualified name + (domain\username), UPN (username@domainname), distinguished name (CN=username,DC=...) and/or + a unqualified (username) for local machine accounts. + + If you set this property in a configuration, do not use either the MembersToExclude or + MembersToInclude property. Doing so will generate an error. + + .PARAMETER MembersToInclude + Use this property to test if members need to be added to the existing membership + of the group. + + The value of this property is an array of strings of the formats domain qualified name + (domain\username), UPN (username@domainname), distinguished name (CN=username,DC=...) and/or + a unqualified (username) for local machine accounts. + + If you set this property in a configuration, do not use the Members property. + Doing so will generate an error. + + .PARAMETER MembersToExclude + Use this property to test if members need to removed from the existing membership + of the group. + + The value of this property is an array of strings of the formats domain qualified name + (domain\username), UPN (username@domainname), distinguished name (CN=username,DC=...) and/or + a unqualified (username) for local machine accounts. + + If you set this property in a configuration, do not use the Members property. + Doing so will generate an error. + + .PARAMETER Credential + The credentials required to resolve non-local group members +#> +function Test-TargetResourceOnFullSKU +{ + [OutputType([System.Boolean])] + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $GroupName, + + [Parameter()] + [ValidateSet('Present', 'Absent')] + [System.String] + $Ensure = 'Present', + + [Parameter()] + [System.String] + $Description, + + [Parameter()] + [System.String[]] + $Members, + + [Parameter()] + [System.String[]] + $MembersToInclude, + + [Parameter()] + [System.String[]] + $MembersToExclude, + + [Parameter()] + [ValidateNotNullOrEmpty()] + [System.Management.Automation.PSCredential] + [System.Management.Automation.Credential()] + $Credential + ) + + $principalContextCache = @{} + $disposables = New-Object -TypeName 'System.Collections.ArrayList' + + try + { + $principalContext = Get-PrincipalContext ` + -PrincipalContextCache $PrincipalContextCache ` + -Disposables $disposables ` + -Scope $env:computerName + + $group = Get-Group -GroupName $GroupName -PrincipalContext $principalContext + + if ($null -eq $group) + { + Write-Verbose -Message ($script:localizedData.GroupDoesNotExist -f $GroupName) + return $Ensure -eq 'Absent' + } + + $null = $disposables.Add($group) + Write-Verbose -Message ($script:localizedData.GroupExists -f $GroupName) + + # Validate separate properties. + if ($Ensure -eq 'Absent') + { + Write-Verbose -Message ($script:localizedData.PropertyMismatch -f 'Ensure', 'Absent', 'Present') + return $false + } + + if ($PSBoundParameters.ContainsKey('Description') -and $Description -ne $group.Description) + { + Write-Verbose -Message ($script:localizedData.PropertyMismatch -f 'Description', $Description, $group.Description) + return $false + } + + if ($PSBoundParameters.ContainsKey('Members')) + { + foreach ($incompatibleParameterName in @( 'MembersToInclude', 'MembersToExclude' )) + { + if ($PSBoundParameters.ContainsKey($incompatibleParameterName)) + { + New-InvalidArgumentException -ArgumentName $incompatibleParameterName ` + -Message ($script:localizedData.MembersAndIncludeExcludeConflict -f 'Members', $incompatibleParameterName) + } + } + + $actualMembersAsPrincipals = @( Get-MembersAsPrincipalsList ` + -Group $group ` + -PrincipalContextCache $principalContextCache ` + -Disposables $disposables ` + -Credential $Credential + ) + + $uniqueMembers = $Members | Select-Object -Unique + + if ($null -eq $uniqueMembers) + { + return ($null -eq $actualMembersAsPrincipals -or $actualMembersAsPrincipals.Count -eq 0) + } + else + { + if ($null -eq $actualMembersAsPrincipals -or $actualMembersAsPrincipals.Count -eq 0) + { + return $false + } + + # Resolve the names to actual principal objects. + $expectedMembersAsPrincipals = @( ConvertTo-UniquePrincipalsList ` + -MemberNames $uniqueMembers ` + -PrincipalContextCache $principalContextCache ` + -Disposables $disposables ` + -Credential $Credential + ) + + if ($expectedMembersAsPrincipals.Count -ne $actualMembersAsPrincipals.Count) + { + Write-Verbose -Message ($script:localizedData.MembersNumberMismatch -f 'Members', + $expectedMembersAsPrincipals.Count, $actualMembersAsPrincipals.Count) + return $false + } + + # Compare the two member lists. + foreach ($expectedMemberAsPrincipal in $expectedMembersAsPrincipals) + { + if ($actualMembersAsPrincipals -notcontains $expectedMemberAsPrincipal) + { + Write-Verbose -Message ($script:localizedData.MembersMemberMismatch -f $expectedMemberAsPrincipal.SamAccountName, + 'Members', $group.SamAccountName) + return $false + } + } + } + } + elseif ($PSBoundParameters.ContainsKey('MembersToInclude') -or $PSBoundParameters.ContainsKey('MembersToExclude')) + { + $actualMembersAsPrincipals = @( Get-MembersAsPrincipalsList ` + -Group $group ` + -PrincipalContextCache $principalContextCache ` + -Disposables $disposables ` + -Credential $Credential + ) + + $membersToIncludeAsPrincipals = $null + $uniqueMembersToInclude = $MembersToInclude | Select-Object -Unique + + if ($null -eq $uniqueMembersToInclude) + { + Write-Verbose -Message $script:localizedData.MembersToIncludeEmpty + } + else + { + # Resolve the names to actual principal objects. + $membersToIncludeAsPrincipals = @( ConvertTo-UniquePrincipalsList ` + -MemberNames $uniqueMembersToInclude ` + -PrincipalContextCache $principalContextCache ` + -Disposables $disposables ` + -Credential $Credential + ) + } + + $membersToExcludeAsPrincipals = $null + $uniqueMembersToExclude = $MembersToExclude | Select-Object -Unique + + if ($null -eq $uniqueMembersToExclude) + { + Write-Verbose -Message $script:localizedData.MembersToExcludeEmpty + } + else + { + # Resolve the names to actual principal objects. + $membersToExcludeAsPrincipals = @( ConvertTo-UniquePrincipalsList ` + -MemberNames $uniqueMembersToExclude ` + -PrincipalContextCache $principalContextCache ` + -Disposables $disposables ` + -Credential $Credential + ) + } + + foreach ($includedPrincipal in $membersToIncludeAsPrincipals) + { + <# + Throw an error if any common principals were provided in MembersToInclude + and MembersToExclude. + #> + if ($membersToExcludeAsPrincipals -contains $includedPrincipal) + { + New-InvalidArgumentException -ArgumentName 'MembersToInclude and MembersToExclude' ` + -Message ($script:localizedData.IncludeAndExcludeConflict -f $includedPrincipal.SamAccountName, + 'MembersToInclude', 'MembersToExclude') + } + + if ($actualMembersAsPrincipals -notcontains $includedPrincipal) + { + return $false + } + } + + foreach ($excludedPrincipal in $membersToExcludeAsPrincipals) + { + if ($actualMembersAsPrincipals -contains $excludedPrincipal) + { + return $false + } + } + } + } + finally + { + Remove-DisposableObject -Disposables $disposables + } + + return $true +} + +<# + .SYNOPSIS + The Test-TargetResource cmdlet on a Nano server + Tests if the group being managed is in the desired state. + + .PARAMETER GroupName + The name of the group for which you want to test a specific state. + + .PARAMETER Ensure + Indicates if the group should exist or not. + + Set this property to Present to ensure that the group exists. + Set this property to Absent to ensure that the group does not exist. + + The default value is Present. + + .PARAMETER Description + The description of the group to test for. + + .PARAMETER Members + Use this property to test if the existing membership of the group matches + the list provided. + + The value of this property is an array of strings of the formats domain qualified name + (domain\username), UPN (username@domainname), distinguished name (CN=username,DC=...) and/or + a unqualified (username) for local machine accounts. + + If you set this property in a configuration, do not use either the MembersToExclude or + MembersToInclude property. Doing so will generate an error. + + .PARAMETER MembersToInclude + Use this property to test if members need to be added to the existing membership + of the group. + + The value of this property is an array of strings of the formats domain qualified name + (domain\username), UPN (username@domainname), distinguished name (CN=username,DC=...) and/or + a unqualified (username) for local machine accounts. + + If you set this property in a configuration, do not use the Members property. + Doing so will generate an error. + + .PARAMETER MembersToExclude + Use this property to test if members need to removed from the existing membership + of the group. + + The value of this property is an array of strings of the formats domain qualified name + (domain\username), UPN (username@domainname), distinguished name (CN=username,DC=...) and/or + a unqualified (username) for local machine accounts. + + If you set this property in a configuration, do not use the Members property. + Doing so will generate an error. + + .PARAMETER Credential + The credentials required to resolve non-local group members +#> +function Test-TargetResourceOnNanoServer +{ + [OutputType([System.Boolean])] + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $GroupName, + + [Parameter()] + [ValidateSet('Present', 'Absent')] + [System.String] + $Ensure = 'Present', + + [Parameter()] + [System.String] + $Description, + + [Parameter()] + [System.String[]] + $Members, + + [Parameter()] + [System.String[]] + $MembersToInclude, + + [Parameter()] + [System.String[]] + $MembersToExclude, + + [Parameter()] + [ValidateNotNullOrEmpty()] + [System.Management.Automation.PSCredential] + [System.Management.Automation.Credential()] + $Credential + ) + + try + { + $group = Get-LocalGroup -Name $GroupName -ErrorAction Stop + } + catch [System.Exception] + { + if ($_.CategoryInfo.Reason -eq 'GroupNotFoundException') + { + # A group with the provided name does not exist. + Write-Verbose -Message ($script:localizedData.GroupDoesNotExist -f $GroupName) + + return ($Ensure -eq 'Absent') + } + + New-InvalidOperationException -ErrorRecord $_ + } + + # A group with the provided name exists. + Write-Verbose -Message ($script:localizedData.GroupExists -f $GroupName) + + # Validate separate properties. + if ($Ensure -eq 'Absent') + { + Write-Verbose -Message ($script:localizedData.PropertyMismatch -f 'Ensure', 'Absent', 'Present') + return $false + } + + if ($PSBoundParameters.ContainsKey('Description') -and $Description -ne $group.Description) + { + Write-Verbose -Message ($script:localizedData.PropertyMismatch -f 'Description', $Description, $group.Description) + return $false + } + + if ($PSBoundParameters.ContainsKey('Members')) + { + foreach ($incompatibleParameterName in @( 'MembersToInclude', 'MembersToExclude' )) + { + if ($PSBoundParameters.ContainsKey($incompatibleParameterName)) + { + New-InvalidArgumentException -ArgumentName $incompatibleParameterName ` + -Message ($script:localizedData.MembersAndIncludeExcludeConflict -f 'Members', $incompatibleParameterName) + } + } + + [System.Array] $groupMembers = Get-MembersOnNanoServer -Group $group + + # Remove duplicate names as strings. + $uniqueMembers = $Members | Select-Object -Unique + + if ($null -eq $uniqueMembers) + { + return ($null -eq $groupMembers -or $groupMembers.Count -eq 0) + } + else + { + if ($null -eq $groupMembers -or $uniqueMembers.Count -ne $groupMembers.Count) + { + return $false + } + + foreach ($groupMember in $groupMembers) + { + if ($uniqueMembers -notcontains $groupMember) + { + return $false + } + } + } + } + elseif ($PSBoundParameters.ContainsKey('MembersToInclude') -or $PSBoundParameters.ContainsKey('MembersToExclude')) + { + $groupMembers = Get-MembersOnNanoServer -Group $group + + $uniqueMembersToInclude = $MembersToInclude | Select-Object -Unique + $uniqueMembersToExclude = $MembersToExclude | Select-Object -Unique + + <# + Both MembersToInclude and MembersToExclude were provided. + Check if they have common principals. + #> + foreach ($includedMember in $uniqueMembersToInclude) + { + foreach ($excludedMember in $uniqueMembersToExclude) + { + if ($includedMember -eq $excludedMember) + { + New-InvalidArgumentException -ArgumentName 'MembersToInclude and MembersToExclude' ` + -Message ($script:localizedData.IncludeAndExcludeConflict -f $includedMember, 'MembersToInclude', + 'MembersToExclude') + } + } + } + + foreach ($includedMember in $uniqueMembersToInclude) + { + if ($groupMembers -notcontains $includedMember) + { + return $false + } + } + + foreach ($excludedMember in $uniqueMembersToExclude) + { + if ($groupMembers -contains $excludedMember) + { + return $false + } + } + } + + # All properties match. Return $true. + return $true +} + +<# + .SYNOPSIS + Retrieves the members of a group on a Nano server. + + .PARAMETER Group + The LocalGroup Object to retrieve members for. +#> +function Get-MembersOnNanoServer +{ + [OutputType([System.String[]])] + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNull()] + [Microsoft.PowerShell.Commands.LocalGroup] + $Group + ) + + $memberNames = New-Object -TypeName 'System.Collections.ArrayList' + + # Get the group members. + $groupMembers = Get-LocalGroupMember -Group $Group + + foreach ($groupMember in $groupMembers) + { + if ($groupMember.PrincipalSource -ieq 'Local') + { + $localMemberName = $groupMember.Name.Substring($groupMember.Name.IndexOf('\') + 1) + $null = $memberNames.Add($localMemberName) + } + else + { + Write-Verbose -Message ($script:localizedData.MemberIsNotALocalUser -f $groupMember.Name,$groupMember.PrincipalSource) + $domainMemberName = $groupMember.Name + $null = $memberNames.Add($domainMemberName) + } + } + + return $memberNames.ToArray() +} + +<# + .SYNOPSIS + Retrieves the members of the given a group on a full server. + + .PARAMETER Group + The GroupPrincipal Object to retrieve members for. + + .PARAMETER PrincipalContextCache + A hashtable cache of PrincipalContext instances for each scope. + This is used to cache PrincipalContext instances for cases where it is used multiple times. + + .PARAMETER Disposables + The ArrayList of disposable objects to which to add any objects that need to be disposed. + + .PARAMETER Credential + The network credential to use when explicit credentials are needed for the target domain. +#> +function Get-MembersOnFullSKU +{ + [OutputType([System.String[]])] + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNull()] + [System.DirectoryServices.AccountManagement.GroupPrincipal] + $Group, + + [Parameter(Mandatory = $true)] + [ValidateNotNull()] + [System.Collections.Hashtable] + [AllowEmptyCollection()] + $PrincipalContextCache, + + [Parameter(Mandatory = $true)] + [ValidateNotNull()] + [System.Collections.ArrayList] + [AllowEmptyCollection()] + $Disposables, + + [Parameter()] + [System.Management.Automation.PSCredential] + [System.Management.Automation.Credential()] + $Credential + ) + + $members = New-Object -TypeName 'System.Collections.ArrayList' + + $membersAsPrincipals = @( Get-MembersAsPrincipalsList ` + -Group $Group ` + -PrincipalContextCache $PrincipalContextCache ` + -Disposables $Disposables ` + -Credential $Credential + ) + + foreach ($memberAsPrincipal in $membersAsPrincipals) + { + if ($memberAsPrincipal.ContextType -eq [System.DirectoryServices.AccountManagement.ContextType]::Domain) + { + # Select only the first part of the full domain name. + $domainName = $memberAsPrincipal.Context.Name + + $domainNameDotIndex = $domainName.IndexOf('.') + if ($domainNameDotIndex -ne -1) + { + $domainName = $domainName.Substring(0, $domainNameDotIndex) + } + + if ($memberAsPrincipal.StructuralObjectClass -ieq 'computer') + { + $null = $members.Add($domainName + '\' + $memberAsPrincipal.Name) + } + else + { + $null = $members.Add($domainName + '\' + $memberAsPrincipal.SamAccountName) + } + } + else + { + $null = $members.Add($memberAsPrincipal.Name) + } + } + + return $members.ToArray() +} + +<# + .SYNOPSIS + Retrieves the members of a group as Principal instances. + + .PARAMETER Group + The group to retrieve members for. + + .PARAMETER PrincipalContextCache + A hashtable cache of PrincipalContext instances for each scope. + This is used to cache PrincipalContext instances for cases where it is used multiple times. + + .PARAMETER Disposables + The ArrayList of disposable objects to which to add any objects that need to be disposed. + + .PARAMETER Credential + The network credential to use when explicit credentials are needed for the target domain. +#> +function Get-MembersAsPrincipalsList +{ + [OutputType([System.DirectoryServices.AccountManagement.Principal[]])] + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNull()] + [System.DirectoryServices.AccountManagement.GroupPrincipal] + $Group, + + [Parameter(Mandatory = $true)] + [ValidateNotNull()] + [System.Collections.Hashtable] + [AllowEmptyCollection()] + $PrincipalContextCache, + + [Parameter(Mandatory = $true)] + [ValidateNotNull()] + [System.Collections.ArrayList] + [AllowEmptyCollection()] + $Disposables, + + [Parameter()] + [System.Management.Automation.PSCredential] + [System.Management.Automation.Credential()] + $Credential + ) + + $principals = New-Object -TypeName 'System.Collections.ArrayList' + + <# + This logic enumerates the group members using the underlying DirectoryEntry API. This is + needed because enumerating the group members as principal instances causes a resolve to + occur. Since there is no facility for passing credentials to perform the resolution, any + members that cannot be resolved using the current user will fail (such as when this + resource runs as SYSTEM). Dropping down to the underyling DirectoryEntry API allows us to + access the account's SID which can then be used to resolve the associated principal using + explicit credentials. + #> + $groupDirectoryMembers = Get-GroupMembersFromDirectoryEntry -Group $Group + + foreach ($groupDirectoryMember in $groupDirectoryMembers) + { + # Extract the ObjectSid from the underlying DirectoryEntry + $memberDirectoryEntry = New-Object -TypeName 'System.DirectoryServices.DirectoryEntry' ` + -ArgumentList @( $groupDirectoryMember ) + $null = $disposables.Add($memberDirectoryEntry) + + $memberDirectoryEntryPathParts = $memberDirectoryEntry.Path.Split('/') + + if ($memberDirectoryEntryPathParts.Count -eq 4) + { + # Parsing WinNT://domainname/accountname or WinNT://machinename/accountname + $scope = $memberDirectoryEntryPathParts[2] + $accountName = $memberDirectoryEntryPathParts[3] + } + elseif ($memberDirectoryEntryPathParts.Count -eq 5) + { + # Parsing WinNT://domainname/machinename/accountname + $scope = $memberDirectoryEntryPathParts[3] + $accountName = $memberDirectoryEntryPathParts[4] + } + else + { + <# + The account is stale either becuase it was deleted or the machine was moved to a + new domain without removing the domain members from the group. If we consider this + a fatal error, the group is no longer managable by the DSC resource. Writing a + warning allows the operation to complete while leaving the stale member in the + group. + #> + Write-Warning -Message ($script:localizedData.MemberNotValid -f $memberDirectoryEntry.Path) + continue + } + + $principalContext = Get-PrincipalContext ` + -Scope $scope ` + -Credential $Credential ` + -PrincipalContextCache $PrincipalContextCache ` + -Disposables $Disposables + + # If local machine qualified, get the PrincipalContext for the local machine + if (Test-IsLocalMachine -Scope $scope) + { + Write-Verbose -Message ($script:localizedData.ResolvingLocalAccount -f $accountName) + } + # The account is domain qualified - credential required to resolve it. + elseif ($null -ne $principalContext) + { + Write-Verbose -Message ($script:localizedData.ResolvingDomainAccount -f $accountName, $scope) + } + else + { + <# + The provided name is not scoped to the local machine and no credential was + provided. This is an unsupported use case. A credential is required to resolve + off-box. + #> + New-InvalidArgumentException -ArgumentName 'Credential' ` + -Message ($script:localizedData.DomainCredentialsRequired -f $accountName) + } + + # Create a SID to enable comparison againt the expected member's SID. + $memberSidBytes = $memberDirectoryEntry.Properties['ObjectSid'].Value + $memberSid = New-Object -TypeName 'System.Security.Principal.SecurityIdentifier' ` + -ArgumentList @( $memberSidBytes, 0 ) + + $principal = Resolve-SidToPrincipal -PrincipalContext $principalContext -Sid $memberSid -Scope $scope + $null = $disposables.Add($principal) + + $null = $principals.Add($principal) + } + + return $principals.ToArray() +} + +<# + .SYNOPSIS + Throws an error if a group name contains invalid characters. + + .PARAMETER GroupName + The group name to test. +#> +function Assert-GroupNameValid +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $GroupName + ) + + $invalidCharacters = @( '\', '/', '"', '[', ']', ':', '|', '<', '>', '+', '=', ';', ',', '?', '*', '@' ) + + if ($GroupName.IndexOfAny($invalidCharacters) -ne -1) + { + New-InvalidArgumentException -ArgumentName 'GroupName' ` + -Message ($script:localizedData.InvalidGroupName -f $GroupName, [System.String]::Join(' ', $invalidCharacters)) + } + + $nameContainsOnlyWhitspaceOrDots = $true + + # Check if the name consists of only periods and/or white spaces. + for ($groupNameIndex = 0; $groupNameIndex -lt $GroupName.Length; $groupNameIndex++) + { + if (-not [System.Char]::IsWhiteSpace($GroupName, $groupNameIndex) -and $GroupName[$groupNameIndex] -ne '.') + { + $nameContainsOnlyWhitspaceOrDots = $false + break + } + } + + if ($nameContainsOnlyWhitspaceOrDots) + { + New-InvalidArgumentException -ArgumentName 'GroupName' ` + -Message ($script:localizedData.InvalidGroupName -f $GroupName, [System.String]::Join(' ', $invalidCharacters)) + } +} + +<# + .SYNOPSIS + Resolves an array of member names to Principal instances. + + .PARAMETER MemberNames + The member names to convert to Principal instances. + + .PARAMETER PrincipalContextCache + A hashtable cache of PrincipalContext instances for each scope. + This is used to cache PrincipalContext instances for cases where it is used multiple times. + + .PARAMETER Disposables + The ArrayList of disposable objects to which to add any objects that need to be disposed. + + .PARAMETER Credential + The network credential to use when explicit credentials are needed for the target domain. +#> +function ConvertTo-UniquePrincipalsList +{ + [OutputType([System.DirectoryServices.AccountManagement.Principal[]])] + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [System.String[]] + $MemberNames, + + [Parameter(Mandatory = $true)] + [ValidateNotNull()] + [System.Collections.Hashtable] + [AllowEmptyCollection()] + $PrincipalContextCache, + + [Parameter(Mandatory = $true)] + [ValidateNotNull()] + [System.Collections.ArrayList] + [AllowEmptyCollection()] + $Disposables, + + [Parameter()] + [System.Management.Automation.PSCredential] + [System.Management.Automation.Credential()] + $Credential + ) + + $principals = @() + + foreach ($memberName in $MemberNames) + { + $principal = ConvertTo-Principal ` + -MemberName $memberName ` + -PrincipalContextCache $PrincipalContextCache ` + -Disposables $Disposables ` + -Credential $Credential + + if ($null -ne $principal) + { + # Do not add duplicate entries + if ($principal.ContextType -eq [System.DirectoryServices.AccountManagement.ContextType]::Domain) + { + $duplicatePrincipal = $principals | Where-Object -FilterScript { $_.DistinguishedName -ieq $principal.DistinguishedName } + + if ($null -eq $duplicatePrincipal) + { + $principals += $principal + } + } + else + { + $duplicatePrincipal = $principals | Where-Object -FilterScript { $_.SamAccountName -ieq $principal.SamAccountName } + + if ($null -eq $duplicatePrincipal) + { + $principals += $principal + } + } + } + } + + return $principals +} + +<# + .SYNOPSIS + Resolves a member name to a Principal instance. + + .PARAMETER MemberName + The member name to convert to a Principal instance. + + .PARAMETER PrincipalContextCache + A hashtable cache of PrincipalContext instances for each scope. + This is used to cache PrincipalContext instances for cases where it is used multiple times. + + .PARAMETER Disposables + The ArrayList of disposable objects to which to add any objects that need to be disposed. + + .PARAMETER Credential + The network credential to use when explicit credentials are needed for the target domain. + + .NOTES + ConvertTo-Principal will fail if a machine name is specified as domainname\machinename. It + will succeed if the machine name is specified as the SAM name (domainname\machinename$) or + as the unqualified machine name. + + Split-MemberName splits the scope and account name to avoid this problem. +#> +function ConvertTo-Principal +{ + [OutputType([System.DirectoryServices.AccountManagement.Principal])] + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNull()] + [System.String] + $MemberName, + + [Parameter(Mandatory = $true)] + [ValidateNotNull()] + [System.Collections.Hashtable] + [AllowEmptyCollection()] + $PrincipalContextCache, + + [Parameter(Mandatory = $true)] + [ValidateNotNull()] + [System.Collections.ArrayList] + [AllowEmptyCollection()] + $Disposables, + + [Parameter()] + [System.Management.Automation.PSCredential] + [System.Management.Automation.Credential()] + $Credential + ) + + # The scope of the the object name when in the form of scope\name, UPN, or DN + $scope, $identityValue = Split-MemberName -MemberName $MemberName + + if (Test-IsLocalMachine -Scope $scope) + { + # If local machine qualified, get the PrincipalContext for the local machine + Write-Verbose -Message ($script:localizedData.ResolvingLocalAccount -f $identityValue) + } + elseif ($null -ne $Credential) + { + # The account is domain qualified - a credential is provided to resolve it. + Write-Verbose -Message ($script:localizedData.ResolvingDomainAccount -f $identityValue, $scope) + } + else + { + <# + The provided name is not scoped to the local machine and no credentials were provided. + If the object is a domain qualified name, we can try to resolve the user with domain + trust, if setup. When using domain trust, we use the object name to resolve. Object + name can be in different formats such as a domain qualified name, UPN, or a + distinguished name for the scope + #> + + Write-Verbose -Message ($script:localizedData.ResolvingDomainAccountWithTrust -f $MemberName) + $identityValue = $MemberName + } + + $principalContext = Get-PrincipalContext ` + -Scope $scope ` + -PrincipalContextCache $PrincipalContextCache ` + -Disposables $Disposables ` + -Credential $Credential + + try + { + $principal = Find-Principal -PrincipalContext $principalContext -IdentityValue $identityValue + } + catch [System.Runtime.InteropServices.COMException] + { + New-InvalidArgumentException -ArgumentName $MemberName ` + -Message ( $script:localizedData.UnableToResolveAccount -f $MemberName, $_.Exception.Message, $_.Exception.HResult ) + } + + if ($null -eq $principal) + { + New-InvalidArgumentException -ArgumentName $MemberName -Message ($script:localizedData.CouldNotFindPrincipal -f $MemberName) + } + + return $principal +} + +<# + .SYNOPSIS + Resolves a SID to a principal. + + .PARAMETER Sid + The security identifier to resolve to a Principal. + + .PARAMETER PrincipalContext + The PrincipalContext to use to resolve the Principal. + + .PARAMETER Scope + The scope of the PrincipalContext. +#> +function Resolve-SidToPrincipal +{ + [OutputType([System.DirectoryServices.AccountManagement.Principal])] + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNull()] + [System.Security.Principal.SecurityIdentifier] + $Sid, + + [Parameter(Mandatory = $true)] + [ValidateNotNull()] + [System.DirectoryServices.AccountManagement.PrincipalContext] + $PrincipalContext, + + [Parameter(Mandatory = $true)] + [System.String] + $Scope + ) + + $principal = Find-Principal -PrincipalContext $PrincipalContext -IdentityValue $Sid.Value -IdentityType ([System.DirectoryServices.AccountManagement.IdentityType]::Sid) + + if ($null -eq $principal) + { + if (Test-IsLocalMachine -Scope $Scope) + { + New-InvalidArgumentException -ArgumentName 'Members, MembersToInclude, or MembersToExclude' -Message ($script:localizedData.CouldNotFindPrincipal -f $Sid.Value) + } + else + { + New-InvalidArgumentException -ArgumentName 'Members, MembersToInclude, MembersToExclude, or Credential' -Message ($script:localizedData.CouldNotFindPrincipal -f $Sid.Value) + } + } + + return $principal +} + +<# + .SYNOPSIS + Retrieves a PrincipalContext to use to resolve an object in the given scope. + + .PARAMETER Scope + The scope to retrieve the principal context for. + + .PARAMETER Credential + The network credential to use when explicit credentials are needed for the target domain. + + .PARAMETER PrincipalContextCache + A hashtable cache of PrincipalContext instances for each scope. + This is used to cache PrincipalContext instances for cases where it is used multiple times. + + .PARAMETER Disposables + The ArrayList of disposable objects to which to add any objects that need to be disposed. + + .NOTES + When a new PrincipalContext is created, it is added to the Disposables list + as well as the PrincipalContextCache. +#> +function Get-PrincipalContext +{ + [OutputType([System.DirectoryServices.AccountManagement.PrincipalContext])] + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $Scope, + + [Parameter()] + [System.Management.Automation.PSCredential] + [System.Management.Automation.Credential()] + $Credential, + + [Parameter(Mandatory = $true)] + [ValidateNotNull()] + [System.Collections.Hashtable] + [AllowEmptyCollection()] + $PrincipalContextCache, + + [Parameter(Mandatory = $true)] + [ValidateNotNull()] + [System.Collections.ArrayList] + [AllowEmptyCollection()] + $Disposables + ) + + $principalContext = $null + + if (Test-IsLocalMachine -Scope $Scope) + { + # Check for a cached PrincipalContext for the local machine. + if ($PrincipalContextCache.ContainsKey($env:computerName)) + { + $principalContext = $PrincipalContextCache[$env:computerName] + } + else + { + # Create a PrincipalContext for the local machine + $principalContext = New-Object -TypeName 'System.DirectoryServices.AccountManagement.PrincipalContext' ` + -ArgumentList @( [System.DirectoryServices.AccountManagement.ContextType]::Machine ) + + # Cache the PrincipalContext for this scope for subsequent calls. + $null = $PrincipalContextCache.Add($env:computerName, $principalContext) + $null = $Disposables.Add($principalContext) + } + } + elseif ($PrincipalContextCache.ContainsKey($Scope)) + { + $principalContext = $PrincipalContextCache[$Scope] + } + elseif ($null -ne $Credential) + { + # Create a PrincipalContext targeting $Scope using the network credentials that were passed in. + $credentialDomain = $Credential.GetNetworkCredential().Domain + $credentialUserName = $Credential.GetNetworkCredential().UserName + if ($credentialDomain -ne [System.String]::Empty) + { + $principalContextName = "$credentialDomain\$credentialUserName" + } + else + { + $principalContextName = $credentialUserName + } + + $principalContext = New-Object -TypeName 'System.DirectoryServices.AccountManagement.PrincipalContext' ` + -ArgumentList @( [System.DirectoryServices.AccountManagement.ContextType]::Domain, $Scope, + $principalContextName, $Credential.GetNetworkCredential().Password ) + + # Cache the PrincipalContext for this scope for subsequent calls. + $null = $PrincipalContextCache.Add($Scope, $principalContext) + $null = $Disposables.Add($principalContext) + } + else + { + # Get a PrincipalContext for the current user in the target domain (even for local System account). + $principalContext = New-Object -TypeName 'System.DirectoryServices.AccountManagement.PrincipalContext' ` + -ArgumentList @( [System.DirectoryServices.AccountManagement.ContextType]::Domain, $Scope ) + + # Cache the PrincipalContext for this scope for subsequent calls. + $null = $PrincipalContextCache.Add($Scope, $principalContext) + $null = $Disposables.Add($principalContext) + } + + return $principalContext +} + +<# + .SYNOPSIS + Determines if a scope represents the current machine. + + .PARAMETER Scope + The scope to test. +#> +function Test-IsLocalMachine +{ + [OutputType([System.Boolean])] + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $Scope + ) + + $localMachineScopes = @( '.', $env:computerName, 'localhost', '127.0.0.1', 'NT Authority', 'NT Service', 'BuiltIn' ) + + if ($localMachineScopes -icontains $Scope) + { + return $true + } + + <# + Determine if we have an ip address that matches an ip address on one of the network + adapters. This is likely overkill. Consider removing it. + #> + if ($Scope.Contains('.')) + { + $win32NetworkAdapterConfigurations = @( Get-CimInstance -ClassName 'Win32_NetworkAdapterConfiguration' ) + foreach ($win32NetworkAdapterConfiguration in $win32NetworkAdapterConfigurations) + { + if ($null -ne $win32NetworkAdapterConfiguration.IPAddress) + { + foreach ($ipAddress in $win32NetworkAdapterConfiguration.IPAddress) + { + if ($ipAddress -eq $Scope) + { + return $true + } + } + } + } + } + + return $false +} + +<# + .SYNOPSIS + Splits a member name into the scope and the account name. + + + .DESCRIPTION + The returned $scope is used to determine where to perform the resolution, the local machine + or a target domain. The returned $accountName is the name of the account to resolve. + + The following details the formats that are handled as well as how the values are + determined: + + Domain Qualified Names: (domainname\username) + + The value is split on the first '\' character with the left hand side returned as the scope + and the right hand side returned as the account name. + + UPN: (username@domainname) + + The value is split on the first '@' character with the left hand side returned as the + account name and the right hand side returned as the scope. + + Distinguished Name: + + The value at the first occurance of 'DC=' is used to extract the unqualified domain name. + The incoming string is returned, as is, for the account name. + + Unqualified Account Names: + + The incoming string is returned as the account name and the local machine name is returned + as the scope. Note that values that do not fall into the above categories are interpreted + as unqualified account names. + + .PARAMETER MemberName + The full name of the member to split. + + .NOTES + ConvertTo-Principal will fail if a machine name is specified as domainname\machinename. It + will succeed if the machine name is specified as the SAM name (domainname\machinename$) or + as the unqualified machine name. + + Split-MemberName splits the scope and account name to avoid this problem. +#> +function Split-MemberName +{ + [OutputType([System.String[]])] + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $MemberName + ) + + # Assume no scope is defined or $FullName is a DistinguishedName + $scope = $env:computerName + $accountName = $MemberName + + # Parse domain or machine qualified account name + $separatorIndex = $MemberName.IndexOf('\') + if ($separatorIndex -ne -1) + { + $scope = $MemberName.Substring(0, $separatorIndex) + + if (Test-IsLocalMachine -Scope $scope) + { + $scope = $env:computerName + } + + $accountName = $MemberName.Substring($separatorIndex + 1) + + return [System.String[]] @( $scope, $accountName ) + } + + # Parse UPN for the scope + $separatorIndex = $MemberName.IndexOf('@') + if ($separatorIndex -ne -1) + { + $scope = $MemberName.Substring($separatorIndex + 1) + $accountName = $MemberName.Substring(0, $separatorIndex) + + return [System.String[]] @( $scope, $accountName ) + } + + # Parse distinguished name for the scope + $distinguishedNamePrefix = 'DC=' + + $separatorIndex = $MemberName.IndexOf($distinguishedNamePrefix, [System.StringComparison]::OrdinalIgnoreCase) + if ($separatorIndex -ne -1) + { + <# + For member names in the distinguished name format, the account name returned should be + the entire distinguished name. + See the initialization of $accountName above. + #> + + $startScopeIndex = $separatorIndex + $distinguishedNamePrefix.Length + $endScopeIndex = $MemberName.IndexOf(',', $startScopeIndex) + + if ($endScopeIndex -gt $startScopeIndex) + { + $scopeLength = $endScopeIndex - $separatorIndex - $distinguishedNamePrefix.Length + $scope = $MemberName.Substring($startScopeIndex, $scopeLength) + + return [System.String[]] @( $scope, $accountName ) + } + } + + return [System.String[]] @( $scope, $accountName ) +} + +<# + .SYNOPSIS + Finds a principal by identity. + Wrapper function for testing. + + .PARAMETER PrincipalContext + The principal context to find the principal in. + + .PARAMETER IdentityValue + The identity value to find the principal by (e.g. username). + + .PARAMETER IdentityType + The identity type of the principal to find. +#> +function Find-Principal +{ + [OutputType([System.DirectoryServices.AccountManagement.Principal])] + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [System.DirectoryServices.AccountManagement.PrincipalContext] + $PrincipalContext, + + [Parameter(Mandatory = $true)] + [System.String] + $IdentityValue, + + [Parameter()] + [System.DirectoryServices.AccountManagement.IdentityType] + $IdentityType + ) + + if ($PSBoundParameters.ContainsKey('IdentityType')) + { + return [System.DirectoryServices.AccountManagement.Principal]::FindByIdentity($PrincipalContext, $IdentityType, $IdentityValue) + } + else + { + return [System.DirectoryServices.AccountManagement.Principal]::FindByIdentity($PrincipalContext, $IdentityValue) + } + +} + +<# + .SYNOPSIS + Retrieves a local Windows group. + + .PARAMETER GroupName + The name of the group to retrieve. + + .PARAMETER Disposables + The ArrayList of disposable objects to which to add any objects that need to be disposed. + + .PARAMETER PrincipalContextCache + A hashtable cache of PrincipalContext instances for each scope. + This is used to cache PrincipalContext instances for cases where it is used multiple times. + + .NOTES + The returned value is NOT added to the $disposables list because the caller may need to + call $group.Delete() which also disposes it. +#> +function Get-Group +{ + [OutputType([System.DirectoryServices.AccountManagement.GroupPrincipal])] + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $GroupName, + + [Parameter(Mandatory = $true)] + [System.DirectoryServices.AccountManagement.PrincipalContext] + $PrincipalContext + ) + + $principalContext = Get-PrincipalContext ` + -PrincipalContextCache $PrincipalContextCache ` + -Disposables $Disposables ` + -Scope $env:COMPUTERNAME + + try + { + $group = [System.DirectoryServices.AccountManagement.GroupPrincipal]::FindByIdentity($PrincipalContext, $GroupName) + } + catch + { + $group = $null + } + + return $group +} + +<# + .SYNOPSIS + Retrieves the members of a group from the underlying directory entry. + + .PARAMETER Group + The group to retrieve the members of. +#> +function Get-GroupMembersFromDirectoryEntry +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNull()] + [System.DirectoryServices.AccountManagement.GroupPrincipal] + $Group + ) + + $groupDirectoryEntry = $Group.GetUnderlyingObject() + return $groupDirectoryEntry.Invoke('Members') +} + +<# + .SYNOPSIS + Clears the members of the specified group. + This is a wrapper function for testing purposes. + + .PARAMETER Group + The group to clear the members of. +#> +function Clear-GroupMember +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNull()] + [System.DirectoryServices.AccountManagement.GroupPrincipal] + $Group + ) + + $Group.Members.Clear() +} + +<# + .SYNOPSIS + Adds the specified member to the specified group. + This is a wrapper function for testing purposes. + + .PARAMETER Group + The group to add the member to. + + .PARAMETER MemberAsPrincipal + The member to add to the group as a principal. +#> +function Add-GroupMember +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNull()] + [System.DirectoryServices.AccountManagement.GroupPrincipal] + $Group, + + [Parameter(Mandatory = $true)] + [ValidateNotNull()] + [System.DirectoryServices.AccountManagement.Principal] + $MemberAsPrincipal + ) + + $Group.Members.Add($MemberAsPrincipal) +} + +<# + .SYNOPSIS + Removes the specified member from the specified group. + This is a wrapper function for testing purposes. + + .PARAMETER Group + The group to remove the member from. + + .PARAMETER MemberAsPrincipal + The member to remove from the group as a principal. +#> +function Remove-GroupMember +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNull()] + [System.DirectoryServices.AccountManagement.GroupPrincipal] + $Group, + + [Parameter(Mandatory = $true)] + [ValidateNotNull()] + [System.DirectoryServices.AccountManagement.Principal] + $MemberAsPrincipal + ) + + $Group.Members.Remove($MemberAsPrincipal) +} + +<# + .SYNOPSIS + Deletes the specified group. + This is a wrapper function for testing purposes. + + .PARAMETER Group + The group to delete. +#> +function Remove-Group +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNull()] + [System.DirectoryServices.AccountManagement.GroupPrincipal] + $Group + ) + + $Group.Delete() +} + +<# + .SYNOPSIS + Saves the specified group. + This is a wrapper function for testing purposes. + + .PARAMETER Group + The group to save. +#> +function Save-Group +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNull()] + [System.DirectoryServices.AccountManagement.GroupPrincipal] + $Group + ) + + $Group.Save() +} + +<# + .SYNOPSIS + Disposes of the contents of an array list containing IDisposable objects. + + .PARAMETER Disosables + The array list of IDisposable Objects to dispose of. +#> +function Remove-DisposableObject +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNull()] + [System.Collections.ArrayList] + [AllowEmptyCollection()] + $Disposables + ) + + foreach ($disposable in $Disposables) + { + if ($disposable -is [System.IDisposable]) + { + $disposable.Dispose() + } + } +} diff --git a/deployment/dsc/azshcihost/xPSDesiredStateConfiguration/9.1.0/DSCResources/DSC_xGroupResource/DSC_xGroupResource.schema.mof b/deployment/dsc/azshcihost/xPSDesiredStateConfiguration/9.1.0/DSCResources/DSC_xGroupResource/DSC_xGroupResource.schema.mof new file mode 100644 index 0000000..764630f --- /dev/null +++ b/deployment/dsc/azshcihost/xPSDesiredStateConfiguration/9.1.0/DSCResources/DSC_xGroupResource/DSC_xGroupResource.schema.mof @@ -0,0 +1,12 @@ + +[ClassVersion("1.0.0"),FriendlyName("xGroup")] +class DSC_xGroupResource : OMI_BaseResource +{ + [Key, Description("The name of the group to create, modify, or remove.")] String GroupName; + [Write, ValueMap{"Present", "Absent"}, Values{"Present", "Absent"}, Description("Indicates if the group should exist or not.")] String Ensure; + [Write, Description("The description the group should have.")] String Description; + [Write, Description("The members the group should have.")] String Members[]; + [Write, Description("The members the group should include.")] String MembersToInclude[]; + [Write, Description("The members the group should exclude.")] String MembersToExclude[]; + [Write, EmbeddedInstance("MSFT_Credential"), Description("A credential to resolve non-local group members.")] String Credential; +}; diff --git a/deployment/dsc/azshcihost/xPSDesiredStateConfiguration/9.1.0/DSCResources/DSC_xGroupResource/en-US/DSC_xGroupResource.schema.mfl b/deployment/dsc/azshcihost/xPSDesiredStateConfiguration/9.1.0/DSCResources/DSC_xGroupResource/en-US/DSC_xGroupResource.schema.mfl new file mode 100644 index 0000000..f999c24 --- /dev/null +++ b/deployment/dsc/azshcihost/xPSDesiredStateConfiguration/9.1.0/DSCResources/DSC_xGroupResource/en-US/DSC_xGroupResource.schema.mfl @@ -0,0 +1,11 @@ +[Description("The xGroup resource provides a mechanism to manage local groups on the target node.") : Amended,AMENDMENT, LOCALE("MS_409")] +class DSC_xGroupResource : OMI_BaseResource +{ + [Key,Description("The name of the group to create, modify, or remove.") : Amended] String GroupName; + [Description("Indicates if the group should exist or not.") : Amended] String Ensure; + [Description("The description the group should have.") : Amended] String Description; + [Description("The members the group should have.") : Amended] String Members[]; + [Description("The members the group should include.") : Amended] String MembersToInclude[]; + [Description("The members the group should exclude.") : Amended] String MembersToExclude[]; + [Description("A credential to resolve non-local group members.") : Amended] String Credential; +}; diff --git a/deployment/dsc/azshcihost/xPSDesiredStateConfiguration/9.1.0/DSCResources/DSC_xGroupResource/en-US/DSC_xGroupResource.strings.psd1 b/deployment/dsc/azshcihost/xPSDesiredStateConfiguration/9.1.0/DSCResources/DSC_xGroupResource/en-US/DSC_xGroupResource.strings.psd1 new file mode 100644 index 0000000..8674e53 --- /dev/null +++ b/deployment/dsc/azshcihost/xPSDesiredStateConfiguration/9.1.0/DSCResources/DSC_xGroupResource/en-US/DSC_xGroupResource.strings.psd1 @@ -0,0 +1,35 @@ +# Localized resources for DSC_xGroupResource + +ConvertFrom-StringData @' + GroupWithName = Group: {0} + RemoveOperation = Remove + AddOperation = Add + SetOperation = Set + GroupCreated = Group {0} created successfully. + GroupUpdated = Group {0} properties updated successfully. + GroupRemoved = Group {0} removed successfully. + NoConfigurationRequired = Group {0} exists on this node with the desired properties. No action required. + NoConfigurationRequiredGroupDoesNotExist = Group {0} does not exist on this node. No action required. + CouldNotFindPrincipal = Could not find a principal with the provided name {0}. + MembersAndIncludeExcludeConflict = The {0} and {1} parameters conflict. The {0} parameter should not be used in any combination with the {1} parameter. + GroupAndMembersEmpty = Members is empty and group {0} has no members. No change to group members is needed. + MemberIsNotALocalUser = {0} is not a local user. User's principal source is {1}. + MemberNotValid = The group member {0} does not exist or cannot be resolved. + IncludeAndExcludeConflict = The principal {0} is included in both {1} and {2} parameter values. The same principal cannot be included in both {1} and {2} parameter values. + InvalidGroupName = The group name {0} cannot be used. Names may not consist entirely of periods and/or whitespace or contain these characters: {1} + GroupExists = A group with the name {0} exists. + GroupDoesNotExist = A group with the name {0} does not exist. + PropertyMismatch = The value of the {0} property is expected to be {1} but it is {2}. + MembersNumberMismatch = The number of provided unique group members {1} in {0} is different from the number of actual group members {2}. + MembersMemberMismatch = At least one member {0} of the provided {1} parameter does not match a user in the existing group {2}. + ResolvingLocalAccount = Resolving {0} as a local account. + ResolvingDomainAccount = Resolving {0} in the domain {1}. + ResolvingDomainAccountWithTrust = Resolving {0} with domain trust. + DomainCredentialsRequired = Credentials are required to resolve the domain account {0}. + UnableToResolveAccount = Unable to resolve account '{0}'. Failed with message: {1} (error code={2}) + InvokingFunctionForGroup = Invoking the function {0} for the group {1}. + SetTargetResourceStartMessage = Begin executing Set functionality on the group {0}. + SetTargetResourceEndMessage = End executing Set functionality on the group {0}. + MembersToIncludeEmpty = MembersToInclude is empty. No group member additions are needed. + MembersToExcludeEmpty = MembersToExclude is empty. No group member removals are needed. +'@ diff --git a/deployment/dsc/azshcihost/xPSDesiredStateConfiguration/9.1.0/DSCResources/DSC_xMsiPackage/DSC_xMsiPackage.psm1 b/deployment/dsc/azshcihost/xPSDesiredStateConfiguration/9.1.0/DSCResources/DSC_xMsiPackage/DSC_xMsiPackage.psm1 new file mode 100644 index 0000000..532d1bb --- /dev/null +++ b/deployment/dsc/azshcihost/xPSDesiredStateConfiguration/9.1.0/DSCResources/DSC_xMsiPackage/DSC_xMsiPackage.psm1 @@ -0,0 +1,1693 @@ +[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '')] +param () + +$errorActionPreference = 'Stop' +Set-StrictMode -Version 'Latest' + +$modulePath = Join-Path -Path (Split-Path -Path (Split-Path -Path $PSScriptRoot -Parent) -Parent) -ChildPath 'Modules' + +# Import the shared modules +Import-Module -Name (Join-Path -Path $modulePath ` + -ChildPath (Join-Path -Path 'xPSDesiredStateConfiguration.Common' ` + -ChildPath 'xPSDesiredStateConfiguration.Common.psm1')) + +# Import Localization Strings +$script:localizedData = Get-LocalizedData -ResourceName 'DSC_xMsiPackage' + +# Path to the directory where the files for a package from a file server will be downloaded to +$script:packageCacheLocation = "$env:ProgramData\Microsoft\Windows\PowerShell\Configuration\BuiltinProvCache\MSFT_xMsiPackage" +$script:msiTools = $null + +<# + .SYNOPSIS + Retrieves the current state of the MSI file with the given Product ID. + + .PARAMETER ProductId + The ID of the MSI file to retrieve the state of, usually a GUID. + + .PARAMETER Path + Not used in Get-TargetResource. +#> +function Get-TargetResource +{ + [OutputType([System.Collections.Hashtable])] + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $ProductId, + + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $Path + ) + + $identifyingNumber = Convert-ProductIdToIdentifyingNumber -ProductId $ProductId + + $packageResourceResult = @{} + + $productEntry = Get-ProductEntry -IdentifyingNumber $identifyingNumber + + if ($null -eq $productEntry) + { + $packageResourceResult = @{ + Ensure = 'Absent' + ProductId = $identifyingNumber + } + + Write-Verbose -Message ($script:localizedData.GetTargetResourceNotFound -f $ProductId) + } + else + { + $packageResourceResult = Get-ProductEntryInfo -ProductEntry $productEntry + $packageResourceResult['ProductId'] = $identifyingNumber + $packageResourceResult['Ensure'] = 'Present' + + Write-Verbose -Message ($script:localizedData.GetTargetResourceFound -f $ProductId) + } + + return $packageResourceResult +} + +<# + .SYNOPSIS + Installs or uninstalls the MSI file at the given path. + + .PARAMETER ProductId + The identifying number used to find the package, usually a GUID. + + .PARAMETER Path + The path to the MSI file to install or uninstall. + + .PARAMETER Ensure + Indicates whether the given MSI file should be installed or uninstalled. + Set this property to Present to install the MSI, and Absent to uninstall + the MSI. + + .PARAMETER Arguments + The arguments to pass to the MSI package during installation or uninstallation + if needed. + + .PARAMETER IgnoreReboot + Ignore a pending reboot if requested by package installation. + By default is `$false` and DSC will try to reboot the system. + + .PARAMETER Credential + The credential of a user account to be used to mount a UNC path if needed. + + .PARAMETER LogPath + The path to the log file to log the output from the MSI execution. + + .PARAMETER FileHash + The expected hash value of the MSI file at the given path. + + .PARAMETER HashAlgorithm + The algorithm used to generate the given hash value. + + .PARAMETER SignerSubject + The subject that should match the signer certificate of the digital signature of the MSI file. + + .PARAMETER SignerThumbprint + The certificate thumbprint that should match the signer certificate of the digital signature of the MSI file. + + .PARAMETER ServerCertificateValidationCallback + PowerShell code to be used to validate SSL certificates for paths using HTTPS. + + .PARAMETER RunAsCredential + The credential of a user account under which to run the installation or uninstallation of the MSI package. +#> +function Set-TargetResource +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $ProductId, + + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $Path, + + [Parameter()] + [ValidateSet('Present', 'Absent')] + [System.String] + $Ensure = 'Present', + + [Parameter()] + [System.String] + $Arguments, + + [Parameter()] + [System.Boolean] + $IgnoreReboot = $false, + + [Parameter()] + [System.Management.Automation.PSCredential] + [System.Management.Automation.Credential()] + $Credential, + + [Parameter()] + [System.String] + $LogPath, + + [Parameter()] + [System.String] + $FileHash, + + [Parameter()] + [ValidateSet('SHA1', 'SHA256', 'SHA384', 'SHA512', 'MD5', 'RIPEMD160')] + [System.String] + $HashAlgorithm = 'SHA256', + + [Parameter()] + [System.String] + $SignerSubject, + + [Parameter()] + [System.String] + $SignerThumbprint, + + [Parameter()] + [System.String] + $ServerCertificateValidationCallback, + + [Parameter()] + [System.Management.Automation.PSCredential] + [System.Management.Automation.CredentialAttribute()] + $RunAsCredential + ) + + $uri = Convert-PathToUri -Path $Path + $identifyingNumber = Convert-ProductIdToIdentifyingNumber -ProductId $ProductId + + # Ensure that the actual file extension is checked if a query string is passed in + if ($null -ne $uri.LocalPath) + { + $uriLocalPath = (Split-Path -Path $uri.LocalPath -Leaf) + Assert-PathExtensionValid -Path $uriLocalPath + } + else + { + Assert-PathExtensionValid -Path $Path + } + + <# + Path gets overwritten in the download code path. Retain the user's original Path so as + to provide a more descriptive error message in case the install succeeds but the named + package can't be found on the system afterward. + #> + $originalPath = $Path + + Write-Verbose -Message $script:localizedData.PackageConfigurationStarting + + $psDrive = $null + $downloadedFileName = $null + + $exitCode = 0 + + try + { + if ($PSBoundParameters.ContainsKey('LogPath')) + { + New-LogFile -LogPath $LogPath + } + + # Download or mount file as necessary + if ($Ensure -eq 'Present') + { + $localPath = $Path + + if ($null -ne $uri.LocalPath) + { + $localPath = $uri.LocalPath + } + + if ($uri.IsUnc) + { + $psDriveArgs = @{ + Name = [System.Guid]::NewGuid() + PSProvider = 'FileSystem' + Root = Split-Path -Path $localPath + } + + if ($PSBoundParameters.ContainsKey('Credential')) + { + $psDriveArgs['Credential'] = $Credential + } + + $psDrive = New-PSDrive @psDriveArgs + $Path = Join-Path -Path $psDrive.Root -ChildPath (Split-Path -Path $localPath -Leaf) + } + elseif (@( 'http', 'https' ) -contains $uri.Scheme) + { + $outStream = $null + + try + { + if (-not (Test-Path -Path $script:packageCacheLocation -PathType 'Container')) + { + Write-Verbose -Message ($script:localizedData.CreatingCacheLocation) + $null = New-Item -Path $script:packageCacheLocation -ItemType 'Directory' + } + + $destinationPath = Join-Path -Path $script:packageCacheLocation -ChildPath (Split-Path -Path $localPath -Leaf) + + try + { + Write-Verbose -Message ($script:localizedData.CreatingTheDestinationCacheFile) + $outStream = New-Object -TypeName 'System.IO.FileStream' -ArgumentList @( $destinationPath, 'Create' ) + } + catch + { + # Should never happen since we own the cache directory + New-InvalidOperationException -Message ($script:localizedData.CouldNotOpenDestFile -f $destinationPath) -ErrorRecord $_ + } + + try + { + $responseStream = Get-WebRequestResponse -Uri $uri -ServerCertificateValidationCallback $ServerCertificateValidationCallback + + Copy-ResponseStreamToFileStream -ResponseStream $responseStream -FileStream $outStream + } + finally + { + if ((Test-Path -Path variable:responseStream) -and ($null -ne $responseStream)) + { + Close-Stream -Stream $responseStream + } + } + } + finally + { + if ($null -ne $outStream) + { + Close-Stream -Stream $outStream + } + } + + Write-Verbose -Message ($script:localizedData.RedirectingPackagePathToCacheFileLocation) + $Path = $destinationPath + $downloadedFileName = $destinationPath + } + + # At this point the Path should be valid if this is an install case + if (-not (Test-Path -Path $Path -PathType 'Leaf')) + { + New-InvalidOperationException -Message ($script:localizedData.PathDoesNotExist -f $Path) + } + + Assert-FileValid -Path $Path -HashAlgorithm $HashAlgorithm -FileHash $FileHash -SignerSubject $SignerSubject -SignerThumbprint $SignerThumbprint + + # Check if the MSI package specifies the ProductCode, and if so make sure they match + $productCode = Get-MsiProductCode -Path $Path + + if ((-not [System.String]::IsNullOrEmpty($identifyingNumber)) -and ($identifyingNumber -ne $productCode)) + { + New-InvalidArgumentException -ArgumentName 'ProductId' -Message ($script:localizedData.InvalidId -f $identifyingNumber, $productCode) + } + } + + $exitCode = Start-MsiProcess -IdentifyingNumber $identifyingNumber -Path $Path -Ensure $Ensure -Arguments $Arguments -LogPath $LogPath -RunAsCredential $RunAsCredential + } + finally + { + if ($null -ne $psDrive) + { + $null = Remove-PSDrive -Name $psDrive -Force + } + } + + if ($null -ne $downloadedFileName) + { + <# + This is deliberately not in the finally block because we want to leave the downloaded + file on disk if an error occurred as a debugging aid for the user. + #> + $null = Remove-Item -Path $downloadedFileName + } + + <# + Check if a reboot is required, if so notify CA. The MSFT_ServerManagerTasks provider is + missing on some client SKUs (worked on both Server and Client Skus in Windows 10). + #> + $serverFeatureData = Invoke-CimMethod ` + -Name 'GetServerFeature' ` + -Namespace 'root\microsoft\windows\servermanager' ` + -Class 'MSFT_ServerManagerTasks' ` + -Arguments @{ + BatchSize = 256 + } ` + -ErrorAction 'Ignore' + + $registryData = Get-ItemProperty -Path 'HKLM:\SYSTEM\CurrentControlSet\Control\Session Manager' -Name 'PendingFileRenameOperations' -ErrorAction 'Ignore' + + $rebootRequired = (($exitcode -eq 3010) -or ($exitcode -eq 1641) -or ($null -ne $registryData)) + + if (($serverFeatureData -and $serverFeatureData.RequiresReboot) -or $rebootRequired) + { + Write-Verbose $script:localizedData.MachineRequiresReboot + if ($IgnoreReboot) + { + Write-Verbose $script:localizedData.IgnoreReboot + } + else + { + Set-DSCMachineRebootRequired + } + } + elseif ($Ensure -eq 'Present') + { + $productEntry = Get-ProductEntry -IdentifyingNumber $identifyingNumber + + if ($null -eq $productEntry) + { + New-InvalidOperationException -Message ($script:localizedData.PostValidationError -f $originalPath) + } + } + + if ($Ensure -eq 'Present') + { + Write-Verbose -Message $script:localizedData.PackageInstalled + } + else + { + Write-Verbose -Message $script:localizedData.PackageUninstalled + } +} + +<# + .SYNOPSIS + Tests if the MSI file with the given product ID is installed or uninstalled. + + .PARAMETER ProductId + The identifying number used to find the package, usually a GUID. + + .PARAMETER Path + Not Used in Test-TargetResource + + .PARAMETER Ensure + Indicates whether the MSI file should be installed or uninstalled. + Set this property to Present if the MSI file should be installed. Set + this property to Absent if the MSI file should be uninstalled. + + .PARAMETER Arguments + Not Used in Test-TargetResource + + .PARAMETER IgnoreReboot + Not Used in Test-TargetResource + + .PARAMETER Credential + Not Used in Test-TargetResource + + .PARAMETER LogPath + Not Used in Test-TargetResource + + .PARAMETER FileHash + Not Used in Test-TargetResource + + .PARAMETER HashAlgorithm + Not Used in Test-TargetResource + + .PARAMETER SignerSubject + Not Used in Test-TargetResource + + .PARAMETER SignerThumbprint + Not Used in Test-TargetResource + + .PARAMETER ServerCertificateValidationCallback + Not Used in Test-TargetResource + + .PARAMETER RunAsCredential + Not Used in Test-TargetResource +#> +function Test-TargetResource +{ + [OutputType([System.Boolean])] + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $ProductId, + + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $Path, + + [Parameter()] + [ValidateSet('Present', 'Absent')] + [System.String] + $Ensure = 'Present', + + [Parameter()] + [System.String] + $Arguments, + + [Parameter()] + [System.Boolean] + $IgnoreReboot = $false, + + [Parameter()] + [System.Management.Automation.PSCredential] + [System.Management.Automation.Credential()] + $Credential, + + [Parameter()] + [System.String] + $LogPath, + + [Parameter()] + [System.String] + $FileHash, + + [Parameter()] + [ValidateSet('SHA1', 'SHA256', 'SHA384', 'SHA512', 'MD5', 'RIPEMD160')] + [System.String] + $HashAlgorithm = 'SHA256', + + [Parameter()] + [System.String] + $SignerSubject, + + [Parameter()] + [System.String] + $SignerThumbprint, + + [Parameter()] + [System.String] + $ServerCertificateValidationCallback, + + [Parameter()] + [System.Management.Automation.PSCredential] + [System.Management.Automation.CredentialAttribute()] + $RunAsCredential + ) + + $identifyingNumber = Convert-ProductIdToIdentifyingNumber -ProductId $ProductId + + $productEntry = Get-ProductEntry -IdentifyingNumber $identifyingNumber + + if ($null -ne $productEntry) + { + $displayName = Get-ProductEntryValue -ProductEntry $productEntry -Property 'DisplayName' + Write-Verbose -Message ($script:localizedData.PackageAppearsInstalled -f $displayName) + } + else + { + Write-Verbose -Message ($script:localizedData.PackageDoesNotAppearInstalled -f $ProductId) + } + + return (($null -ne $productEntry -and $Ensure -eq 'Present') -or ($null -eq $productEntry -and $Ensure -eq 'Absent')) +} + +<# + .SYNOPSIS + Asserts that the path extension is '.msi' + + .PARAMETER Path + The path to the file to validate the extension of. +#> +function Assert-PathExtensionValid +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $Path + ) + + $pathExtension = [System.IO.Path]::GetExtension($Path) + Write-Verbose -Message ($script:localizedData.ThePathExtensionWasPathExt -f $pathExtension) + + if ($pathExtension.ToLower() -ne '.msi') + { + New-InvalidArgumentException -ArgumentName 'Path' -Message ($script:localizedData.InvalidBinaryType -f $Path) + } +} + +<# + .SYNOPSIS + Converts the given path to a URI and returns the URI object. + Throws an exception if the path's scheme as a URI is not valid. + + .PARAMETER Path + The path to the file to retrieve as a URI. +#> +function Convert-PathToUri +{ + [OutputType([System.Uri])] + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $Path + ) + + try + { + $uri = [System.Uri] $Path + } + catch + { + New-InvalidArgumentException -ArgumentName 'Path' -Message ($script:localizedData.InvalidPath -f $Path) + } + + $validUriSchemes = @( 'file', 'http', 'https' ) + + if ($validUriSchemes -notcontains $uri.Scheme) + { + Write-Verbose -Message ($script:localizedData.TheUriSchemeWasUriScheme -f $uri.Scheme) + New-InvalidArgumentException -ArgumentName 'Path' -Message ($script:localizedData.InvalidPath -f $Path) + } + + return $uri +} + +<# + .SYNOPSIS + Converts the product ID to the identifying number format. + + .PARAMETER ProductId + The product ID to convert to an identifying number. +#> +function Convert-ProductIdToIdentifyingNumber +{ + [OutputType([System.String])] + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $ProductId + ) + + try + { + Write-Verbose -Message ($script:localizedData.ParsingProductIdAsAnIdentifyingNumber -f $ProductId) + $identifyingNumber = '{{{0}}}' -f [System.Guid]::Parse($ProductId).ToString().ToUpper() + + Write-Verbose -Message ($script:localizedData.ParsedProductIdAsIdentifyingNumber -f $ProductId, $identifyingNumber) + return $identifyingNumber + } + catch + { + New-InvalidArgumentException -ArgumentName 'ProductId' -Message ($script:localizedData.InvalidIdentifyingNumber -f $ProductId) + } +} + + +<# + .SYNOPSIS + Retrieves the product entry for the package with the given identifying number. + + .PARAMETER IdentifyingNumber + The identifying number of the product entry to retrieve. +#> +function Get-ProductEntry +{ + [OutputType([Microsoft.Win32.RegistryKey])] + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $IdentifyingNumber + ) + + $uninstallRegistryKey = 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall' + $uninstallRegistryKeyWow64 = 'HKLM:\SOFTWARE\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall' + + $productEntry = $null + + if (-not [System.String]::IsNullOrEmpty($IdentifyingNumber)) + { + $productEntryKeyLocation = Join-Path -Path $uninstallRegistryKey -ChildPath $IdentifyingNumber + $productEntry = Get-Item -Path $productEntryKeyLocation -ErrorAction 'SilentlyContinue' + + if ($null -eq $productEntry) + { + $productEntryKeyLocation = Join-Path -Path $uninstallRegistryKeyWow64 -ChildPath $IdentifyingNumber + $productEntry = Get-Item $productEntryKeyLocation -ErrorAction 'SilentlyContinue' + } + } + + return $productEntry +} + +<# + .SYNOPSIS + Retrieves the information for the given product entry and returns it as a hashtable. + + .PARAMETER ProductEntry + The product entry to retrieve the information for. +#> +function Get-ProductEntryInfo +{ + [OutputType([System.Collections.Hashtable])] + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [Microsoft.Win32.RegistryKey] + $ProductEntry + ) + + $installDate = Get-ProductEntryValue -ProductEntry $ProductEntry -Property 'InstallDate' + + if ($null -ne $installDate) + { + try + { + $installDate = '{0:d}' -f [System.DateTime]::ParseExact($installDate, 'yyyyMMdd', [System.Globalization.CultureInfo]::CurrentCulture).Date + } + catch + { + $installDate = $null + } + } + + $publisher = Get-ProductEntryValue -ProductEntry $ProductEntry -Property 'Publisher' + + $estimatedSizeInKB = Get-ProductEntryValue -ProductEntry $ProductEntry -Property 'EstimatedSize' + + if ($null -ne $estimatedSizeInKB) + { + $estimatedSizeInMB = $estimatedSizeInKB / 1024 + } + + $displayVersion = Get-ProductEntryValue -ProductEntry $ProductEntry -Property 'DisplayVersion' + + $comments = Get-ProductEntryValue -ProductEntry $ProductEntry -Property 'Comments' + + $displayName = Get-ProductEntryValue -ProductEntry $ProductEntry -Property 'DisplayName' + + $installSource = Get-ProductEntryValue -ProductEntry $ProductEntry -Property 'InstallSource' + + return @{ + Name = $displayName + InstallSource = $installSource + InstalledOn = $installDate + Size = $estimatedSizeInMB + Version = $displayVersion + PackageDescription = $comments + Publisher = $publisher + } +} + +<# + .SYNOPSIS + Retrieves the value of the given property for the given product entry. + This is a wrapper for unit testing. + + .PARAMETER ProductEntry + The product entry object to retrieve the property value from. + + .PARAMETER Property + The property to retrieve the value of from the product entry. +#> +function Get-ProductEntryValue +{ + [OutputType([System.Object])] + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [Microsoft.Win32.RegistryKey] + $ProductEntry, + + [Parameter(Mandatory = $true)] + [System.String] + $Property + ) + + return $ProductEntry.GetValue($Property) +} + +<# + .SYNOPSIS + Removes the file at the given path if it exists and creates a new file + to be written to. + + .PARAMETER LogPath + The path where the log file should be created. +#> +function New-LogFile +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $LogPath + ) + + try + { + <# + Pre-verify the log path exists and is writable ahead of time so the user won't + have to detect why the MSI log path doesn't exist. + #> + if (Test-Path -Path $LogPath) + { + $null = Remove-Item -Path $LogPath + } + + $null = New-Item -Path $LogPath -Type 'File' + } + catch + { + New-InvalidOperationException -Message ($script:localizedData.CouldNotOpenLog -f $LogPath) -ErrorRecord $_ + } +} + +<# + .SYNOPSIS + Retrieves the WebRequest response as a stream for the MSI file with the given URI. + + .PARAMETER Uri + The Uri to retrieve the WebRequest from. + + .PARAMETER ServerCertificationValidationCallback + The callback code to validate the SSL certificate for HTTPS URI schemes. +#> +function Get-WebRequestResponse +{ + [OutputType([System.IO.Stream])] + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [System.Uri] + $Uri, + + [Parameter()] + [System.String] + $ServerCertificateValidationCallback + ) + + try + { + $uriScheme = $Uri.Scheme + + Write-Verbose -Message ($script:localizedData.CreatingTheSchemeStream -f $uriScheme) + $webRequest = Get-WebRequest -Uri $Uri + + Write-Verbose -Message ($script:localizedData.SettingDefaultCredential) + $webRequest.Credentials = [System.Net.CredentialCache]::DefaultCredentials + $webRequest.AuthenticationLevel = [System.Net.Security.AuthenticationLevel]::None + + if ($uriScheme -eq 'http') + { + # Default value is MutualAuthRequested, which applies to the https scheme + Write-Verbose -Message ($script:localizedData.SettingAuthenticationLevel) + $webRequest.AuthenticationLevel = [System.Net.Security.AuthenticationLevel]::None + } + elseif ($uriScheme -eq 'https' -and -not [System.String]::IsNullOrEmpty($ServerCertificateValidationCallback)) + { + Write-Verbose -Message $script:localizedData.SettingCertificateValidationCallback + $webRequest.ServerCertificateValidationCallBack = (Get-ScriptBlock -FunctionName $ServerCertificateValidationCallback) + } + + Write-Verbose -Message ($script:localizedData.GettingTheSchemeResponseStream -f $uriScheme) + $responseStream = Get-WebRequestResponseStream -WebRequest $webRequest + + return $responseStream + } + catch + { + New-InvalidOperationException -Message ($script:localizedData.CouldNotGetResponseFromWebRequest -f $uriScheme, $Uri.OriginalString) -ErrorRecord $_ + } +} + +<# + .SYNOPSIS + Creates a WebRequst object based on the given Uri and returns it. + This is a wrapper for unit testing + + .PARAMETER Uri + The URI object to create the WebRequest from +#> +function Get-WebRequest +{ + [OutputType([System.Net.WebRequest])] + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [System.Uri] + $Uri + ) + + return [System.Net.WebRequest]::Create($Uri) +} + +<# + .SYNOPSIS + Retrieves the response stream from the given WebRequest object. + This is a wrapper for unit testing. + + .PARAMETER WebRequest + The WebRequest object to retrieve the response stream from. +#> +function Get-WebRequestResponseStream +{ + [OutputType([System.IO.Stream])] + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [System.Net.WebRequest] + $WebRequest + ) + + return (([System.Net.HttpWebRequest] $WebRequest).GetResponse()).GetResponseStream() +} + +<# + .SYNOPSIS + Converts the given function into a script block and returns it. + This is a wrapper for unit testing + + .PARAMETER Function + The name of the function to convert to a script block +#> +function Get-ScriptBlock +{ + [OutputType([System.Management.Automation.ScriptBlock])] + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $FunctionName + ) + + return [System.Management.Automation.ScriptBlock]::Create($FunctionName) +} + +<# + .SYNOPSIS + Copies the given response stream to the given file stream. + + .PARAMETER ResponseStream + The response stream to copy over. + + .PARAMETER FileStream + The file stream to copy to. +#> +function Copy-ResponseStreamToFileStream +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [System.IO.Stream] + $ResponseStream, + + [Parameter(Mandatory = $true)] + [System.IO.Stream] + $FileStream + ) + + try + { + Write-Verbose -Message ($script:localizedData.CopyingTheSchemeStreamBytesToTheDiskCache) + $null = $ResponseStream.CopyTo($FileStream) + $null = $ResponseStream.Flush() + $null = $FileStream.Flush() + } + catch + { + New-InvalidOperationException -Message ($script:localizedData.ErrorCopyingDataToFile) -ErrorRecord $_ + } +} + +<# + .SYNOPSIS + Closes the given stream. + Wrapper function for unit testing. + + .PARAMETER Stream + The stream to close. +#> +function Close-Stream +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [System.IO.Stream] + $Stream + ) + + $null = $Stream.Close() +} + +<# + .SYNOPSIS + Asserts that the file at the given path has a valid hash, signer thumbprint, and/or + signer subject. If only Path is provided, then this function will never throw. + If FileHash is provided and HashAlgorithm is not, then Sha-256 will be used as the hash + algorithm by default. + + .PARAMETER Path + The path to the file to check. + + .PARAMETER FileHash + The hash that should match the hash of the file. + + .PARAMETER HashAlgorithm + The algorithm to use to retrieve the file hash. + + .PARAMETER SignerThumbprint + The certificate thumbprint that should match the file's signer certificate. + + .PARAMETER SignerSubject + The certificate subject that should match the file's signer certificate. +#> +function Assert-FileValid +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $Path, + + [Parameter()] + [System.String] + $FileHash, + + [Parameter()] + [System.String] + $HashAlgorithm = 'SHA256', + + [Parameter()] + [System.String] + $SignerThumbprint, + + [Parameter()] + [System.String] + $SignerSubject + ) + + if (-not [System.String]::IsNullOrEmpty($FileHash)) + { + Assert-FileHashValid -Path $Path -Hash $FileHash -Algorithm $HashAlgorithm + } + + if (-not [System.String]::IsNullOrEmpty($SignerThumbprint) -or -not [System.String]::IsNullOrEmpty($SignerSubject)) + { + Assert-FileSignatureValid -Path $Path -Thumbprint $SignerThumbprint -Subject $SignerSubject + } +} + +<# + .SYNOPSIS + Asserts that the hash of the file at the given path matches the given hash. + + .PARAMETER Path + The path to the file to check the hash of. + + .PARAMETER Hash + The hash to check against. + + .PARAMETER Algorithm + The algorithm to use to retrieve the file's hash. + Default is 'Sha256' +#> +function Assert-FileHashValid +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $Path, + + [Parameter(Mandatory = $true)] + [System.String] + $Hash, + + [Parameter()] + [System.String] + $Algorithm = 'SHA256' + ) + + Write-Verbose -Message ($script:localizedData.CheckingFileHash -f $Path, $Hash, $Algorithm) + + $fileHash = Get-FileHash -LiteralPath $Path -Algorithm $Algorithm + + if ($fileHash.Hash -ne $Hash) + { + New-InvalidArgumentException -ArgumentName 'FileHash' -Message ($script:localizedData.InvalidFileHash -f $Path, $Hash, $Algorithm) + } +} + +<# + .SYNOPSIS + Asserts that the signature of the file at the given path is valid. + + .PARAMETER Path + The path to the file to check the signature of + + .PARAMETER Thumbprint + The certificate thumbprint that should match the file's signer certificate. + + .PARAMETER Subject + The certificate subject that should match the file's signer certificate. +#> +function Assert-FileSignatureValid +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $Path, + + [Parameter()] + [System.String] + $Thumbprint, + + [Parameter()] + [System.String] + $Subject + ) + + Write-Verbose -Message ($script:localizedData.CheckingFileSignature -f $Path) + + $signature = Get-AuthenticodeSignature -LiteralPath $Path + + if ($signature.Status -ne [System.Management.Automation.SignatureStatus]::Valid) + { + New-InvalidArgumentException -ArgumentName 'Path' -Message ($script:localizedData.InvalidFileSignature -f $Path, $signature.Status) + } + else + { + Write-Verbose -Message ($script:localizedData.FileHasValidSignature -f $Path, $signature.SignerCertificate.Thumbprint, $signature.SignerCertificate.Subject) + } + + if (-not [System.String]::IsNullOrEmpty($Subject) -and ($signature.SignerCertificate.Subject -notlike $Subject)) + { + New-InvalidArgumentException -ArgumentName 'SignerSubject' -Message ($script:localizedData.WrongSignerSubject -f $Path, $Subject) + } + + if (-not [System.String]::IsNullOrEmpty($Thumbprint) -and ($signature.SignerCertificate.Thumbprint -ne $Thumbprint)) + { + New-InvalidArgumentException -ArgumentName 'SignerThumbprint' -Message ($script:localizedData.WrongSignerThumbprint -f $Path, $Thumbprint) + } +} + +<# + .SYNOPSIS + Starts the given MSI installation or uninstallation either as a process or + under a user credential if RunAsCredential is specified. + + .PARAMETER IdentifyingNumber + The identifying number used to find the package. + + .PARAMETER Path + The path to the MSI file to install or uninstall. + + .PARAMETER Ensure + Indicates whether the given MSI file should be installed or uninstalled. + + .PARAMETER Arguments + The arguments to pass to the MSI package. + + .PARAMETER LogPath + The path to the log file to log the output from the MSI execution. + + .PARAMETER RunAsCredential + The credential of a user account under which to run the installation or uninstallation. +#> +function Start-MsiProcess +{ + [OutputType([System.Int32])] + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $IdentifyingNumber, + + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $Path, + + [Parameter()] + [ValidateSet('Present', 'Absent')] + [System.String] + $Ensure = 'Present', + + [Parameter()] + [System.String] + $Arguments, + + [Parameter()] + [System.String] + $LogPath, + + [Parameter()] + [System.Management.Automation.PSCredential] + [System.Management.Automation.CredentialAttribute()] + $RunAsCredential + ) + + $startInfo = New-Object -TypeName 'System.Diagnostics.ProcessStartInfo' + + # Necessary for I/O redirection + $startInfo.UseShellExecute = $false + + $startInfo.FileName = "$env:winDir\system32\msiexec.exe" + + if ($Ensure -eq 'Present') + { + $startInfo.Arguments = '/i "{0}"' -f $Path + } + # Ensure -eq 'Absent' + else + { + $productEntry = Get-ProductEntry -IdentifyingNumber $identifyingNumber + + $id = Split-Path -Path $productEntry.Name -Leaf + $startInfo.Arguments = ('/x{0}' -f $id) + } + + if (-not [System.String]::IsNullOrEmpty($LogPath)) + { + $startInfo.Arguments += (' /log "{0}"' -f $LogPath) + } + + $startInfo.Arguments += ' /quiet /norestart' + + if (-not [System.String]::IsNullOrEmpty($Arguments)) + { + # Append any specified arguments with a space + $startInfo.Arguments += (' {0}' -f $Arguments) + } + + Write-Verbose -Message ($script:localizedData.StartingWithStartInfoFileNameStartInfoArguments -f $startInfo.FileName, $startInfo.Arguments) + + $exitCode = 0 + + try + { + if (-not [System.String]::IsNullOrEmpty($RunAsCredential)) + { + $commandLine = ('"{0}" {1}' -f $startInfo.FileName, $startInfo.Arguments) + $exitCode = Invoke-PInvoke -CommandLine $commandLine -RunAsCredential $RunAsCredential + } + else + { + $process = New-Object -TypeName 'System.Diagnostics.Process' + $process.StartInfo = $startInfo + $exitCode = Invoke-Process -Process $process + } + } + catch + { + New-InvalidOperationException -Message ($script:localizedData.CouldNotStartProcess -f $Path) -ErrorRecord $_ + } + + return $exitCode +} + +<# + .SYNOPSIS + Runs a process as the specified user via PInvoke. Returns the exitCode that + PInvoke returns. + + .PARAMETER CommandLine + The command line (including arguments) of the process to start. + + .PARAMETER RunAsCredential + The user credential to start the process as. +#> +function Invoke-PInvoke +{ + [OutputType([System.Int32])] + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $CommandLine, + + [Parameter(Mandatory = $true)] + [System.Management.Automation.PSCredential] + [System.Management.Automation.CredentialAttribute()] + $RunAsCredential + ) + + Register-PInvoke + [System.Int32] $exitCode = 0 + + $null = [Source.NativeMethods]::CreateProcessAsUser($CommandLine, ` + $RunAsCredential.GetNetworkCredential().Domain, ` + $RunAsCredential.GetNetworkCredential().UserName, ` + $RunAsCredential.GetNetworkCredential().Password, ` + [ref] $exitCode + ) + + return $exitCode +} + +<# + .SYNOPSIS + Starts and waits for a process. + + .PARAMETER Process + The System.Diagnositics.Process object to start. +#> +function Invoke-Process +{ + [OutputType([System.Int32])] + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [System.Diagnostics.Process] + $Process + ) + + $null = $Process.Start() + + $null = $Process.WaitForExit() + return $Process.ExitCode +} + +<# + .SYNOPSIS + Retrieves product code from the MSI at the given path. + + .PARAMETER Path + The path to the MSI to retrieve the product code from. +#> +function Get-MsiProductCode +{ + [OutputType([System.String])] + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $Path + ) + + $msiTools = Get-MsiTool + + $productCode = $msiTools::GetProductCode($Path) + + return $productCode +} + +<# + .SYNOPSIS + Retrieves the MSI tools type. +#> +function Get-MsiTool +{ + [OutputType([System.Type])] + [CmdletBinding()] + param () + + # Check if the variable is already defined + if ($null -ne $script:msiTools) + { + return $script:msiTools + } + + $msiToolsCodeDefinition = @' + [DllImport("msi.dll", CharSet = CharSet.Unicode, PreserveSig = true, SetLastError = true, ExactSpelling = true)] + private static extern UInt32 MsiOpenPackageExW(string szPackagePath, int dwOptions, out IntPtr hProduct); + [DllImport("msi.dll", CharSet = CharSet.Unicode, PreserveSig = true, SetLastError = true, ExactSpelling = true)] + private static extern uint MsiCloseHandle(IntPtr hAny); + [DllImport("msi.dll", CharSet = CharSet.Unicode, PreserveSig = true, SetLastError = true, ExactSpelling = true)] + private static extern uint MsiGetPropertyW(IntPtr hAny, string name, StringBuilder buffer, ref int bufferLength); + private static string GetPackageProperty(string msi, string property) + { + IntPtr MsiHandle = IntPtr.Zero; + try + { + var res = MsiOpenPackageExW(msi, 1, out MsiHandle); + if (res != 0) + { + return null; + } + int length = 256; + var buffer = new StringBuilder(length); + res = MsiGetPropertyW(MsiHandle, property, buffer, ref length); + return buffer.ToString(); + } + finally + { + if (MsiHandle != IntPtr.Zero) + { + MsiCloseHandle(MsiHandle); + } + } + } + public static string GetProductCode(string msi) + { + return GetPackageProperty(msi, "ProductCode"); + } + public static string GetProductName(string msi) + { + return GetPackageProperty(msi, "ProductName"); + } +'@ + + # Check if the the type is already defined + if (([System.Management.Automation.PSTypeName]'Microsoft.Windows.DesiredStateConfiguration.xPackageResource.MsiTools').Type) + { + $script:msiTools = ([System.Management.Automation.PSTypeName]'Microsoft.Windows.DesiredStateConfiguration.xPackageResource.MsiTools').Type + } + else + { + $script:msiTools = Add-Type ` + -Namespace 'Microsoft.Windows.DesiredStateConfiguration.xPackageResource' ` + -Name 'MsiTools' ` + -Using 'System.Text' ` + -MemberDefinition $msiToolsCodeDefinition ` + -PassThru + } + + return $script:msiTools +} + +<# + .SYNOPSIS + Registers PInvoke to run a process as a user. +#> +function Register-PInvoke +{ + [CmdletBinding()] + param () + + $programSource = @' + using System; + using System.Collections.Generic; + using System.Text; + using System.Security; + using System.Runtime.InteropServices; + using System.Diagnostics; + using System.Security.Principal; + using System.ComponentModel; + using System.IO; + namespace Source + { + [SuppressUnmanagedCodeSecurity] + public static class NativeMethods + { + //The following structs and enums are used by the various Win32 API's that are used in the code below + [StructLayout(LayoutKind.Sequential)] + public struct STARTUPINFO + { + public Int32 cb; + public string lpReserved; + public string lpDesktop; + public string lpTitle; + public Int32 dwX; + public Int32 dwY; + public Int32 dwXSize; + public Int32 dwXCountChars; + public Int32 dwYCountChars; + public Int32 dwFillAttribute; + public Int32 dwFlags; + public Int16 wShowWindow; + public Int16 cbReserved2; + public IntPtr lpReserved2; + public IntPtr hStdInput; + public IntPtr hStdOutput; + public IntPtr hStdError; + } + [StructLayout(LayoutKind.Sequential)] + public struct PROCESS_INFORMATION + { + public IntPtr hProcess; + public IntPtr hThread; + public Int32 dwProcessID; + public Int32 dwThreadID; + } + [Flags] + public enum LogonType + { + LOGON32_LOGON_INTERACTIVE = 2, + LOGON32_LOGON_NETWORK = 3, + LOGON32_LOGON_BATCH = 4, + LOGON32_LOGON_SERVICE = 5, + LOGON32_LOGON_UNLOCK = 7, + LOGON32_LOGON_NETWORK_CLEARTEXT = 8, + LOGON32_LOGON_NEW_CREDENTIALS = 9 + } + [Flags] + public enum LogonProvider + { + LOGON32_PROVIDER_DEFAULT = 0, + LOGON32_PROVIDER_WINNT35, + LOGON32_PROVIDER_WINNT40, + LOGON32_PROVIDER_WINNT50 + } + [StructLayout(LayoutKind.Sequential)] + public struct SECURITY_ATTRIBUTES + { + public Int32 Length; + public IntPtr lpSecurityDescriptor; + public bool bInheritHandle; + } + public enum SECURITY_IMPERSONATION_LEVEL + { + SecurityAnonymous, + SecurityIdentification, + SecurityImpersonation, + SecurityDelegation + } + public enum TOKEN_TYPE + { + TokenPrimary = 1, + TokenImpersonation + } + [StructLayout(LayoutKind.Sequential, Pack = 1)] + internal struct TokPriv1Luid + { + public int Count; + public long Luid; + public int Attr; + } + public const int GENERIC_ALL_ACCESS = 0x10000000; + public const int CREATE_NO_WINDOW = 0x08000000; + internal const int SE_PRIVILEGE_ENABLED = 0x00000002; + internal const int TOKEN_QUERY = 0x00000008; + internal const int TOKEN_ADJUST_PRIVILEGES = 0x00000020; + internal const string SE_INCRASE_QUOTA = "SeIncreaseQuotaPrivilege"; + [DllImport("kernel32.dll", + EntryPoint = "CloseHandle", SetLastError = true, + CharSet = CharSet.Auto, CallingConvention = CallingConvention.StdCall)] + public static extern bool CloseHandle(IntPtr handle); + [DllImport("advapi32.dll", + EntryPoint = "CreateProcessAsUser", SetLastError = true, + CharSet = CharSet.Ansi, CallingConvention = CallingConvention.StdCall)] + public static extern bool CreateProcessAsUser( + IntPtr hToken, + string lpApplicationName, + string lpCommandLine, + ref SECURITY_ATTRIBUTES lpProcessAttributes, + ref SECURITY_ATTRIBUTES lpThreadAttributes, + bool bInheritHandle, + Int32 dwCreationFlags, + IntPtr lpEnvrionment, + string lpCurrentDirectory, + ref STARTUPINFO lpStartupInfo, + ref PROCESS_INFORMATION lpProcessInformation + ); + [DllImport("advapi32.dll", EntryPoint = "DuplicateTokenEx")] + public static extern bool DuplicateTokenEx( + IntPtr hExistingToken, + Int32 dwDesiredAccess, + ref SECURITY_ATTRIBUTES lpThreadAttributes, + Int32 ImpersonationLevel, + Int32 dwTokenType, + ref IntPtr phNewToken + ); + [DllImport("advapi32.dll", CharSet = CharSet.Unicode, SetLastError = true)] + public static extern Boolean LogonUser( + String lpszUserName, + String lpszDomain, + String lpszPassword, + LogonType dwLogonType, + LogonProvider dwLogonProvider, + out IntPtr phToken + ); + [DllImport("advapi32.dll", ExactSpelling = true, SetLastError = true)] + internal static extern bool AdjustTokenPrivileges( + IntPtr htok, + bool disall, + ref TokPriv1Luid newst, + int len, + IntPtr prev, + IntPtr relen + ); + [DllImport("kernel32.dll", ExactSpelling = true)] + internal static extern IntPtr GetCurrentProcess(); + [DllImport("advapi32.dll", ExactSpelling = true, SetLastError = true)] + internal static extern bool OpenProcessToken( + IntPtr h, + int acc, + ref IntPtr phtok + ); + [DllImport("kernel32.dll", ExactSpelling = true)] + internal static extern int WaitForSingleObject( + IntPtr h, + int milliseconds + ); + [DllImport("kernel32.dll", ExactSpelling = true)] + internal static extern bool GetExitCodeProcess( + IntPtr h, + out int exitcode + ); + [DllImport("advapi32.dll", SetLastError = true)] + internal static extern bool LookupPrivilegeValue( + string host, + string name, + ref long pluid + ); + public static void CreateProcessAsUser(string strCommand, string strDomain, string strName, string strPassword, ref int ExitCode ) + { + var hToken = IntPtr.Zero; + var hDupedToken = IntPtr.Zero; + TokPriv1Luid tp; + var pi = new PROCESS_INFORMATION(); + var sa = new SECURITY_ATTRIBUTES(); + sa.Length = Marshal.SizeOf(sa); + Boolean bResult = false; + try + { + bResult = LogonUser( + strName, + strDomain, + strPassword, + LogonType.LOGON32_LOGON_BATCH, + LogonProvider.LOGON32_PROVIDER_DEFAULT, + out hToken + ); + if (!bResult) + { + throw new Win32Exception("Logon error #" + Marshal.GetLastWin32Error().ToString()); + } + IntPtr hproc = GetCurrentProcess(); + IntPtr htok = IntPtr.Zero; + bResult = OpenProcessToken( + hproc, + TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY, + ref htok + ); + if(!bResult) + { + throw new Win32Exception("Open process token error #" + Marshal.GetLastWin32Error().ToString()); + } + tp.Count = 1; + tp.Luid = 0; + tp.Attr = SE_PRIVILEGE_ENABLED; + bResult = LookupPrivilegeValue( + null, + SE_INCRASE_QUOTA, + ref tp.Luid + ); + if(!bResult) + { + throw new Win32Exception("Lookup privilege error #" + Marshal.GetLastWin32Error().ToString()); + } + bResult = AdjustTokenPrivileges( + htok, + false, + ref tp, + 0, + IntPtr.Zero, + IntPtr.Zero + ); + if(!bResult) + { + throw new Win32Exception("Token elevation error #" + Marshal.GetLastWin32Error().ToString()); + } + bResult = DuplicateTokenEx( + hToken, + GENERIC_ALL_ACCESS, + ref sa, + (int)SECURITY_IMPERSONATION_LEVEL.SecurityIdentification, + (int)TOKEN_TYPE.TokenPrimary, + ref hDupedToken + ); + if(!bResult) + { + throw new Win32Exception("Duplicate Token error #" + Marshal.GetLastWin32Error().ToString()); + } + var si = new STARTUPINFO(); + si.cb = Marshal.SizeOf(si); + si.lpDesktop = ""; + bResult = CreateProcessAsUser( + hDupedToken, + null, + strCommand, + ref sa, + ref sa, + false, + 0, + IntPtr.Zero, + null, + ref si, + ref pi + ); + if(!bResult) + { + throw new Win32Exception("Create process as user error #" + Marshal.GetLastWin32Error().ToString()); + } + int status = WaitForSingleObject(pi.hProcess, -1); + if(status == -1) + { + throw new Win32Exception("Wait during create process failed user error #" + Marshal.GetLastWin32Error().ToString()); + } + bResult = GetExitCodeProcess(pi.hProcess, out ExitCode); + if(!bResult) + { + throw new Win32Exception("Retrieving status error #" + Marshal.GetLastWin32Error().ToString()); + } + } + finally + { + if (pi.hThread != IntPtr.Zero) + { + CloseHandle(pi.hThread); + } + if (pi.hProcess != IntPtr.Zero) + { + CloseHandle(pi.hProcess); + } + if (hDupedToken != IntPtr.Zero) + { + CloseHandle(hDupedToken); + } + } + } + } + } +'@ + $null = Add-Type -TypeDefinition $programSource -ReferencedAssemblies 'System.ServiceProcess' +} diff --git a/deployment/dsc/azshcihost/xPSDesiredStateConfiguration/9.1.0/DSCResources/DSC_xMsiPackage/DSC_xMsiPackage.schema.mof b/deployment/dsc/azshcihost/xPSDesiredStateConfiguration/9.1.0/DSCResources/DSC_xMsiPackage/DSC_xMsiPackage.schema.mof new file mode 100644 index 0000000..69692b9 --- /dev/null +++ b/deployment/dsc/azshcihost/xPSDesiredStateConfiguration/9.1.0/DSCResources/DSC_xMsiPackage/DSC_xMsiPackage.schema.mof @@ -0,0 +1,24 @@ +[ClassVersion("1.0.0"),FriendlyName("xMsiPackage")] +class DSC_xMsiPackage : OMI_BaseResource +{ + [Key, Description("The identifying number used to find the package, usually a GUID.")] String ProductId; + [Required, Description("The path to the MSI file that should be installed or uninstalled.")] String Path; + [Write, Description("Specifies whether or not the MSI file should be installed or uninstalled."), ValueMap{"Present", "Absent"}, Values{"Present", "Absent"}] String Ensure; + [Write, Description("The arguments to be passed to the MSI package during installation or uninstallation.")] String Arguments; + [Write, Description("The credential of a user account to be used to mount a UNC path if needed."), EmbeddedInstance("MSFT_Credential")] String Credential; + [Write, Description("The path to the log file to log the output from the MSI execution.")] String LogPath; + [Write, Description("The expected hash value of the MSI file at the given path.")] String FileHash; + [Write, Description("The algorithm used to generate the given hash value."), ValueMap{"SHA1", "SHA256", "SHA384", "SHA512", "MD5", "RIPEMD160"}, Values{"SHA1", "SHA256", "SHA384", "SHA512", "MD5", "RIPEMD160"}] String HashAlgorithm; + [Write, Description("The subject that should match the signer certificate of the digital signature of the MSI file.")] String SignerSubject; + [Write, Description("The certificate thumbprint that should match the signer certificate of the digital signature of the MSI file.")] String SignerThumbprint; + [Write, Description("PowerShell code that should be used to validate SSL certificates for paths using HTTPS.")] String ServerCertificateValidationCallback; + [Write, Description("Ignore a pending reboot if requested by package installation.")] Boolean IgnoreReboot; + [Write, Description("The credential of a user account under which to run the installation or uninstallation of the MSI package."), EmbeddedInstance("MSFT_Credential")] String RunAsCredential; + [Read, Description("The display name of the MSI package.")] String Name; + [Read, Description("The path to the MSI package.")] String InstallSource; + [Read, Description("The date that the MSI package was installed on or serviced on, whichever is later.")] String InstalledOn; + [Read, Description("The size of the MSI package in MB.")] UInt32 Size; + [Read, Description("The version number of the MSI package.")] String Version; + [Read, Description("The description of the MSI package.")] String PackageDescription; + [Read, Description("The publisher of the MSI package.")] String Publisher; +}; diff --git a/deployment/dsc/azshcihost/xPSDesiredStateConfiguration/9.1.0/DSCResources/DSC_xMsiPackage/en-US/DSC_xMsiPackage.schema.mfl b/deployment/dsc/azshcihost/xPSDesiredStateConfiguration/9.1.0/DSCResources/DSC_xMsiPackage/en-US/DSC_xMsiPackage.schema.mfl new file mode 100644 index 0000000..ba42832 --- /dev/null +++ b/deployment/dsc/azshcihost/xPSDesiredStateConfiguration/9.1.0/DSCResources/DSC_xMsiPackage/en-US/DSC_xMsiPackage.schema.mfl @@ -0,0 +1,23 @@ +[Description("The xMsiPackage resource is used to manage MSI files.") : Amended,AMENDMENT, LOCALE("MS_409")] +class DSC_xMsiPackage : OMI_BaseResource +{ + [Key,Description("The identifying number used to uniquely identify this package") : Amended] String ProductId; + [Description("The path, URL or UNC path to the package") : Amended] String Path; + [Description("Indicates whether to Ensure that the package is Present or Absent (default Present)") : Amended] String Ensure; + [Description("The arguments to be passed to the package during addition or removal") : Amended] String Arguments; + [Description("The credentials to be used for mounting the UNC path (if applicable)") : Amended] String Credential; + [Description("The path to log the output of the MSI") : Amended] String LogPath; + [Description("The expected hash value of the file found in the Path location.") : Amended] String FileHash; + [Description("The algorithm used to generate the FileHash value. Defaults to SHA256") : Amended] String HashAlgorithm; + [Description("The subject that must match the signer certificate of the digital signature. Wildcards are allowed.") : Amended] String SignerSubject; + [Description("The certificate thumbprint which must match the signer certificate of the digital signature.") : Amended] String SignerThumbprint; + [Description("PowerShell code used to validate SSL certificates of HTTPS url assigned to Path.") : Amended] String ServerCertificateValidationCallback; + [Description("The credentials under which to run the installation") : Amended] String RunAsCredential; + [Description("The display name of the identified package") : Amended] String Name; + [Description("The path to the identified package") : Amended] String InstallSource; + [Description("The date that the identified package was last serviced or its install date, whichever is later") : Amended] String InstalledOn; + [Description("The size of the identified package in MB") : Amended] UInt32 Size; + [Description("The version number of the identified package") : Amended] String Version; + [Description("The description of the identified package") : Amended] String PackageDescription; + [Description("The publisher for the identified package") : Amended] String Publisher; +}; diff --git a/deployment/dsc/azshcihost/xPSDesiredStateConfiguration/9.1.0/DSCResources/DSC_xMsiPackage/en-US/DSC_xMsiPackage.strings.psd1 b/deployment/dsc/azshcihost/xPSDesiredStateConfiguration/9.1.0/DSCResources/DSC_xMsiPackage/en-US/DSC_xMsiPackage.strings.psd1 new file mode 100644 index 0000000..4a08925 --- /dev/null +++ b/deployment/dsc/azshcihost/xPSDesiredStateConfiguration/9.1.0/DSCResources/DSC_xMsiPackage/en-US/DSC_xMsiPackage.strings.psd1 @@ -0,0 +1,45 @@ +# Localized resources for DSC_xMsiPackage + +ConvertFrom-StringData @' + CheckingFileHash = Checking file '{0}' for expected {2} hash value of {1} + CheckingFileSignature = Checking file '{0}' for valid digital signature + CopyingTheSchemeStreamBytesToTheDiskCache = Copying the stream bytes to the disk cache + CouldNotGetResponseFromWebRequest = An error occurred while trying to get the {0} response for file {1} + CouldNotOpenDestFile = Could not open the file {0} for writing + CouldNotOpenLog = The specified LogPath ({0}) could not be opened + CouldNotStartProcess = The process {0} could not be started + CreatingCacheLocation = Creating cache location + CreatingTheDestinationCacheFile = Creating the destination cache file + CreatingTheSchemeStream = Creating the {0} stream + ErrorCopyingDataToFile = Encountered an error while copying the response to the output stream + FileHasValidSignature = File '{0}' contains a valid digital signature. Signer Thumbprint: {1}, Subject: {2} + GetTargetResourceFound = Successfully retrieved package {0} + GetTargetResourceNotFound = Unable to find package: {0} + GettingTheSchemeResponseStream = Getting the {0} response stream + IgnoreReboot = Ignore a pending reboot + InvalidBinaryType = The specified Path ({0}) does not appear to specify an MSI file and as such is not supported + InvalidFileHash = File '{0}' does not match expected {2} hash value of {1} + InvalidFileSignature = File '{0}' does not have a valid Authenticode signature. Status: {1} + InvalidId = The specified IdentifyingNumber ({0}) does not match the IdentifyingNumber ({1}) in the MSI file + InvalidIdentifyingNumber = The specified IdentifyingNumber ({0}) is not a valid GUID + InvalidPath = The specified Path ({0}) is not in a valid format. Valid formats are local paths, UNC, HTTP, and HTTPS + MachineRequiresReboot = The machine requires a reboot + PackageAppearsInstalled = The package {0} is installed + PackageConfigurationStarting = Package configuration starting + PackageDoesNotAppearInstalled = The package {0} is not installed + PathDoesNotExist = The given Path ({0}) could not be found + PackageInstalled = Package has been installed + PackageUninstalled = Package has been uninstalled + ParsedProductIdAsIdentifyingNumber = Parsed {0} as {1} + ParsingProductIdAsAnIdentifyingNumber = Parsing {0} as an identifyingNumber + PostValidationError = Package from {0} was installed, but the specified ProductId does not match package details + RedirectingPackagePathToCacheFileLocation = Redirecting package path to cache file location + SettingAuthenticationLevel = Setting authentication level to None + SettingCertificateValidationCallback = Assigning user-specified certificate verification callback + SettingDefaultCredential = Setting default credential + StartingWithStartInfoFileNameStartInfoArguments = Starting {0} with {1} + ThePathExtensionWasPathExt = The path extension was {0} + TheUriSchemeWasUriScheme = The uri scheme was {0} + WrongSignerSubject = File '{0}' was not signed by expected signer subject '{1}' + WrongSignerThumbprint = File '{0}' was not signed by expected signer certificate thumbprint '{1}' +'@ diff --git a/deployment/dsc/azshcihost/xPSDesiredStateConfiguration/9.1.0/DSCResources/DSC_xPSSessionConfiguration/DSC_xPSSessionConfiguration.psm1 b/deployment/dsc/azshcihost/xPSDesiredStateConfiguration/9.1.0/DSCResources/DSC_xPSSessionConfiguration/DSC_xPSSessionConfiguration.psm1 new file mode 100644 index 0000000..5a13ac4 --- /dev/null +++ b/deployment/dsc/azshcihost/xPSDesiredStateConfiguration/9.1.0/DSCResources/DSC_xPSSessionConfiguration/DSC_xPSSessionConfiguration.psm1 @@ -0,0 +1,799 @@ +$errorActionPreference = 'Stop' +Set-StrictMode -Version 'Latest' + +$modulePath = Join-Path -Path (Split-Path -Path (Split-Path -Path $PSScriptRoot -Parent) -Parent) -ChildPath 'Modules' + +# Import the shared modules +Import-Module -Name (Join-Path -Path $modulePath ` + -ChildPath (Join-Path -Path 'xPSDesiredStateConfiguration.Common' ` + -ChildPath 'xPSDesiredStateConfiguration.Common.psm1')) + +# Import Localization Strings +$script:localizedData = Get-LocalizedData -ResourceName 'DSC_xPSSessionConfiguration' + +<# + .SYNOPSIS + Returns the current state of the specified PSSessionConfiguration + + .PARAMETER Name + Specifies the name of the session configuration. +#> +function Get-TargetResource +{ + [CmdletBinding()] + [OutputType([System.Collections.Hashtable])] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $Name + ) + + Write-Verbose -Message ($script:localizedData.GetTargetResourceStartMessage -f $Name) + + # Try getting the specified endpoint + $endpoint = Get-PSSessionConfiguration -Name $Name -ErrorAction SilentlyContinue -Verbose:$false + + # If endpoint is null, it is absent + if ($null -eq $endpoint) + { + $ensure = 'Absent' + } + # If endpoint is present, check other properties + else + { + $ensure = 'Present' + + # If runAsUser is specified, return only the username in the credential property + if ($endpoint.RunAsUser) + { + $newCimInstanceParams = @{ + ClassName = 'MSFT_Credential' + + Property = @{ + Username = [System.String] $endpoint.RunAsUser + Password = [System.String] $null + } + + Namespace = 'root/microsoft/windows/desiredstateconfiguration' + ClientOnly = $true + } + + $convertToCimCredential = New-CimInstance @newCimInstanceParams + } + + $accessMode = Get-EndpointAccessMode -Endpoint $endpoint + } + + @{ + Name = $Name + RunAsCredential = [Microsoft.Management.Infrastructure.CimInstance] $convertToCimCredential + SecurityDescriptorSDDL = $endpoint.Permission + StartupScript = $endpoint.StartupScript + AccessMode = $accessMode + Ensure = $ensure + } + + Write-Verbose -Message ($script:localizedData.GetTargetResourceEndMessage -f $Name) +} + +<# + .SYNOPSIS + Ensures the specified PSSessionConfiguration is in its desired state + + .PARAMETER Name + Specifies the name of the session configuration. + + .PARAMETER StartupScript + Specifies the startup script for the configuration. + Enter the fully qualified path of a Windows PowerShell script. + + .PARAMETER RunAsCredential + Specifies the credential for commands of this session configuration. + + .PARAMETER SecurityDescriptorSDDL + Specifies the Security Descriptor Definition Language (SDDL) string for the configuration. + + .PARAMETER AccessMode + Enables and disables the session configuration and determines whether it can be used for + remote or local sessions on the computer. The default value is 'Remote'. + + .PARAMETER Ensure + Indicates if the session configuration should exist. The default value is 'Present'. +#> +function Set-TargetResource +{ + [CmdletBinding(SupportsShouldProcess = $true)] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $Name, + + [Parameter()] + [AllowEmptyString()] + [System.String] + $StartupScript, + + [Parameter()] + [System.Management.Automation.PSCredential] + [System.Management.Automation.Credential()] + $RunAsCredential, + + [Parameter()] + [System.String] + $SecurityDescriptorSDDL, + + [Parameter()] + [ValidateSet('Local', 'Remote', 'Disabled')] + [System.String] + $AccessMode = 'Remote', + + [Parameter()] + [ValidateSet('Present', 'Absent')] + [System.String] + $Ensure = 'Present' + ) + + Write-Verbose -Message ($script:localizedData.SetTargetResourceStartMessage -f $Name) + + # Check if the session configuration exists + Write-Verbose -Message ($script:localizedData.CheckEndpointMessage -f $Name) + + # Try to get a named session configuration + $endpoint = Get-PSSessionConfiguration -Name $Name -ErrorAction SilentlyContinue -Verbose:$false + + if ($PSCmdlet.ShouldProcess(($script:localizedData.EnsureSessionConfigurationMessage -f $Ensure))) + { + # If endpoint is present, set ensure correctly + if ($endpoint) + { + Write-Verbose -Message ($script:localizedData.EndpointNameMessage -f $Name, 'present') + + # If the endpoint should be absent, delete the endpoint + if ($Ensure -eq 'Absent') + { + try + { + <# + Set the following preference so the functions inside Unregister-PSSessionConfig + doesn't get these settings + #> + $oldDebugPrefernce = $DebugPreference + $oldVerbosePreference = $VerbosePreference + $DebugPreference = $VerbosePreference = "SilentlyContinue" + + $unregisterPSSessionConfigParams = @{ + Name = $Name + Force = $true + NoServiceRestart = $true + ErrorAction = 'Stop' + } + + $null = Unregister-PSSessionConfiguration @unregisterPSSessionConfigParams + + # Reset the following preference to older values + $DebugPreference = $oldDebugPrefernce + $VerbosePreference = $oldVerbosePreference + + Write-Verbose -Message ($script:localizedData.EndpointNameMessage -f $Name, 'absent') + + $restartNeeded = $true + } + catch + { + $invokeThrowErrorHelperParams = @{ + ErrorId = 'UnregisterPSSessionConfigurationFailed' + ErrorMessage = $_.Exception + ErrorCategory = 'InvalidOperation' + } + + Invoke-ThrowErrorHelper @invokeThrowErrorHelperParams + } + + } + + # else validate endpoint properties and return the result + else + { + # Remove Name and Ensure from the bound Parameters for splatting + if ($PSBoundParameters.ContainsKey('Name')) + { + $null = $PSBoundParameters.Remove('Name') + } + + if ($PSBoundParameters.ContainsKey('Ensure')) + { + $null = $PSBoundParameters.Remove('Ensure') + } + + [System.Collections.Hashtable] $validatedProperties = ( + Get-ValidatedResourcePropertyTable -Endpoint $endpoint @PSBoundParameters -Apply + ) + $null = $validatedProperties.Add('Name', $Name) + + # If the $validatedProperties contain more than 1 key, something needs to be changed + if ($validatedProperties.count -gt 1) + { + try + { + $setPSSessionConfigurationParams = $validatedProperties.psobject.Copy() + $setPSSessionConfigurationParams['Force'] = $true + $setPSSessionConfigurationParams['NoServiceRestart'] = $true + $setPSSessionConfigurationParams['Verbose'] = $false + $null = Set-PSSessionConfiguration @setPSSessionConfigurationParams + $restartNeeded = $true + + # Write verbose message for all the properties, except Name, that are changing + Write-EndpointMessage -Parameters $validatedProperties -keysToSkip 'Name' + } + catch + { + $invokeThrowErrorHelperParams = @{ + ErrorId = 'SetPSSessionConfigurationFailed' + ErrorMessage = $_.Exception + ErrorCategory = 'InvalidOperation' + } + + Invoke-ThrowErrorHelper @invokeThrowErrorHelperParams + } + } + } + } + else + { + # Named session configuration is absent + Write-Verbose -Message ($script:localizedData.EndpointNameMessage -f $Name, 'absent') + + # If the endpoint should have been present, create it + if ($Ensure -eq 'Present') + { + # Remove Ensure,Verbose,Debug from the bound Parameters for splatting + foreach ($key in @('Ensure', 'Verbose', 'Debug')) + { + if ($PSBoundParameters.ContainsKey($key)) + { + $null = $PSBoundParameters.Remove($key) + } + } + + # Register the endpoint with specified properties + try + { + <# + Set the following preference so the functions inside + Unregister-PSSessionConfig doesn't get these settings + #> + $oldDebugPrefernce = $DebugPreference + $oldVerbosePreference = $VerbosePreference + $DebugPreference = $VerbosePreference = "SilentlyContinue" + + $null = Register-PSSessionConfiguration @PSBoundParameters -Force -NoServiceRestart + + # Reset the following preference to older values + $DebugPreference = $oldDebugPrefernce + $VerbosePreference = $oldVerbosePreference + + # If access mode is specified, set it on the endpoint + if ($PSBoundParameters.ContainsKey('AccessMode') -and $AccessMode -ne 'Remote') + { + $setPSSessionConfigurationParams = @{ + Name = $Name + AccessMode = $AccessMode + Force = $true + NoServiceRestart = $true + Verbose = $false + } + + $null = Set-PSSessionConfiguration @setPSSessionConfigurationParams + } + + $restartNeeded = $true + + Write-Verbose -Message ($script:localizedData.EndpointNameMessage -f $Name, 'present') + } + catch + { + $invokeThrowErrorHelperParams = @{ + ErrorId = 'RegisterOrSetPSSessionConfigurationFailed' + ErrorMessage = $_.Exception + ErrorCategory = 'InvalidOperation' + } + + Invoke-ThrowErrorHelper @invokeThrowErrorHelperParams + } + } + } + + <# + Any change to existing endpoint or creating new endpoint requires WinRM restart. + Since DSC(CIM) uses WSMan as well it will stop responding. + Hence telling the DSC Engine to restart the machine + #> + if ($restartNeeded) + { + Set-DSCMachineRebootRequired + } + } + + Write-Verbose -Message ($script:localizedData.SetTargetResourceEndMessage -f $Name) +} + +<# + .SYNOPSIS + Tests if the specified PSSessionConfiguration is in its desired state + + .PARAMETER Name + Specifies the name of the session configuration. + + .PARAMETER StartupScript + Specifies the startup script for the configuration. + Enter the fully qualified path of a Windows PowerShell script. + + .PARAMETER RunAsCredential + Specifies the credential for commands of this session configuration. + + .PARAMETER SecurityDescriptorSDDL + Specifies the Security Descriptor Definition Language (SDDL) string for the configuration. + + .PARAMETER AccessMode + Enables and disables the session configuration and determines whether it can be used for + remote or local sessions on the computer. The default value is 'Remote'. + + .PARAMETER Ensure + Indicates if the session configuration should exist. The default value is 'Present'. +#> +function Test-TargetResource +{ + [CmdletBinding()] + [OutputType([System.Boolean])] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $Name, + + [Parameter()] + [AllowEmptyString()] + [System.String] + $StartupScript, + + [Parameter()] + [System.Management.Automation.PSCredential] + [System.Management.Automation.Credential()] + $RunAsCredential, + + [Parameter()] + [System.String] + $SecurityDescriptorSDDL, + + [Parameter()] + [ValidateSet('Local', 'Remote', 'Disabled')] + [System.String] + $AccessMode = 'Remote', + + [Parameter()] + [ValidateSet('Present', 'Absent')] + [System.String] + $Ensure = 'Present' + ) + + Write-Verbose -Message ($script:localizedData.TestTargetResourceStartMessage -f $Name) + + #region Input Validation + # Check if the endpoint name is blank/whitespaced string + if ([System.String]::IsNullOrWhiteSpace($Name)) + { + $invokeThrowErrorHelperParams = @{ + ErrorId = 'BlankString' + ErrorMessage = $script:localizedData.WhitespacedStringMessage -f 'name' + ErrorCategory = 'SyntaxError' + } + + Invoke-ThrowErrorHelper @invokeThrowErrorHelperParams + } + + # Check for Startup script path and extension + if ($PSBoundParameters.ContainsKey('StartupScript')) + { + # Check if startup script path is valid + if (!(Test-Path -Path $StartupScript)) + { + $invokeThrowErrorHelperParams = @{ + ErrorId = 'PathNotFound' + ErrorMessage = $script:localizedData.StartupPathNotFoundMessage -f $StartupScript + ErrorCategory = 'ObjectNotFound' + } + + Invoke-ThrowErrorHelper @invokeThrowErrorHelperParams + } + + # Check the startup script extension + $startupScriptFileExtension = $StartupScript.Split('.')[-1] + + if ($startupScriptFileExtension -ne 'ps1') + { + $invokeThrowErrorHelperParams = @{ + ErrorId = 'WrongFileExtension' + ErrorMessage = + $script:localizedData.WrongStartupScriptExtensionMessage -f $startupScriptFileExtension + ErrorCategory = 'InvalidData' + } + + Invoke-ThrowErrorHelper @invokeThrowErrorHelperParams + } + } + + # Check if SecurityDescriptorSDDL is whitespaced + if ($PSBoundParameters.ContainsKey('SecurityDescriptorSDDL') -and + [System.String]::IsNullOrWhiteSpace($SecurityDescriptorSDDL)) + { + $invokeThrowErrorHelperParams = @{ + ErrorId = 'BlankString' + ErrorMessage = $script:localizedData.WhitespacedStringMessage -f 'securityDescriptorSddl' + ErrorCategory = 'SyntaxError' + } + + Invoke-ThrowErrorHelper @invokeThrowErrorHelperParams + } + + # Check if the RunAsCredential is not empty + if ($PSBoundParameters.ContainsKey('RunAsCredential') -and + ($RunAsCredential -eq [System.Management.Automation.PSCredential]::Empty)) + { + $invokeThrowErrorHelperParams = @{ + ErrorId = 'EmptyCredential' + ErrorMessage = $script:localizedData.EmptyCredentialMessage + ErrorCategory = 'InvalidArgument' + } + + Invoke-ThrowErrorHelper @invokeThrowErrorHelperParams + } + #endregion + + # Check if the session configuration exists + Write-Verbose -Message ($script:localizedData.CheckEndpointMessage -f $Name) + + try + { + # Try to get a named session configuration + $endpoint = Get-PSSessionConfiguration -Name $Name -ErrorAction Stop -Verbose:$false + + Write-Verbose -Message ($script:localizedData.EndpointNameMessage -f $Name, 'present') + + # If the endpoint shouldn't be present, return false + if ($Ensure -eq 'Absent') + { + return $false + } + # else validate endpoint properties and return the result + else + { + # Remove Name and Ensure from the bound Parameters for splatting + if ($PSBoundParameters.ContainsKey('Name')) + { + $null = $PSBoundParameters.Remove('Name') + } + + if ($PSBoundParameters.ContainsKey('Ensure')) + { + $null = $PSBoundParameters.Remove('Ensure') + } + + return (Get-ValidatedResourcePropertyTable -Endpoint $endpoint @PSBoundParameters) + } + } + catch [Microsoft.PowerShell.Commands.WriteErrorException] + { + Write-Verbose -Message ($script:localizedData.EndpointNameMessage -f $Name, 'absent') + + return ($Ensure -eq 'Absent') + } + + Write-Verbose -Message ($script:localizedData.TestTargetResourceEndMessage -f $Name) +} + +<# + .SYNOPSIS + Helper function to translate the endpoint's accessmode + to the 'Disabled', 'Local', 'Remote' values + + .PARAMETER Endpoint + Specifies a valid session configuration endpoint object +#> +function Get-EndpointAccessMode +{ + [CmdletBinding()] + [OutputType([System.String])] + param + ( + [Parameter(Mandatory = $true)] + $Endpoint + ) + + if (-not $endpoint.Enabled) + { + return 'Disabled' + } + elseif ($endpoint.Permission -and + ($endpoint.Permission).contains('NT AUTHORITY\NETWORK AccessDenied')) + { + return 'Local' + } + else + { + return 'Remote' + } +} + +<# + .SYNOPSIS + Helper function to write verbose messages for collection of properties + + .PARAMETER Parameters + Specifies a properties Hashtable. + + .PARAMETER KeysToSkip + Specifies an array of Hashtable keys to ignore. +#> +function Write-EndpointMessage +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [System.Collections.Hashtable] + $Parameters, + + [Parameter(Mandatory = $true)] + [System.String[]] + $KeysToSkip + ) + + foreach ($key in $Parameters.keys) + { + if ($KeysToSkip -notcontains $key) + { + Write-Verbose -Message ($script:localizedData.SetPropertyMessage -f $key, $Parameters[$key]) + } + } +} + +<# + .SYNOPSIS + Helper function to get a Hashtable of validated endpoint properties + + .PARAMETER Endpoint + Specifies a valid session configuration endpoint. + + .PARAMETER StartupScript + Specifies the startup script for the configuration. + Enter the fully qualified path of a Windows PowerShell script. + + .PARAMETER RunAsCredential + Specifies the credential for commands of this session configuration. + + .PARAMETER SecurityDescriptorSDDL + Specifies the Security Descriptor Definition Language (SDDL) string for the configuration. + + .PARAMETER AccessMode + Enables and disables the session configuration and determines whether it can be used for + remote or local sessions on the computer. + + The acceptable values for this parameter are: + - Disabled + - Local + - Remote + + .PARAMETER Apply + Indicates that this function should return a hashtable of validated endpoint properties. + By default, this function returns the value $false. +#> +function Get-ValidatedResourcePropertyTable +{ + [CmdletBinding()] + [OutputType([System.Boolean])] + param + ( + [Parameter(Mandatory = $true)] + $Endpoint, + + [Parameter()] + [System.String] + $StartupScript, + + [Parameter()] + [System.Management.Automation.PSCredential] + $RunAsCredential, + + [Parameter()] + [System.String] + $SecurityDescriptorSDDL, + + [Parameter()] + [ValidateSet('Local', 'Remote', 'Disabled')] + [System.String] + $AccessMode, + + [Parameter()] + [System.Management.Automation.SwitchParameter] + $Apply + ) + + if ($Apply) + { + $validatedProperties = @{} + } + + # Check if the SDDL is same as specified + if ($PSBoundParameters.ContainsKey('SecurityDescriptorSDDL')) + { + Write-Verbose -Message ($script:localizedData.CheckPropertyMessage -f 'SDDL', + $SecurityDescriptorSDDL) + + # If endpoint SDDL is not same as specified + if ($endpoint.SecurityDescriptorSddl -and + ($endpoint.SecurityDescriptorSddl -ne $SecurityDescriptorSDDL)) + { + $notDesiredSDDLMessage = $script:localizedData.NotDesiredPropertyMessage -f 'SDDL', + $SecurityDescriptorSDDL, $endpoint.SecurityDescriptorSddl + Write-Verbose -Message $notDesiredSDDLMessage + + if ($Apply) + { + $validatedProperties['SecurityDescriptorSddl'] = $SecurityDescriptorSDDL + } + else + { + return $false + } + } + # If endpoint SDDL is same as specified + else + { + Write-Verbose -Message ($script:localizedData.DesiredPropertyMessage -f 'SDDL', + $SecurityDescriptorSDDL) + } + } + + # Check the RunAs user is same as specified + if ($PSBoundParameters.ContainsKey('RunAsCredential')) + { + Write-Verbose -Message ($script:localizedData.CheckPropertyMessage -f 'RunAs user', + $RunAsCredential.UserName) + + # If endpoint RunAsUser is not same as specified + if ($endpoint.RunAsUser -ne $RunAsCredential.UserName) + { + Write-Verbose -Message ($script:localizedData.NotDesiredPropertyMessage -f 'RunAs user', + $RunAsCredential.UserName, $endpoint.RunAsUser) + + if ($Apply) + { + $validatedProperties['RunAsCredential'] = $RunAsCredential + } + else + { + return $false + } + } + # If endpoint RunAsUser is same as specified + else + { + Write-Verbose -Message ($script:localizedData.DesiredPropertyMessage -f 'RunAs user', + $RunAsCredential.UserName) + } + } + + # Check if the StartupScript is same as specified + if ($PSBoundParameters.ContainsKey('StartupScript')) + { + Write-Verbose -Message ($script:localizedData.CheckPropertyMessage -f 'startup script', + $StartupScript) + + # If endpoint StartupScript is not same as specified + if ($endpoint.StartupScript -ne $StartupScript) + { + Write-Verbose -Message ($script:localizedData.NotDesiredPropertyMessage -f 'startup script', + $StartupScript, $endpoint.StartupScript) + + if ($Apply) + { + $validatedProperties['StartupScript'] = $StartupScript + } + else + { + return $false + } + } + # If endpoint StartupScript is same as specified + else + { + Write-Verbose -Message ($script:localizedData.DesiredPropertyMessage -f 'startup script', + $StartupScript) + } + } + + # Check if AccessMode is same as specified + if ($PSBoundParameters.ContainsKey('AccessMode')) + { + Write-Verbose -Message ($script:localizedData.CheckPropertyMessage -f 'acess mode', $AccessMode) + + $curAccessMode = Get-EndpointAccessMode -Endpoint $Endpoint + + # If endpoint access mode is not same as specified + if ($curAccessMode -ne $AccessMode) + { + Write-Verbose -Message ($script:localizedData.NotDesiredPropertyMessage -f 'access mode', + $AccessMode, $curAccessMode) + + if ($Apply) + { + $validatedProperties['AccessMode'] = $AccessMode + } + else + { + return $false + } + } + # If endpoint access mode is same as specified + else + { + Write-Verbose -Message ($script:localizedData.DesiredPropertyMessage -f 'access mode', + $AccessMode) + } + } + + if ($Apply) + { + return $validatedProperties + } + else + { + return ($Ensure -eq 'Present') + } +} + +<# + .SYNOPSIS + Invoke this helper function to throw a terminating error. + + .PARAMETER ErrorId + Specifies a developer-defined identifier of the error. + This identifier must be a non-localized string for a specific error type. + + .PARAMETER ExceptionMessage + Specifies the message that describes the error. + + .PARAMETER ErrorCategory + Specifies the category of the error. +#> +function Invoke-ThrowErrorHelper +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $ErrorId, + + [Parameter(Mandatory = $true)] + [System.String] + $ErrorMessage, + + [Parameter(Mandatory = $true)] + [System.Management.Automation.ErrorCategory] + $ErrorCategory + ) + + $exception = New-Object System.InvalidOperationException $ErrorMessage + $errorRecord = New-Object System.Management.Automation.ErrorRecord $exception, $ErrorId, + $ErrorCategory, $null + + throw $errorRecord +} + +Export-ModuleMember -Function Get-TargetResource, Set-TargetResource, Test-TargetResource diff --git a/deployment/dsc/azshcihost/xPSDesiredStateConfiguration/9.1.0/DSCResources/DSC_xPSSessionConfiguration/DSC_xPSSessionConfiguration.schema.mof b/deployment/dsc/azshcihost/xPSDesiredStateConfiguration/9.1.0/DSCResources/DSC_xPSSessionConfiguration/DSC_xPSSessionConfiguration.schema.mof new file mode 100644 index 0000000..c5bef7e --- /dev/null +++ b/deployment/dsc/azshcihost/xPSDesiredStateConfiguration/9.1.0/DSCResources/DSC_xPSSessionConfiguration/DSC_xPSSessionConfiguration.schema.mof @@ -0,0 +1,11 @@ +[ClassVersion("1.0.0.0"), FriendlyName("xPSEndpoint")] +class DSC_xPSSessionConfiguration : OMI_BaseResource +{ + [Key, Description("Specifies the name of the session configuration.")] String Name; + [Write, Description("Indicates if the session configuration should exist. The default value is 'Present'."), ValueMap{"Present","Absent"}, Values{"Present","Absent"}] String Ensure; + [Write, Description("Specifies the startup script for the configuration. Enter the fully qualified path of a Windows PowerShell script.")] String StartupScript; + [Write, Description("Specifies the credential for commands of this session configuration."), EmbeddedInstance("MSFT_Credential")] String RunAsCredential; + [Write, Description("Specifies the Security Descriptor Definition Language (SDDL) string for the configuration.")] String SecurityDescriptorSDDL; + [Write, Description("Enables and disables the session configuration and determines whether it can be used for remote or local sessions on the computer. The default value is 'Remote'."), ValueMap{"Local","Remote", "Disabled"}, Values{"Local","Remote","Disabled"}] String AccessMode; +}; + diff --git a/deployment/dsc/azshcihost/xPSDesiredStateConfiguration/9.1.0/DSCResources/DSC_xPSSessionConfiguration/en-US/DSC_xPSSessionConfiguration.strings.psd1 b/deployment/dsc/azshcihost/xPSDesiredStateConfiguration/9.1.0/DSCResources/DSC_xPSSessionConfiguration/en-US/DSC_xPSSessionConfiguration.strings.psd1 new file mode 100644 index 0000000..0e2e680 --- /dev/null +++ b/deployment/dsc/azshcihost/xPSDesiredStateConfiguration/9.1.0/DSCResources/DSC_xPSSessionConfiguration/en-US/DSC_xPSSessionConfiguration.strings.psd1 @@ -0,0 +1,25 @@ +# Localized resources for DSC_xPSSessionConfiguration + +ConvertFrom-StringData @' + CheckEndpointMessage = Checking if session configuration {0} exists ... + EndpointNameMessage = Session configuration {0} is {1} + + CheckPropertyMessage = Checking if session configuration {0} is {1} ... + NotDesiredPropertyMessage = Session configuration {0} is NOT {1}, but {2} + DesiredPropertyMessage = Session configuration {0} is {1} + SetPropertyMessage = Session configuration {0} is now {1} + + WhitespacedStringMessage = The session configuration {0} should not be white-spaced string + StartupPathNotFoundMessage = Startup path {0} not found + EmptyCredentialMessage = The value of RunAsCredential can not be an empty credential + WrongStartupScriptExtensionMessage = The startup script should have a 'ps1' extension, and not '{0}' + + GetTargetResourceStartMessage = Begin executing Get functionality on the session configuration {0}. + GetTargetResourceEndMessage = End executing Get functionality on the session configuration {0}. + SetTargetResourceStartMessage = Begin executing Set functionality on the session configuration {0}. + SetTargetResourceEndMessage = End executing Set functionality on the session configuration {0}. + TestTargetResourceStartMessage = Begin executing Test functionality on the session configuration {0}. + TestTargetResourceEndMessage = End executing Test functionality on the session configuration {0}. + + EnsureSessionConfigurationMessage = Ensure the specified session configuration is "{0}" +'@ diff --git a/deployment/dsc/azshcihost/xPSDesiredStateConfiguration/9.1.0/DSCResources/DSC_xPackageResource/DSC_xPackageResource.psm1 b/deployment/dsc/azshcihost/xPSDesiredStateConfiguration/9.1.0/DSCResources/DSC_xPackageResource/DSC_xPackageResource.psm1 new file mode 100644 index 0000000..743cd4b --- /dev/null +++ b/deployment/dsc/azshcihost/xPSDesiredStateConfiguration/9.1.0/DSCResources/DSC_xPackageResource/DSC_xPackageResource.psm1 @@ -0,0 +1,2054 @@ +[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '')] +param () + +$errorActionPreference = 'Stop' +Set-StrictMode -Version 'Latest' + +$modulePath = Join-Path -Path (Split-Path -Path (Split-Path -Path $PSScriptRoot -Parent) -Parent) -ChildPath 'Modules' + +# Import the shared modules +Import-Module -Name (Join-Path -Path $modulePath ` + -ChildPath (Join-Path -Path 'xPSDesiredStateConfiguration.Common' ` + -ChildPath 'xPSDesiredStateConfiguration.Common.psm1')) + +# Import Localization Strings +$script:localizedData = Get-LocalizedData -ResourceName 'DSC_xPackageResource' + +$script:packageCacheLocation = "$env:programData\Microsoft\Windows\PowerShell\Configuration\BuiltinProvCache\DSC_xPackageResource" +$script:msiTools = $null + +function Get-TargetResource +{ + [OutputType([System.Collections.Hashtable])] + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [AllowEmptyString()] + [System.String] + $Name, + + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $Path, + + [Parameter(Mandatory = $true)] + [AllowEmptyString()] + [System.String] + $ProductId, + + [Parameter()] + [System.Boolean] + $CreateCheckRegValue = $false, + + [Parameter()] + [ValidateSet('LocalMachine', 'CurrentUser')] + [System.String] + $InstalledCheckRegHive = 'LocalMachine', + + [Parameter()] + [System.String] + $InstalledCheckRegKey, + + [Parameter()] + [System.String] + $InstalledCheckRegValueName, + + [Parameter()] + [System.String] + $InstalledCheckRegValueData + ) + + Write-Verbose -Message $script:localizedData.EnteringGetTargetResource + + Assert-PathExtensionValid -Path $Path + + $identifyingNumber = [System.String]::Empty + + if (-not [System.String]::IsNullOrEmpty($ProductId)) + { + $identifyingNumber = Convert-ProductIdToIdentifyingNumber -ProductId $ProductId + } + + $packageResourceResult = @{} + + $getProductEntryParameters = @{ + Name = $Name + IdentifyingNumber = $identifyingNumber + } + + $checkRegistryValueParameters = @{ + CreateCheckRegValue = $CreateCheckRegValue + InstalledCheckRegHive = $InstalledCheckRegHive + InstalledCheckRegKey = $InstalledCheckRegKey + InstalledCheckRegValueName = $InstalledCheckRegValueName + InstalledCheckRegValueData = $InstalledCheckRegValueData + } + + if ($CreateCheckRegValue) + { + Assert-RegistryParametersValid -InstalledCheckRegKey $InstalledCheckRegKey -InstalledCheckRegValueName $InstalledCheckRegValueName -InstalledCheckRegValueData $InstalledCheckRegValueData + + $getProductEntryParameters += $checkRegistryValueParameters + $packageResourceResult += $checkRegistryValueParameters + } + + $productEntry = Get-ProductEntry @getProductEntryParameters + + if ($null -eq $productEntry) + { + $packageResourceResult += @{ + Ensure = 'Absent' + Name = $Name + ProductId = $identifyingNumber + Path = $Path + Installed = $false + } + + return $packageResourceResult + } + elseif ($CreateCheckRegValue) + { + $packageResourceResult += @{ + Ensure = 'Present' + Name = $Name + ProductId = $identifyingNumber + Path = $Path + Installed = $true + } + + return $packageResourceResult + } + + <# + Identifying number can still be null here (e.g. remote MSI with Name specified, local EXE). + If the user gave a product ID just pass it through, otherwise get it from the product. + #> + if ($null -eq $identifyingNumber -and $null -ne $productEntry.Name) + { + $identifyingNumber = Split-Path -Path $productEntry.Name -Leaf + } + + $installDate = $productEntry.GetValue('InstallDate') + + if ($null -ne $installDate) + { + try + { + $installDate = '{0:d}' -f [System.DateTime]::ParseExact($installDate, 'yyyyMMdd', [System.Globalization.CultureInfo]::CurrentCulture).Date + } + catch + { + $installDate = $null + } + } + + $publisher = Get-LocalizedRegistryKeyValue -RegistryKey $productEntry -ValueName 'Publisher' + + $estimatedSize = $productEntry.GetValue('EstimatedSize') + + if ($null -ne $estimatedSize) + { + $estimatedSize = $estimatedSize / 1024 + } + + $displayVersion = $productEntry.GetValue('DisplayVersion') + + $comments = $productEntry.GetValue('Comments') + + $displayName = Get-LocalizedRegistryKeyValue -RegistryKey $productEntry -ValueName 'DisplayName' + + $packageResourceResult += @{ + Ensure = 'Present' + Name = $displayName + Path = $Path + InstalledOn = $installDate + ProductId = $identifyingNumber + Size = $estimatedSize + Installed = $true + Version = $displayVersion + PackageDescription = $comments + Publisher = $publisher + } + + return $packageResourceResult +} + +function Set-TargetResource +{ + [CmdletBinding(SupportsShouldProcess = $true)] + param + ( + [Parameter()] + [ValidateSet('Present', 'Absent')] + [System.String] + $Ensure = 'Present', + + [Parameter(Mandatory = $true)] + [AllowEmptyString()] + [System.String] + $Name, + + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $Path, + + [Parameter(Mandatory = $true)] + [AllowEmptyString()] + [System.String] + $ProductId, + + [Parameter()] + [System.String] + $Arguments, + + [Parameter()] + [System.Management.Automation.PSCredential] + [System.Management.Automation.Credential()] + $Credential, + + # Return codes 1641 and 3010 indicate success when a restart is requested per installation + [Parameter()] + [ValidateNotNullOrEmpty()] + [System.UInt32[]] + $ReturnCode = @( 0, 1641, 3010 ), + + [Parameter()] + [System.String] + $LogPath, + + [Parameter()] + [System.String] + $FileHash, + + [Parameter()] + [ValidateSet('SHA1', 'SHA256', 'SHA384', 'SHA512', 'MD5', 'RIPEMD160')] + [System.String] + $HashAlgorithm, + + [Parameter()] + [System.String] + $SignerSubject, + + [Parameter()] + [System.String] + $SignerThumbprint, + + [Parameter()] + [System.String] + $ServerCertificateValidationCallback, + + [Parameter()] + [System.Boolean] + $CreateCheckRegValue = $false, + + [Parameter()] + [ValidateSet('LocalMachine', 'CurrentUser')] + [System.String] + $InstalledCheckRegHive = 'LocalMachine', + + [Parameter()] + [System.String] + $InstalledCheckRegKey, + + [Parameter()] + [System.String] + $InstalledCheckRegValueName, + + [Parameter()] + [System.String] + $InstalledCheckRegValueData, + + [Parameter()] + [System.Boolean] + $IgnoreReboot = $false, + + [Parameter()] + [System.Management.Automation.PSCredential] + [System.Management.Automation.CredentialAttribute()] + $RunAsCredential + ) + + $ErrorActionPreference = 'Stop' + + if (Test-TargetResource @PSBoundParameters) + { + return + } + + Assert-PathExtensionValid -Path $Path + $uri = Convert-PathToUri -Path $Path + + if (-not [System.String]::IsNullOrEmpty($ProductId)) + { + $identifyingNumber = Convert-ProductIdToIdentifyingNumber -ProductId $ProductId + } + else + { + $identifyingNumber = $ProductId + } + + $productEntry = Get-ProductEntry -Name $Name -IdentifyingNumber $identifyingNumber + + <# + Path gets overwritten in the download code path. Retain the user's original Path in case + the install succeeded but the named package wasn't present on the system afterward so we + can give a better error message. + #> + $originalPath = $Path + + Write-Verbose -Message $script:localizedData.PackageConfigurationStarting + + $logStream = $null + $psDrive = $null + $downloadedFileName = $null + + try + { + $fileExtension = [System.IO.Path]::GetExtension($Path).ToLower() + if (-not [System.String]::IsNullOrEmpty($LogPath)) + { + try + { + if ($fileExtension -eq '.msi') + { + <# + We want to pre-verify the log path exists and is writable ahead of time + even in the MSI case, as detecting WHY the MSI log path doesn't exist would + be rather problematic for the user. + #> + if ((Test-Path -Path $LogPath) -and $PSCmdlet.ShouldProcess($script:localizedData.RemoveExistingLogFile, $null, $null)) + { + Remove-Item -Path $LogPath + } + + if ($PSCmdlet.ShouldProcess($script:localizedData.CreateLogFile, $null, $null)) + { + New-Item -Path $LogPath -Type 'File' | Out-Null + } + } + elseif ($PSCmdlet.ShouldProcess($script:localizedData.CreateLogFile, $null, $null)) + { + $logStream = New-Object -TypeName 'System.IO.StreamWriter' -ArgumentList @( $LogPath, $false ) + } + } + catch + { + New-InvalidOperationException -Message ($script:localizedData.CouldNotOpenLog -f $LogPath) -ErrorRecord $_ + } + } + + # Download or mount file as necessary + if (-not ($fileExtension -eq '.msi' -and $Ensure -eq 'Absent')) + { + if ($uri.IsUnc -and $PSCmdlet.ShouldProcess($script:localizedData.MountSharePath, $null, $null)) + { + $psDriveArgs = @{ + Name = [System.Guid]::NewGuid() + PSProvider = 'FileSystem' + Root = Split-Path -Path $uri.LocalPath + } + + # If we pass a null for Credential, a dialog will pop up. + if ($null -ne $Credential) + { + $psDriveArgs['Credential'] = $Credential + } + + $psDrive = New-PSDrive @psDriveArgs + $Path = Join-Path -Path $psDrive.Root -ChildPath (Split-Path -Path $uri.LocalPath -Leaf) + } + elseif (@( 'http', 'https' ) -contains $uri.Scheme -and $Ensure -eq 'Present' -and $PSCmdlet.ShouldProcess($script:localizedData.DownloadHTTPFile, $null, $null)) + { + $uriScheme = $uri.Scheme + $outStream = $null + $responseStream = $null + + try + { + Write-Verbose -Message ($script:localizedData.CreatingCacheLocation) + + if (-not (Test-Path -Path $script:packageCacheLocation -PathType 'Container')) + { + New-Item -Path $script:packageCacheLocation -ItemType 'Directory' | Out-Null + } + + $destinationPath = Join-Path -Path $script:packageCacheLocation -ChildPath (Split-Path -Path $uri.LocalPath -Leaf) + + Write-Verbose -Message ($script:localizedData.NeedtodownloadfilefromschemedestinationwillbedestName -f $uriScheme, $destinationPath) + + try + { + Write-Verbose -Message ($script:localizedData.CreatingTheDestinationCacheFile) + $outStream = New-Object -TypeName 'System.IO.FileStream' -ArgumentList @( $destinationPath, 'Create' ) + } + catch + { + # Should never happen since we own the cache directory + New-InvalidOperationException -Message ($script:localizedData.CouldNotOpenDestFile -f $destinationPath) -ErrorRecord $_ + } + + try + { + Write-Verbose -Message ($script:localizedData.CreatingTheSchemeStream -f $uriScheme) + $webRequest = [System.Net.WebRequest]::Create($uri) + + Write-Verbose -Message ($script:localizedData.SettingDefaultCredential) + $webRequest.Credentials = [System.Net.CredentialCache]::DefaultCredentials + + if ($uriScheme -eq 'http') + { + # Default value is MutualAuthRequested, which applies to the https scheme + Write-Verbose -Message ($script:localizedData.SettingAuthenticationLevel) + $webRequest.AuthenticationLevel = [System.Net.Security.AuthenticationLevel]::None + } + elseif ($uriScheme -eq 'https' -and -not [System.String]::IsNullOrEmpty($ServerCertificateValidationCallback)) + { + Write-Verbose -Message 'Assigning user-specified certificate verification callback' + $serverCertificateValidationScriptBlock = [System.Management.Automation.ScriptBlock]::Create($ServerCertificateValidationCallback) + $webRequest.ServerCertificateValidationCallBack = $serverCertificateValidationScriptBlock + } + + Write-Verbose -Message ($script:localizedData.Gettingtheschemeresponsestream -f $uriScheme) + $responseStream = (([System.Net.HttpWebRequest] $webRequest).GetResponse()).GetResponseStream() + } + catch + { + Write-Verbose -Message ($script:localizedData.ErrorOutString -f ($_ | Out-String)) + New-InvalidOperationException -Message ($script:localizedData.CouldNotGetHttpStream -f $uriScheme, $Path) -ErrorRecord $_ + } + + try + { + Write-Verbose -Message ($script:localizedData.CopyingTheSchemeStreamBytesToTheDiskCache -f $uriScheme) + $responseStream.CopyTo($outStream) + $responseStream.Flush() + $outStream.Flush() + } + catch + { + New-InvalidOperationException -Message ($script:localizedData.ErrorCopyingDataToFile -f $Path, $destinationPath) -ErrorRecord $_ + } + } + finally + { + if ($null -ne $outStream) + { + $outStream.Close() + } + + if ($null -ne $responseStream) + { + $responseStream.Close() + } + } + + Write-Verbose -Message ($script:localizedData.RedirectingPackagePathToCacheFileLocation) + $Path = $destinationPath + $downloadedFileName = $destinationPath + } + + # At this point the Path ought to be valid unless it's a MSI uninstall case + if (-not (Test-Path -Path $Path -PathType 'Leaf')) + { + New-InvalidOperationException -Message ($script:localizedData.PathDoesNotExist -f $Path) + } + + Assert-FileValid -Path $Path -HashAlgorithm $HashAlgorithm -FileHash $FileHash -SignerSubject $SignerSubject -SignerThumbprint $SignerThumbprint + } + + $startInfo = New-Object -TypeName 'System.Diagnostics.ProcessStartInfo' + + # Necessary for I/O redirection and just generally a good idea + $startInfo.UseShellExecute = $false + + $process = New-Object -TypeName 'System.Diagnostics.Process' + $process.StartInfo = $startInfo + + # Concept only, will never touch disk + $errorLogPath = $LogPath + '.err' + + if ($fileExtension -eq '.msi') + { + $startInfo.FileName = "$env:winDir\system32\msiexec.exe" + + if ($Ensure -eq 'Present') + { + # Check if the MSI package specifies the ProductName and Code + $productName = Get-MsiProductName -Path $Path + $productCode = Get-MsiProductCode -Path $Path + + if ((-not [System.String]::IsNullOrEmpty($Name)) -and ($productName -ne $Name)) + { + New-InvalidArgumentException -ArgumentName 'Name' -Message ($script:localizedData.InvalidNameOrId -f $Name, $identifyingNumber, $productName, $productCode) + } + + if ((-not [System.String]::IsNullOrEmpty($identifyingNumber)) -and ($identifyingNumber -ne $productCode)) + { + New-InvalidArgumentException -ArgumentName 'ProductId' -Message ($script:localizedData.InvalidNameOrId -f $Name, $identifyingNumber, $productName, $productCode) + } + + $startInfo.Arguments = '/i "{0}"' -f $Path + } + else + { + $productEntry = Get-ProductEntry -Name $Name -IdentifyingNumber $identifyingNumber + + # We may have used the Name earlier, now we need the actual ID + $id = Split-Path -Path $productEntry.Name -Leaf + $startInfo.Arguments = '/x{0}' -f $id + } + + if ($LogPath) + { + $startInfo.Arguments += ' /log "{0}"' -f $LogPath + } + + $startInfo.Arguments += ' /quiet /norestart' + + if ($Arguments) + { + # Append any specified arguments with a space (#195) + $startInfo.Arguments += ' {0}' -f $Arguments + } + } + else + { + # EXE + Write-Verbose -Message $script:localizedData.TheBinaryIsAnExe + + if ($Ensure -eq 'Present') + { + $startInfo.FileName = $Path + $startInfo.Arguments = $Arguments + + if ($LogPath) + { + Write-Verbose -Message ($script:localizedData.UserHasRequestedLoggingNeedToAttachEventHandlersToTheProcess) + $startInfo.RedirectStandardError = $true + $startInfo.RedirectStandardOutput = $true + + Register-ObjectEvent -InputObject $process -EventName 'OutputDataReceived' -SourceIdentifier $LogPath + Register-ObjectEvent -InputObject $process -EventName 'ErrorDataReceived' -SourceIdentifier $errorLogPath + } + } + else + { + # Absent case + $startInfo.FileName = "$env:winDir\system32\msiexec.exe" + + # We may have used the Name earlier, now we need the actual ID + if ($null -eq $productEntry -or $null -eq $productEntry.Name) + { + $id = $Path + } + else + { + $id = Split-Path -Path $productEntry.Name -Leaf + } + + $startInfo.Arguments = "/x `"$id`" /quiet /norestart" + + if ($LogPath) + { + $startInfo.Arguments += ' /log "{0}"' -f $LogPath + } + + if ($Arguments) + { + # Append the specified arguments with a space (#195) + $startInfo.Arguments += ' {0}' -f $Arguments + } + } + } + + Write-Verbose -Message ($script:localizedData.StartingWithStartInfoFileNameStartInfoArguments -f $startInfo.FileName, $startInfo.Arguments) + + if ($PSCmdlet.ShouldProcess(($script:localizedData.StartingProcessMessage -f $startInfo.FileName, $startInfo.Arguments), $null, $null)) + { + try + { + [System.Int32] $exitCode = 0 + if ($PSBoundParameters.ContainsKey('RunAsCredential')) + { + $commandLine = '"{0}" {1}' -f $startInfo.FileName, $startInfo.Arguments + $exitCode = Invoke-PInvoke -CommandLine $commandLine -Credential $RunAsCredential + } + else + { + $process = Invoke-Process -Process $process -LogStream ($null -ne $logStream) + $exitCode = $process.ExitCode + } + } + catch + { + New-InvalidOperationException -Message ($script:localizedData.CouldNotStartProcess -f $Path) -ErrorRecord $_ + } + + if ($logStream) + { + <# + We have to re-mux these since they appear to us as different streams + the underlying Win32 APIs prevent this problem, as would constructing a script + on the fly and executing it, but the former is highly problematic from PowerShell + and the latter doesn't let us get the return code for UI-based EXEs + #> + $outputEvents = Get-Event -SourceIdentifier $LogPath + $errorEvents = Get-Event -SourceIdentifier $errorLogPath + $masterEvents = @() + $outputEvents + $errorEvents + $masterEvents = $masterEvents | Sort-Object -Property TimeGenerated + + foreach ($event in $masterEvents) + { + $logStream.Write($event.SourceEventArgs.Data); + } + + Remove-Event -SourceIdentifier $LogPath + Remove-Event -SourceIdentifier $errorLogPath + } + + if (-not ($ReturnCode -contains $exitCode)) + { + # Some .exe files do not support uninstall + if ($Ensure -eq 'Absent' -and $fileExtension -eq '.exe' -and $exitCode -eq '1620') + { + Write-Warning -Message ($script:localizedData.ExeCouldNotBeUninstalled -f $Path) + } + else + { + New-InvalidOperationException ($script:localizedData.UnexpectedReturnCode -f $exitCode.ToString()) + } + } + } + } + finally + { + if ($psDrive) + { + Remove-PSDrive -Name $psDrive -Force + } + + if ($logStream) + { + $logStream.Dispose() + } + } + + if ($downloadedFileName -and $PSCmdlet.ShouldProcess($script:localizedData.RemoveDownloadedFile, $null, $null)) + { + <# + This is deliberately not in the finally block because we want to leave the downloaded + file on disk if an error occurred as a debugging aid for the user. + #> + Remove-Item -Path $downloadedFileName + } + + $operationMessageString = $script:localizedData.PackageUninstalled + if ($Ensure -eq 'Present') + { + $operationMessageString = $script:localizedData.PackageInstalled + } + + if ($CreateCheckRegValue) + { + $registryValueString = '{0}\{1}\{2}' -f $InstalledCheckRegHive, $InstalledCheckRegKey, $InstalledCheckRegValueName + if ($Ensure -eq 'Present') + { + Write-Verbose -Message ($script:localizedData.CreatingRegistryValue -f $registryValueString) + Set-RegistryValue -RegistryHive $InstalledCheckRegHive -Key $InstalledCheckRegKey -Value $InstalledCheckRegValueName -Data $InstalledCheckRegValueData + } + else + { + Write-Verbose ($script:localizedData.RemovingRegistryValue -f $registryValueString) + Remove-RegistryValue -RegistryHive $InstalledCheckRegHive -Key $InstalledCheckRegKey -Value $InstalledCheckRegValueName + } + } + + <# + Check if a reboot is required, if so notify CA. The MSFT_ServerManagerTasks provider is + missing on some client SKUs (worked on both Server and Client Skus in Windows 10). + #> + + $serverFeatureData = Invoke-CimMethod ` + -Name 'GetServerFeature' ` + -Namespace 'root\microsoft\windows\servermanager' ` + -Class 'MSFT_ServerManagerTasks' ` + -Arguments @{ + BatchSize = 256 + } ` + -ErrorAction 'Ignore' + $registryData = Get-ItemProperty ` + -Path 'HKLM:\SYSTEM\CurrentControlSet\Control\Session Manager' ` + -Name 'PendingFileRenameOperations' ` + -ErrorAction 'Ignore' + + if (($serverFeatureData -and $serverFeatureData.RequiresReboot) -or $registryData -or $exitcode -eq 3010 -or $exitcode -eq 1641) + { + Write-Verbose $script:localizedData.MachineRequiresReboot + if ($IgnoreReboot) + { + Write-Verbose $script:localizedData.IgnoreReboot + } + else + { + Set-DSCMachineRebootRequired + } + } + elseif ($Ensure -eq 'Present') + { + $getProductEntryParameters = @{ + Name = $Name + IdentifyingNumber = $identifyingNumber + } + + $checkRegistryValueParameters = @{ + CreateCheckRegValue = $CreateCheckRegValue + InstalledCheckRegHive = $InstalledCheckRegHive + InstalledCheckRegKey = $InstalledCheckRegKey + InstalledCheckRegValueName = $InstalledCheckRegValueName + InstalledCheckRegValueData = $InstalledCheckRegValueData + } + + if ($CreateCheckRegValue) + { + $getProductEntryParameters += $checkRegistryValueParameters + } + + $productEntry = Get-ProductEntry @getProductEntryParameters + + if ($null -eq $productEntry) + { + New-InvalidOperationException -Message ($script:localizedData.PostValidationError -f $originalPath) + } + } + + Write-Verbose -Message $operationMessageString + Write-Verbose -Message $script:localizedData.PackageConfigurationComplete +} + +function Test-TargetResource +{ + [OutputType([System.Boolean])] + [CmdletBinding()] + param + ( + [Parameter()] + [ValidateSet('Present', 'Absent')] + [System.String] + $Ensure = 'Present', + + [Parameter(Mandatory = $true)] + [AllowEmptyString()] + [System.String] + $Name, + + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $Path, + + [Parameter(Mandatory = $true)] + [AllowEmptyString()] + [System.String] + $ProductId, + + [Parameter()] + [System.String] + $Arguments, + + [Parameter()] + [System.Management.Automation.PSCredential] + [System.Management.Automation.Credential()] + $Credential, + + # Return codes 1641 and 3010 indicate success when a restart is requested per installation + [Parameter()] + [ValidateNotNullOrEmpty()] + [System.UInt32[]] + $ReturnCode = @( 0, 1641, 3010 ), + + [Parameter()] + [System.String] + $LogPath, + + [Parameter()] + [System.String] + $FileHash, + + [Parameter()] + [ValidateSet('SHA1', 'SHA256', 'SHA384', 'SHA512', 'MD5', 'RIPEMD160')] + [System.String] + $HashAlgorithm, + + [Parameter()] + [System.String] + $SignerSubject, + + [Parameter()] + [System.String] + $SignerThumbprint, + + [Parameter()] + [System.String] + $ServerCertificateValidationCallback, + + [Parameter()] + [System.Boolean] + $CreateCheckRegValue = $false, + + [Parameter()] + [ValidateSet('LocalMachine', 'CurrentUser')] + [System.String] + $InstalledCheckRegHive = 'LocalMachine', + + [Parameter()] + [System.String] + $InstalledCheckRegKey, + + [Parameter()] + [System.String] + $InstalledCheckRegValueName, + + [Parameter()] + [System.String] + $InstalledCheckRegValueData, + + [Parameter()] + [System.Boolean] + $IgnoreReboot = $false, + + [Parameter()] + [System.Management.Automation.PSCredential] + $RunAsCredential + ) + + Assert-PathExtensionValid -Path $Path + $identifyingNumber = $null + + if (-not [System.String]::IsNullOrEmpty($ProductId)) + { + $identifyingNumber = Convert-ProductIdToIdentifyingNumber -ProductId $ProductId + } + + $getProductEntryParameters = @{ + Name = $Name + IdentifyingNumber = $identifyingNumber + } + + $checkRegistryValueParameters = @{ + CreateCheckRegValue = $CreateCheckRegValue + InstalledCheckRegHive = $InstalledCheckRegHive + InstalledCheckRegKey = $InstalledCheckRegKey + InstalledCheckRegValueName = $InstalledCheckRegValueName + InstalledCheckRegValueData = $InstalledCheckRegValueData + } + + if ($CreateCheckRegValue) + { + Assert-RegistryParametersValid -InstalledCheckRegKey $InstalledCheckRegKey -InstalledCheckRegValueName $InstalledCheckRegValueName -InstalledCheckRegValueData $InstalledCheckRegValueData + $getProductEntryParameters += $checkRegistryValueParameters + } + + $productEntry = Get-ProductEntry @getProductEntryParameters + + Write-Verbose -Message ($script:localizedData.EnsureIsEnsure -f $Ensure) + + if ($null -ne $productEntry) + { + Write-Verbose -Message ($script:localizedData.ProductIsProduct -f $productEntry) + } + else + { + Write-Verbose -Message 'Product installation cannot be determined' + } + + Write-Verbose -Message ($script:localizedData.ProductAsBooleanIs -f [System.Boolean] $productEntry) + + if ($null -ne $productEntry) + { + if ($CreateCheckRegValue) + { + Write-Verbose -Message ($script:localizedData.PackageAppearsInstalled -f $Name) + } + else + { + $displayName = Get-LocalizedRegistryKeyValue -RegistryKey $productEntry -ValueName 'DisplayName' + Write-Verbose -Message ($script:localizedData.PackageAppearsInstalled -f $displayName) + } + } + else + { + $displayName = $null + + if (-not [System.String]::IsNullOrEmpty($Name)) + { + $displayName = $Name + } + else + { + $displayName = $ProductId + } + + Write-Verbose -Message ($script:localizedData.PackageDoesNotAppearInstalled -f $displayName) + } + + return ($null -ne $productEntry -and $Ensure -eq 'Present') -or ($null -eq $productEntry -and $Ensure -eq 'Absent') +} + +<# + .SYNOPSIS + Asserts that the path extension is valid - either .msi or .exe. + + .PARAMETER Path + The path to validate the extension of. +#> +function Assert-PathExtensionValid +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $Path + ) + + $pathExtension = [System.IO.Path]::GetExtension($Path) + Write-Verbose -Message ($script:localizedData.ThePathExtensionWasPathExt -f $pathExtension) + + $validPathExtensions = @( '.msi', '.exe' ) + + if ($validPathExtensions -notcontains $pathExtension.ToLower()) + { + New-InvalidArgumentException -ArgumentName 'Path' -Message ($script:localizedData.InvalidBinaryType -f $Path) + } +} + +<# + .SYNOPSIS + Converts the given path to a URI. + Throws an exception if the path's scheme as a URI is not valid. + + .PARAMETER Path + The path to retrieve as a URI. +#> +function Convert-PathToUri +{ + [OutputType([System.Uri])] + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $Path + ) + + try + { + $uri = [System.Uri] $Path + } + catch + { + New-InvalidArgumentException -ArgumentName 'Path' -Message ($script:localizedData.InvalidPath -f $Path) + } + + $validUriSchemes = @( 'file', 'http', 'https' ) + + if ($validUriSchemes -notcontains $uri.Scheme) + { + Write-Verbose -Message ($script:localizedData.TheUriSchemeWasUriScheme -f $uri.Scheme) + New-InvalidArgumentException -ArgumentName 'Path' -Message ($script:localizedData.InvalidPath -f $Path) + } + + return $uri +} + +<# + .SYNOPSIS + Retrieves the product ID as an identifying number. + + .PARAMETER ProductId + The product id to retrieve as an identifying number. +#> +function Convert-ProductIdToIdentifyingNumber +{ + [OutputType([System.String])] + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $ProductId + ) + + try + { + Write-Verbose -Message ($script:localizedData.ParsingProductIdAsAnIdentifyingNumber -f $ProductId) + $identifyingNumber = '{{{0}}}' -f [System.Guid]::Parse($ProductId).ToString().ToUpper() + + Write-Verbose -Message ($script:localizedData.ParsedProductIdAsIdentifyingNumber -f $ProductId, $identifyingNumber) + return $identifyingNumber + } + catch + { + New-InvalidArgumentException -ArgumentName 'ProductId' -Message ($script:localizedData.InvalidIdentifyingNumber -f $ProductId) + } +} + +<# + .SYNOPSIS + Asserts that the InstalledCheckRegKey, InstalledCheckRegValueName, and + InstalledCheckRegValueData parameter required for retrieving package installation status + from a registry are not null or empty. + + .PARAMETER InstalledCheckRegKey + The InstalledCheckRegKey parameter to check. + + .PARAMETER InstalledCheckRegValueName + The InstalledCheckRegValueName parameter to check. + + .PARAMETER InstalledCheckRegValueData + The InstalledCheckRegValueData parameter to check. + + .NOTES + This could be done with parameter validation. + It is implemented this way to provide a clearer error message. +#> +function Assert-RegistryParametersValid +{ + [CmdletBinding()] + param + ( + [Parameter()] + [System.String] + $InstalledCheckRegKey, + + [Parameter()] + [System.String] + $InstalledCheckRegValueName, + + [Parameter()] + [System.String] + $InstalledCheckRegValueData + ) + + foreach ($parameter in $PSBoundParameters.Keys) + { + if ([System.String]::IsNullOrEmpty($PSBoundParameters[$parameter])) + { + New-InvalidArgumentException -ArgumentName $parameter -Message ($script:localizedData.ProvideParameterForRegistryCheck -f $parameter) + } + } +} + +<# + .SYNOPSIS + Retrieves the product entry for the package with the given name and/or identifying number. + + .PARAMETER Name + The name of the product entry to retrieve. + + .PARAMETER CreateCheckRegValue + Indicates whether or not to retrieve the package installation status from a registry. + + .PARAMETER IdentifyingNumber + The identifying number of the product entry to retrieve. + + .PARAMETER InstalledCheckRegHive + The registry hive to check for package installation status. + + .PARAMETER InstalledCheckRegKey + The registry key to open to check for package installation status. + + .PARAMETER InstalledCheckRegValueName + The registry value name to check for package installation status. + + .PARAMETER InstalledCheckRegValueData + The value to compare against the retrieved registry value to check for package installation. +#> +function Get-ProductEntry +{ + [CmdletBinding()] + param + ( + [Parameter()] + [System.String] + $Name, + + [Parameter()] + [System.String] + $IdentifyingNumber, + + [Parameter()] + [System.Management.Automation.SwitchParameter] + $CreateCheckRegValue, + + [Parameter()] + [ValidateSet('LocalMachine', 'CurrentUser')] + [System.String] + $InstalledCheckRegHive = 'LocalMachine', + + [Parameter()] + [System.String] + $InstalledCheckRegKey, + + [Parameter()] + [System.String] + $InstalledCheckRegValueName, + + [Parameter()] + [System.String] + $InstalledCheckRegValueData + ) + + $uninstallRegistryKey = 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall' + $uninstallRegistryKeyWow64 = 'HKLM:\SOFTWARE\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall' + + $productEntry = $null + + if (-not [System.String]::IsNullOrEmpty($IdentifyingNumber)) + { + $productEntryKeyLocation = Join-Path -Path $uninstallRegistryKey -ChildPath $IdentifyingNumber + $productEntry = Get-Item -Path $productEntryKeyLocation -ErrorAction 'SilentlyContinue' + + if ($null -eq $productEntry) + { + $productEntryKeyLocation = Join-Path -Path $uninstallRegistryKeyWow64 -ChildPath $IdentifyingNumber + $productEntry = Get-Item $productEntryKeyLocation -ErrorAction 'SilentlyContinue' + } + } + else + { + foreach ($registryKeyEntry in (Get-ChildItem -Path @( $uninstallRegistryKey, $uninstallRegistryKeyWow64) -ErrorAction 'Ignore' )) + { + if ($Name -eq (Get-LocalizedRegistryKeyValue -RegistryKey $registryKeyEntry -ValueName 'DisplayName')) + { + $productEntry = $registryKeyEntry + break + } + } + } + + if ($null -eq $productEntry) + { + if ($CreateCheckRegValue) + { + $installValue = $null + + $win32OperatingSystem = Get-CimInstance -ClassName 'Win32_OperatingSystem' -ErrorAction 'SilentlyContinue' + + # If 64-bit OS, check 64-bit registry view first + if ($win32OperatingSystem.OSArchitecture -ieq '64-bit') + { + $installValue = Get-RegistryValueWithErrorsIgnored -Key $InstalledCheckRegKey -Value $InstalledCheckRegValueName -RegistryHive $InstalledCheckRegHive -RegistryView 'Registry64' + } + + if ($null -eq $installValue) + { + $installValue = Get-RegistryValueWithErrorsIgnored -Key $InstalledCheckRegKey -Value $InstalledCheckRegValueName -RegistryHive $InstalledCheckRegHive -RegistryView 'Registry32' + } + + if ($null -ne $installValue) + { + if ($InstalledCheckRegValueData -and $installValue -eq $InstalledCheckRegValueData) + { + $productEntry = @{ + Installed = $true + } + } + } + } + } + + return $productEntry +} + +<# + .SYNOPSIS + Retrieves a value from a registry without throwing errors. + + .PARAMETER Key + The key of the registry to get the value from. + + .PARAMETER Value + The name of the value to retrieve. + + .PARAMETER RegistryHive + The registry hive to retrieve the value from. + + .PARAMETER RegistyView + The registry view to retrieve the value from. +#> +function Get-RegistryValueWithErrorsIgnored +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $Key, + + [Parameter(Mandatory = $true)] + [System.String] + $Value, + + [Parameter(Mandatory = $true)] + [Microsoft.Win32.RegistryHive] + $RegistryHive, + + [Parameter(Mandatory = $true)] + [Microsoft.Win32.RegistryView] + $RegistryView + ) + + $registryValue = $null + + try + { + $baseRegistryKey = [Microsoft.Win32.RegistryKey]::OpenBaseKey($RegistryHive, $RegistryView) + $subRegistryKey = $baseRegistryKey.OpenSubKey($Key) + + if ($null -ne $subRegistryKey) + { + $registryValue = $subRegistryKey.GetValue($Value) + } + } + catch + { + $exceptionText = ($_ | Out-String).Trim() + Write-Verbose -Message "An exception occured while attempting to retrieve a registry value: $exceptionText" + } + + return $registryValue +} + +<# + .SYNOPSIS + Retrieves a localized registry key value. + + .PARAMETER RegistryKey + The registry key to retrieve the value from. + + .PARAMETER ValueName + The name of the value to retrieve. +#> +function Get-LocalizedRegistryKeyValue +{ + [CmdletBinding()] + param + ( + [Parameter()] + [System.Object] + $RegistryKey, + + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $ValueName + ) + + $localizedRegistryKeyValue = $RegistryKey.GetValue('{0}_Localized' -f $ValueName) + + if ($null -eq $localizedRegistryKeyValue) + { + $localizedRegistryKeyValue = $RegistryKey.GetValue($ValueName) + } + + return $localizedRegistryKeyValue +} + +<# + .SYNOPSIS + Asserts that the file at the given path is valid. + + .PARAMETER Path + The path to the file to check. + + .PARAMETER FileHash + The hash that should match the hash of the file. + + .PARAMETER HashAlgorithm + The algorithm to use to retrieve the file hash. + + .PARAMETER SignerThumbprint + The certificate thumbprint that should match the file's signer certificate. + + .PARAMETER SignerSubject + The certificate subject that should match the file's signer certificate. +#> +function Assert-FileValid +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $Path, + + [Parameter()] + [System.String] + $FileHash, + + [Parameter()] + [System.String] + $HashAlgorithm, + + [Parameter()] + [System.String] + $SignerThumbprint, + + [Parameter()] + [System.String] + $SignerSubject + ) + + if (-not [System.String]::IsNullOrEmpty($FileHash)) + { + Assert-FileHashValid -Path $Path -Hash $FileHash -Algorithm $HashAlgorithm + } + + if (-not [System.String]::IsNullOrEmpty($SignerThumbprint) -or -not [System.String]::IsNullOrEmpty($SignerSubject)) + { + Assert-FileSignatureValid -Path $Path -Thumbprint $SignerThumbprint -Subject $SignerSubject + } +} + +<# + .SYNOPSIS + Asserts that the hash of the file at the given path matches the given hash. + + .PARAMETER Path + The path to the file to check the hash of. + + .PARAMETER Hash + The hash to check against. + + .PARAMETER Algorithm + The algorithm to use to retrieve the file's hash. +#> +function Assert-FileHashValid +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $Path, + + [Parameter()] + [Parameter(Mandatory = $true)] + [System.String] + $Hash, + + [Parameter()] + [System.String] + $Algorithm = 'SHA256' + ) + + if ([System.String]::IsNullOrEmpty($Algorithm)) + { + $Algorithm = 'SHA256' + } + + Write-Verbose -Message ($script:localizedData.CheckingFileHash -f $Path, $Hash, $Algorithm) + + $fileHash = Get-FileHash -LiteralPath $Path -Algorithm $Algorithm -ErrorAction 'Stop' + + if ($fileHash.Hash -ne $Hash) + { + throw ($script:localizedData.InvalidFileHash -f $Path, $Hash, $Algorithm) + } +} + +<# + .SYNOPSIS + Asserts that the signature of the file at the given path is valid. + + .PARAMETER Path + The path to the file to check the signature of + + .PARAMETER Thumbprint + The certificate thumbprint that should match the file's signer certificate. + + .PARAMETER Subject + The certificate subject that should match the file's signer certificate. +#> +function Assert-FileSignatureValid +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $Path, + + [Parameter()] + [System.String] + $Thumbprint, + + [Parameter()] + [System.String] + $Subject + ) + + Write-Verbose -Message ($script:localizedData.CheckingFileSignature -f $Path) + + $signature = Get-AuthenticodeSignature -LiteralPath $Path -ErrorAction 'Stop' + + if ($signature.Status -ne [System.Management.Automation.SignatureStatus]::Valid) + { + throw ($script:localizedData.InvalidFileSignature -f $Path, $signature.Status) + } + else + { + Write-Verbose -Message ($script:localizedData.FileHasValidSignature -f $Path, $signature.SignerCertificate.Thumbprint, $signature.SignerCertificate.Subject) + } + + if ($null -ne $Subject -and ($signature.SignerCertificate.Subject -notlike $Subject)) + { + throw ($script:localizedData.WrongSignerSubject -f $Path, $Subject) + } + + if ($null -ne $Thumbprint -and ($signature.SignerCertificate.Thumbprint -ne $Thumbprint)) + { + throw ($script:localizedData.WrongSignerThumbprint -f $Path, $Thumbprint) + } +} + +<# + .SYNOPSIS + Retrieves the name of a product from an msi. + + .PARAMETER Path + The path to the msi to retrieve the name from. +#> +function Get-MsiProductName +{ + [OutputType([System.String])] + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $Path + ) + + $msiTools = Get-MsiTool + + $productName = $msiTools::GetProductName($Path) + + return $productName +} + +<# + .SYNOPSIS + Retrieves the code of a product from an msi. + + .PARAMETER Path + The path to the msi to retrieve the code from. +#> +function Get-MsiProductCode +{ + [OutputType([System.String])] + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $Path + ) + + $msiTools = Get-MsiTool + + $productCode = $msiTools::GetProductCode($Path) + + return $productCode +} + +<# + .SYNOPSIS + Retrieves the MSI tools type. +#> +function Get-MsiTool +{ + [OutputType([System.Type])] + [CmdletBinding()] + param () + + if ($null -ne $script:msiTools) + { + return $script:msiTools + } + + $msiToolsCodeDefinition = @' + [DllImport("msi.dll", CharSet = CharSet.Unicode, PreserveSig = true, SetLastError = true, ExactSpelling = true)] + private static extern UInt32 MsiOpenPackageExW(string szPackagePath, int dwOptions, out IntPtr hProduct); + + [DllImport("msi.dll", CharSet = CharSet.Unicode, PreserveSig = true, SetLastError = true, ExactSpelling = true)] + private static extern uint MsiCloseHandle(IntPtr hAny); + + [DllImport("msi.dll", CharSet = CharSet.Unicode, PreserveSig = true, SetLastError = true, ExactSpelling = true)] + private static extern uint MsiGetPropertyW(IntPtr hAny, string name, StringBuilder buffer, ref int bufferLength); + + private static string GetPackageProperty(string msi, string property) + { + IntPtr MsiHandle = IntPtr.Zero; + try + { + var res = MsiOpenPackageExW(msi, 1, out MsiHandle); + if (res != 0) + { + return null; + } + + int length = 256; + var buffer = new StringBuilder(length); + res = MsiGetPropertyW(MsiHandle, property, buffer, ref length); + return buffer.ToString(); + } + finally + { + if (MsiHandle != IntPtr.Zero) + { + MsiCloseHandle(MsiHandle); + } + } + } + public static string GetProductCode(string msi) + { + return GetPackageProperty(msi, "ProductCode"); + } + + public static string GetProductName(string msi) + { + return GetPackageProperty(msi, "ProductName"); + } +'@ + + if (([System.Management.Automation.PSTypeName]'Microsoft.Windows.DesiredStateConfiguration.xPackageResource.MsiTools').Type) + { + $script:msiTools = ([System.Management.Automation.PSTypeName]'Microsoft.Windows.DesiredStateConfiguration.xPackageResource.MsiTools').Type + } + else + { + $script:msiTools = Add-Type ` + -Namespace 'Microsoft.Windows.DesiredStateConfiguration.xPackageResource' ` + -Name 'MsiTools' ` + -Using 'System.Text' ` + -MemberDefinition $msiToolsCodeDefinition ` + -PassThru + } + + return $script:msiTools +} + +<# + .SYNOPSIS + Runs a process as the specified user via PInvoke. + + .PARAMETER CommandLine + The command line (including arguments) of the process to start. + + .PARAMETER Credential + The user credential to start the process as. +#> +function Invoke-PInvoke +{ + [CmdletBinding()] + [OutputType([System.Int32])] + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $CommandLine, + + [Parameter(Mandatory = $true)] + [System.Management.Automation.PSCredential] + [System.Management.Automation.CredentialAttribute()] + $Credential + ) + + Register-PInvoke + [System.Int32] $exitCode = 0 + + [Source.NativeMethods]::CreateProcessAsUser($CommandLine, ` + $Credential.GetNetworkCredential().Domain, ` + $Credential.GetNetworkCredential().UserName, ` + $Credential.GetNetworkCredential().Password, ` + [ref] $exitCode + ) + + return $exitCode; +} + +<# + .SYNOPSIS + Starts and waits for a process. + + .DESCRIPTION + Allows mocking and testing of process arguments. + + .PARAMETER Process + The System.Diagnositics.Process object to start. + + .PARAMETER LogStream + Redirect STDOUT and STDERR output. +#> +function Invoke-Process +{ + [CmdletBinding()] + [OutputType([System.Diagnostics.Process])] + param ( + [Parameter(Mandatory = $true)] + [System.Diagnostics.Process] + $Process, + + [Parameter()] + [System.Boolean] + $LogStream + ) + + $Process.Start() | Out-Null + + if ($LogStream) + { + $Process.BeginOutputReadLine() + $Process.BeginErrorReadLine() + } + + $Process.WaitForExit() + return $Process +} + +<# + .SYNOPSIS + Sets the value of a registry key to the specified data. + + .PARAMETER Key + The registry key that contains the value to set. + + .PARAMETER Value + The value name of the registry key value to set. + + .PARAMETER RegistryHive + The registry hive that contains the registry key to set. + + .PARAMETER Data + The data to set the registry key value to. +#> +function Set-RegistryValue +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $Key, + + [Parameter(Mandatory = $true)] + [System.String] + $Value, + + [Parameter(Mandatory = $true)] + [Microsoft.Win32.RegistryHive] + $RegistryHive, + + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $Data + ) + + try + { + $baseRegistryKey = [Microsoft.Win32.RegistryKey]::OpenBaseKey($RegistryHive, [Microsoft.Win32.RegistryView]::Default) + + # Opens the subkey with write access + $subRegistryKey = $baseRegistryKey.OpenSubKey($Key, $true) + + if ($null -eq $subRegistryKey) + { + Write-Verbose "Key: '$Key'" + $subRegistryKey = $baseRegistryKey.CreateSubKey($Key) + } + + $subRegistryKey.SetValue($Value, $Data) + $subRegistryKey.Close() + } + catch + { + New-InvalidOperationException -Message ($script:localizedData.ErrorSettingRegistryValue -f $Key, $Value, $Data) -ErrorRecord $_ + } +} + +<# + .SYNOPSIS + Removes the specified value of a registry key. + + .PARAMETER Key + The registry key that contains the value to remove. + + .PARAMETER Value + The value name of the registry key value to remove. + + .PARAMETER RegistryHive + The registry hive that contains the registry key to remove. +#> +function Remove-RegistryValue +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $Key, + + [Parameter(Mandatory = $true)] + [System.String] + $Value, + + [Parameter(Mandatory = $true)] + [Microsoft.Win32.RegistryHive] + $RegistryHive + ) + + try + { + $baseRegistryKey = [Microsoft.Win32.RegistryKey]::OpenBaseKey($RegistryHive, [Microsoft.Win32.RegistryView]::Default) + + $subRegistryKey = $baseRegistryKey.OpenSubKey($Key, $true) + $subRegistryKey.DeleteValue($Value) + $subRegistryKey.Close() + } + catch + { + New-InvalidOperationException -Message ($script:localizedData.ErrorRemovingRegistryValue -f $Key, $Value) -ErrorRecord $_ + } +} + +<# + .SYNOPSIS + Registers PInvoke to run a process as a user. +#> +function Register-PInvoke +{ + $programSource = @' + using System; + using System.Collections.Generic; + using System.Text; + using System.Security; + using System.Runtime.InteropServices; + using System.Diagnostics; + using System.Security.Principal; + using System.ComponentModel; + using System.IO; + + namespace Source + { + [SuppressUnmanagedCodeSecurity] + public static class NativeMethods + { + //The following structs and enums are used by the various Win32 API's that are used in the code below + + [StructLayout(LayoutKind.Sequential)] + public struct STARTUPINFO + { + public Int32 cb; + public string lpReserved; + public string lpDesktop; + public string lpTitle; + public Int32 dwX; + public Int32 dwY; + public Int32 dwXSize; + public Int32 dwXCountChars; + public Int32 dwYCountChars; + public Int32 dwFillAttribute; + public Int32 dwFlags; + public Int16 wShowWindow; + public Int16 cbReserved2; + public IntPtr lpReserved2; + public IntPtr hStdInput; + public IntPtr hStdOutput; + public IntPtr hStdError; + } + + [StructLayout(LayoutKind.Sequential)] + public struct PROCESS_INFORMATION + { + public IntPtr hProcess; + public IntPtr hThread; + public Int32 dwProcessID; + public Int32 dwThreadID; + } + + [Flags] + public enum LogonType + { + LOGON32_LOGON_INTERACTIVE = 2, + LOGON32_LOGON_NETWORK = 3, + LOGON32_LOGON_BATCH = 4, + LOGON32_LOGON_SERVICE = 5, + LOGON32_LOGON_UNLOCK = 7, + LOGON32_LOGON_NETWORK_CLEARTEXT = 8, + LOGON32_LOGON_NEW_CREDENTIALS = 9 + } + + [Flags] + public enum LogonProvider + { + LOGON32_PROVIDER_DEFAULT = 0, + LOGON32_PROVIDER_WINNT35, + LOGON32_PROVIDER_WINNT40, + LOGON32_PROVIDER_WINNT50 + } + [StructLayout(LayoutKind.Sequential)] + public struct SECURITY_ATTRIBUTES + { + public Int32 Length; + public IntPtr lpSecurityDescriptor; + public bool bInheritHandle; + } + + public enum SECURITY_IMPERSONATION_LEVEL + { + SecurityAnonymous, + SecurityIdentification, + SecurityImpersonation, + SecurityDelegation + } + + public enum TOKEN_TYPE + { + TokenPrimary = 1, + TokenImpersonation + } + + [StructLayout(LayoutKind.Sequential, Pack = 1)] + internal struct TokPriv1Luid + { + public int Count; + public long Luid; + public int Attr; + } + + public const int GENERIC_ALL_ACCESS = 0x10000000; + public const int CREATE_NO_WINDOW = 0x08000000; + internal const int SE_PRIVILEGE_ENABLED = 0x00000002; + internal const int TOKEN_QUERY = 0x00000008; + internal const int TOKEN_ADJUST_PRIVILEGES = 0x00000020; + internal const string SE_INCRASE_QUOTA = "SeIncreaseQuotaPrivilege"; + + [DllImport("kernel32.dll", + EntryPoint = "CloseHandle", SetLastError = true, + CharSet = CharSet.Auto, CallingConvention = CallingConvention.StdCall)] + public static extern bool CloseHandle(IntPtr handle); + + [DllImport("advapi32.dll", + EntryPoint = "CreateProcessAsUser", SetLastError = true, + CharSet = CharSet.Ansi, CallingConvention = CallingConvention.StdCall)] + public static extern bool CreateProcessAsUser( + IntPtr hToken, + string lpApplicationName, + string lpCommandLine, + ref SECURITY_ATTRIBUTES lpProcessAttributes, + ref SECURITY_ATTRIBUTES lpThreadAttributes, + bool bInheritHandle, + Int32 dwCreationFlags, + IntPtr lpEnvrionment, + string lpCurrentDirectory, + ref STARTUPINFO lpStartupInfo, + ref PROCESS_INFORMATION lpProcessInformation + ); + + [DllImport("advapi32.dll", EntryPoint = "DuplicateTokenEx")] + public static extern bool DuplicateTokenEx( + IntPtr hExistingToken, + Int32 dwDesiredAccess, + ref SECURITY_ATTRIBUTES lpThreadAttributes, + Int32 ImpersonationLevel, + Int32 dwTokenType, + ref IntPtr phNewToken + ); + + [DllImport("advapi32.dll", CharSet = CharSet.Unicode, SetLastError = true)] + public static extern Boolean LogonUser( + String lpszUserName, + String lpszDomain, + String lpszPassword, + LogonType dwLogonType, + LogonProvider dwLogonProvider, + out IntPtr phToken + ); + + [DllImport("advapi32.dll", ExactSpelling = true, SetLastError = true)] + internal static extern bool AdjustTokenPrivileges( + IntPtr htok, + bool disall, + ref TokPriv1Luid newst, + int len, + IntPtr prev, + IntPtr relen + ); + + [DllImport("kernel32.dll", ExactSpelling = true)] + internal static extern IntPtr GetCurrentProcess(); + + [DllImport("advapi32.dll", ExactSpelling = true, SetLastError = true)] + internal static extern bool OpenProcessToken( + IntPtr h, + int acc, + ref IntPtr phtok + ); + + [DllImport("kernel32.dll", ExactSpelling = true)] + internal static extern int WaitForSingleObject( + IntPtr h, + int milliseconds + ); + + [DllImport("kernel32.dll", ExactSpelling = true)] + internal static extern bool GetExitCodeProcess( + IntPtr h, + out int exitcode + ); + + [DllImport("advapi32.dll", SetLastError = true)] + internal static extern bool LookupPrivilegeValue( + string host, + string name, + ref long pluid + ); + + public static void CreateProcessAsUser(string strCommand, string strDomain, string strName, string strPassword, ref int ExitCode ) + { + var hToken = IntPtr.Zero; + var hDupedToken = IntPtr.Zero; + TokPriv1Luid tp; + var pi = new PROCESS_INFORMATION(); + var sa = new SECURITY_ATTRIBUTES(); + sa.Length = Marshal.SizeOf(sa); + Boolean bResult = false; + try + { + bResult = LogonUser( + strName, + strDomain, + strPassword, + LogonType.LOGON32_LOGON_BATCH, + LogonProvider.LOGON32_PROVIDER_DEFAULT, + out hToken + ); + if (!bResult) + { + throw new Win32Exception("Logon error #" + Marshal.GetLastWin32Error().ToString()); + } + IntPtr hproc = GetCurrentProcess(); + IntPtr htok = IntPtr.Zero; + bResult = OpenProcessToken( + hproc, + TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY, + ref htok + ); + if(!bResult) + { + throw new Win32Exception("Open process token error #" + Marshal.GetLastWin32Error().ToString()); + } + tp.Count = 1; + tp.Luid = 0; + tp.Attr = SE_PRIVILEGE_ENABLED; + bResult = LookupPrivilegeValue( + null, + SE_INCRASE_QUOTA, + ref tp.Luid + ); + if(!bResult) + { + throw new Win32Exception("Lookup privilege error #" + Marshal.GetLastWin32Error().ToString()); + } + bResult = AdjustTokenPrivileges( + htok, + false, + ref tp, + 0, + IntPtr.Zero, + IntPtr.Zero + ); + if(!bResult) + { + throw new Win32Exception("Token elevation error #" + Marshal.GetLastWin32Error().ToString()); + } + + bResult = DuplicateTokenEx( + hToken, + GENERIC_ALL_ACCESS, + ref sa, + (int)SECURITY_IMPERSONATION_LEVEL.SecurityIdentification, + (int)TOKEN_TYPE.TokenPrimary, + ref hDupedToken + ); + if(!bResult) + { + throw new Win32Exception("Duplicate Token error #" + Marshal.GetLastWin32Error().ToString()); + } + var si = new STARTUPINFO(); + si.cb = Marshal.SizeOf(si); + si.lpDesktop = ""; + bResult = CreateProcessAsUser( + hDupedToken, + null, + strCommand, + ref sa, + ref sa, + false, + 0, + IntPtr.Zero, + null, + ref si, + ref pi + ); + if(!bResult) + { + throw new Win32Exception("Create process as user error #" + Marshal.GetLastWin32Error().ToString()); + } + + int status = WaitForSingleObject(pi.hProcess, -1); + if(status == -1) + { + throw new Win32Exception("Wait during create process failed user error #" + Marshal.GetLastWin32Error().ToString()); + } + + bResult = GetExitCodeProcess(pi.hProcess, out ExitCode); + if(!bResult) + { + throw new Win32Exception("Retrieving status error #" + Marshal.GetLastWin32Error().ToString()); + } + } + finally + { + if (pi.hThread != IntPtr.Zero) + { + CloseHandle(pi.hThread); + } + if (pi.hProcess != IntPtr.Zero) + { + CloseHandle(pi.hProcess); + } + if (hDupedToken != IntPtr.Zero) + { + CloseHandle(hDupedToken); + } + } + } + } + } +'@ + Add-Type -TypeDefinition $programSource -ReferencedAssemblies 'System.ServiceProcess' +} + +Export-ModuleMember -Function *-TargetResource diff --git a/deployment/dsc/azshcihost/xPSDesiredStateConfiguration/9.1.0/DSCResources/DSC_xPackageResource/DSC_xPackageResource.schema.mof b/deployment/dsc/azshcihost/xPSDesiredStateConfiguration/9.1.0/DSCResources/DSC_xPackageResource/DSC_xPackageResource.schema.mof new file mode 100644 index 0000000..e81147a --- /dev/null +++ b/deployment/dsc/azshcihost/xPSDesiredStateConfiguration/9.1.0/DSCResources/DSC_xPackageResource/DSC_xPackageResource.schema.mof @@ -0,0 +1,30 @@ +[ClassVersion("1.0.0"),FriendlyName("xPackage")] +class DSC_xPackageResource : OMI_BaseResource +{ + [Write,ValueMap{"Present", "Absent"},Values{"Present", "Absent"}] string Ensure; + [Key] string Name; + [Required] string Path; + [Key] string ProductId; + [Write] string Arguments; + [Write,EmbeddedInstance("MSFT_Credential")] string Credential; + [Write] uint32 ReturnCode[]; + [Write] string LogPath; + [Read] string PackageDescription; + [Read] string Publisher; + [Read] string InstalledOn; + [Read] uint32 Size; + [Read] string Version; + [Read] boolean Installed; + [Write] string FileHash; + [Write,ValueMap{"SHA1","SHA256","SHA384","SHA512","MD5","RIPEMD160"},Values{"SHA1","SHA256","SHA384","SHA512","MD5","RIPEMD160"}] string HashAlgorithm; + [Write] string SignerSubject; + [Write] string SignerThumbprint; + [Write] string ServerCertificateValidationCallback; + [Write,ValueMap{"LocalMachine","CurrentUser"},Values{"LocalMachine","CurrentUser"}] string InstalledCheckRegHive; + [Write] string InstalledCheckRegKey; + [Write] string InstalledCheckRegValueName; + [Write] string InstalledCheckRegValueData; + [Write] boolean CreateCheckRegValue; + [Write, Description("Ignore a pending reboot if requested by package installation.")] Boolean IgnoreReboot; + [Write,EmbeddedInstance("MSFT_Credential")] string RunAsCredential; +}; diff --git a/deployment/dsc/azshcihost/xPSDesiredStateConfiguration/9.1.0/DSCResources/DSC_xPackageResource/en-US/DSC_xPackageResource.schema.mfl b/deployment/dsc/azshcihost/xPSDesiredStateConfiguration/9.1.0/DSCResources/DSC_xPackageResource/en-US/DSC_xPackageResource.schema.mfl new file mode 100644 index 0000000..454e1e9 --- /dev/null +++ b/deployment/dsc/azshcihost/xPSDesiredStateConfiguration/9.1.0/DSCResources/DSC_xPackageResource/en-US/DSC_xPackageResource.schema.mfl @@ -0,0 +1,32 @@ +#pragma namespace("\\\\.\\root\\default") +instance of __namespace{ name="MS_409";}; +#pragma namespace("\\\\.\\root\\default\\MS_409") + +[AMENDMENT, LOCALE("MS_409")] +class DSC_xPackageResource : OMI_BaseResource +{ + [Description("Indicates whether to Ensure that the package is Present or Absent (default Present)") : Amended] string Ensure; + [Key,Description("The name of the package to be added or removed") : Amended] string Name; + [Description("The path, URL or UNC path to the package") : Amended] string Path; + [Key,Description("The identifying number used to uniquely identify this package") : Amended] string ProductId; + [Description("The arguments to be passed to the package during addition or removal") : Amended] string Arguments; + [Description("The credentials to be used for mounting the UNC path (if applicable)") : Amended] string Credential; + [Description("The list of possible valid return codes for this install or removal") : Amended] uint32 ReturnCode[]; + [Description("The path to log the output of the MSI or EXE") : Amended] string LogPath; + [Description("The description of the identified package") : Amended] string PackageDescription; + [Description("The publisher for the identified package") : Amended] string Publisher; + [Description("The date that the identified package was last serviced or its install date, whichever is later") : Amended] string InstalledOn; + [Description("The size of the identified package") : Amended] uint32 Size; + [Description("The version number of the identified package") : Amended] string Version; + [Description("Whether the identified package is installed") : Amended] boolean Installed; + [Description("The credentials under which to run the installation") : Amended] string RunAsCredential; + [Description("The expected hash value of the file found in the Path location.") : Amended] string FileHash; + [Description("The algorithm used to generate the FileHash value. Defaults to SHA256") : Amended] string HashAlgorithm; + [Description("The subject that must match the signer certificate of the digital signature. Wildcards are allowed.") : Amended] string SignerSubject; + [Description("The certificate thumbprint which must match the signer certificate of the digital signature.") : Amended] string SignerThumbprint; + [Description("PowerShell code used to validate SSL certificates of HTTPS url assigned to Path.") : Amended] string ServerCertificateValidationCallback; + [Description("The Registry key to validate the package is installed") : Amended] string InstalledCheckRegKey; + [Description("The Registry value name to validate the package is installed") : Amended] string InstalledCheckRegValueName; + [Description("The Registry value to validate the package is installed") : Amended] string InstalledCheckRegValueData; +}; + diff --git a/deployment/dsc/azshcihost/xPSDesiredStateConfiguration/9.1.0/DSCResources/DSC_xPackageResource/en-US/DSC_xPackageResource.strings.psd1 b/deployment/dsc/azshcihost/xPSDesiredStateConfiguration/9.1.0/DSCResources/DSC_xPackageResource/en-US/DSC_xPackageResource.strings.psd1 new file mode 100644 index 0000000..06c8215 --- /dev/null +++ b/deployment/dsc/azshcihost/xPSDesiredStateConfiguration/9.1.0/DSCResources/DSC_xPackageResource/en-US/DSC_xPackageResource.strings.psd1 @@ -0,0 +1,64 @@ +# Localized resources for DSC_xPackageResource + +ConvertFrom-StringData @' + InvalidIdentifyingNumber = The specified IdentifyingNumber '{0}' is not a valid Guid. + InvalidPath = The specified Path '{0}' is not in a valid format. Valid formats are local paths, UNC, and HTTP. + InvalidNameOrId = The specified Name '{0}' and IdentifyingNumber '{1}' do not match Name '{2}' and IdentifyingNumber '{3}' in the MSI file. + InvalidBinaryType = The specified Path '{0}' does not appear to specify an EXE or MSI file and as such is not supported. + CouldNotOpenLog = The specified LogPath '{0}' could not be opened. + CouldNotStartProcess = The process '{0}' could not be started. + UnexpectedReturnCode = The return code '{0}' was not expected. Configuration is likely not correct. + PathDoesNotExist = The given Path '{0}' could not be found. + CouldNotOpenDestFile = Could not open the file '{0}' for writing. + CouldNotGetHttpStream = Could not get the '{0}' stream for file '{1}'. + ErrorCopyingDataToFile = Encountered error while writing the contents of '{0}' to '{1}'. + PackageConfigurationComplete = Package configuration finished. + PackageConfigurationStarting = Package configuration starting. + RemoveExistingLogFile = Remove existing log file. + CreateLogFile = Create log file. + MountSharePath = Mount share to get media. + DownloadHTTPFile = Download the media over HTTP or HTTPS. + StartingProcessMessage = Starting process '{0}' with arguments '{1}'. + RemoveDownloadedFile = Remove the downloaded file. + PackageInstalled = Package has been installed. + PackageUninstalled = Package has been uninstalled. + MachineRequiresReboot = The machine requires a reboot. + IgnoreReboot = Ignore a pending reboot. + PackageDoesNotAppearInstalled = The package '{0}' is not installed. + PackageAppearsInstalled = The package '{0}' is installed. + PostValidationError = Package from '{0}' was installed, but the specified ProductId and/or Name does not match package details. + CheckingFileHash = Checking file '{0}' for expected '{2}' hash value of '{1}'. + InvalidFileHash = File '{0}' does not match expected '{2}' hash value of '{1}'. + CheckingFileSignature = Checking file '{0}' for valid digital signature. + FileHasValidSignature = File '{0}' contains a valid digital signature. Signer Thumbprint: '{1}', Subject: '{2}'. + InvalidFileSignature = File '{0}' does not have a valid Authenticode signature. Status: '{1}'. + WrongSignerSubject = File '{0}' was not signed by expected signer subject '{1}'. + WrongSignerThumbprint = File '{0}' was not signed by expected signer certificate thumbprint '{1}'. + CreatingRegistryValue = Creating package registry value of '{0}'. + RemovingRegistryValue = Removing package registry value of '{0}'. + TheurischemewasuriScheme = The uri scheme was '{0}'. + ThepathextensionwaspathExt = The path extension was '{0}'. + ParsingProductIdasanidentifyingNumber = Parsing '{0}' as an identifyingNumber. + ParsedProductIdasidentifyingNumber = Parsed '{0}' as '{1}'. + EnsureisEnsure = Ensure is '{0}'. + productisproduct = product '{0}' found. + productasbooleanis = product as boolean is '{0}'. + Creatingcachelocation = Creating cache location. + NeedtodownloadfilefromschemedestinationwillbedestName = Need to download file from '{0}', destination will be '{1}'. + Creatingthedestinationcachefile = Creating the destination cache file. + Creatingtheschemestream = Creating the '{0}' stream. + Settingdefaultcredential = Setting default credential. + Settingauthenticationlevel = Setting authentication level. + Gettingtheschemeresponsestream = Getting the '{0}' response stream. + ErrorOutString = Error: {0}. + Copyingtheschemestreambytestothediskcache = Copying the '{0}' stream bytes to the disk cache. + Redirectingpackagepathtocachefilelocation = Redirecting package path to cache file location. + ThebinaryisanEXE = The binary is an EXE. + Userhasrequestedloggingneedtoattacheventhandlerstotheprocess = User has requested logging, need to attach event handlers to the process. + StartingwithstartInfoFileNamestartInfoArguments = Starting '{0}' with '{1}'. + ProvideParameterForRegistryCheck = Please provide the '{0}' parameter in order to check for installation status from a registry key. + ErrorSettingRegistryValue = An error occured while attempting to set the registry key '{0}' value '{1}' to '{2}'. + ErrorRemovingRegistryValue = An error occured while attempting to remove the registry key '{0}' value '{1}'. + ExeCouldNotBeUninstalled = The .exe file found at '{0}' could not be uninstalled. The uninstall functionality may not be implemented in this .exe file. + EnteringGetTargetResource = Entering Get-TargetResource in file DSC_xPackageResource.psm1. +'@ diff --git a/deployment/dsc/azshcihost/xPSDesiredStateConfiguration/9.1.0/DSCResources/DSC_xRegistryResource/DSC_xRegistryResource.psm1 b/deployment/dsc/azshcihost/xPSDesiredStateConfiguration/9.1.0/DSCResources/DSC_xRegistryResource/DSC_xRegistryResource.psm1 new file mode 100644 index 0000000..eca2357 --- /dev/null +++ b/deployment/dsc/azshcihost/xPSDesiredStateConfiguration/9.1.0/DSCResources/DSC_xRegistryResource/DSC_xRegistryResource.psm1 @@ -0,0 +1,1536 @@ +$errorActionPreference = 'Stop' +Set-StrictMode -Version 'Latest' + +$modulePath = Join-Path -Path (Split-Path -Path (Split-Path -Path $PSScriptRoot -Parent) -Parent) -ChildPath 'Modules' + +# Import the shared modules +Import-Module -Name (Join-Path -Path $modulePath ` + -ChildPath (Join-Path -Path 'xPSDesiredStateConfiguration.Common' ` + -ChildPath 'xPSDesiredStateConfiguration.Common.psm1')) + +# Import Localization Strings +$script:localizedData = Get-LocalizedData -ResourceName 'DSC_xRegistryResource' + +$script:registryDriveRoots = @{ + 'HKCC' = 'HKEY_CURRENT_CONFIG' + 'HKCR' = 'HKEY_CLASSES_ROOT' + 'HKCU' = 'HKEY_CURRENT_USER' + 'HKLM' = 'HKEY_LOCAL_MACHINE' + 'HKUS' = 'HKEY_USERS' +} + +<# + .SYNOPSIS + Retrieves the current state of the Registry resource with the given Key. + + .PARAMETER Key + The path of the registry key to retrieve the state of. + This path must include the registry hive. + + .PARAMETER ValueName + The name of the registry value to retrieve the state of. + + .PARAMETER ValueData + Used only as a boolean flag (along with ValueType) to determine if the target entity is the + Default Value or the key itself. + + .PARAMETER ValueType + Used only as a boolean flag (along with ValueData) to determine if the target entity is the + Default Value or the key itself. +#> +function Get-TargetResource +{ + [CmdletBinding()] + [OutputType([System.Collections.Hashtable])] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $Key, + + [Parameter(Mandatory = $true)] + [ValidateNotNull()] + [System.String] + [AllowEmptyString()] + $ValueName, + + [Parameter()] + [System.String[]] + $ValueData, + + [Parameter()] + [ValidateSet('String', 'Binary', 'DWord', 'QWord', 'MultiString', 'ExpandString')] + [System.String] + $ValueType + ) + + Write-Verbose -Message ($script:localizedData.GetTargetResourceStartMessage -f $Key) + + $registryResource = @{ + Key = $Key + Ensure = 'Absent' + ValueName = $null + ValueType = $null + ValueData = $null + } + + # Retrieve the registry key at the specified path + $registryKey = Get-RegistryKey -RegistryKeyPath $Key + + # Check if the registry key exists + if ($null -eq $registryKey) + { + Write-Verbose -Message ($script:localizedData.RegistryKeyDoesNotExist -f $Key) + } + else + { + Write-Verbose -Message ($script:localizedData.RegistryKeyExists -f $Key) + + # Check if the user specified a value name to retrieve + $valueNameSpecified = (-not [System.String]::IsNullOrEmpty($ValueName)) -or $PSBoundParameters.ContainsKey('ValueType') -or $PSBoundParameters.ContainsKey('ValueData') + + if ($valueNameSpecified) + { + $valueDisplayName = Get-RegistryKeyValueDisplayName -RegistryKeyValueName $ValueName + $registryResource['ValueName'] = $valueDisplayName + + # If a value name was specified, retrieve the value with the specified name from the retrieved registry key + $registryKeyValue = Get-RegistryKeyValue -RegistryKey $registryKey -RegistryKeyValueName $ValueName + + # Check if the registry key value exists + if ($null -eq $registryKeyValue) + { + Write-Verbose -Message ($script:localizedData.RegistryKeyValueDoesNotExist -f $Key, $valueDisplayName) + } + else + { + Write-Verbose -Message ($script:localizedData.RegistryKeyValueExists -f $Key, $valueDisplayName) + + # If the registry key value exists, retrieve its type + $actualValueType = Get-RegistryKeyValueType -RegistryKey $registryKey -RegistryKeyValueName $ValueName + + # If the registry key value exists, convert it to a readable string + $registryKeyValueAsReadableString = @() + (ConvertTo-ReadableString -RegistryKeyValue $registryKeyValue -RegistryKeyValueType $actualValueType) + + $registryResource['Ensure'] = 'Present' + $registryResource['ValueType'] = $actualValueType + $registryResource['ValueData'] = $registryKeyValueAsReadableString + } + } + else + { + $registryResource['Ensure'] = 'Present' + } + } + + Write-Verbose -Message ($script:localizedData.GetTargetResourceEndMessage -f $Key) + + return $registryResource +} + +<# + .SYNOPSIS + Sets the Registry resource with the given Key to the specified state. + + .PARAMETER Key + The path of the registry key to set the state of. + This path must include the registry hive. + + .PARAMETER ValueName + The name of the registry value to set. + + To add or remove a registry key, specify this property as an empty string without + specifying ValueType or ValueData. To modify or remove the default value of a registry key, + specify this property as an empty string while also specifying ValueType or ValueData. + + .PARAMETER Ensure + Specifies whether or not the registry key with the given path and the registry key value with the given name should exist. + + To ensure that the registry key and value exists, set this property to Present. + To ensure that the registry key and value do not exist, set this property to Absent. + + The default value is Present. + + .PARAMETER ValueData + The data to set as the registry key value. + + .PARAMETER ValueType + The type of the value to set. + + The supported types are: + String (REG_SZ) + Binary (REG-BINARY) + Dword 32-bit (REG_DWORD) + Qword 64-bit (REG_QWORD) + Multi-string (REG_MULTI_SZ) + Expandable string (REG_EXPAND_SZ) + + .PARAMETER Hex + Specifies whether or not the value data should be expressed in hexadecimal format. + + If specified, DWORD/QWORD value data is presented in hexadecimal format. + Not valid for other value types. + + The default value is $false. + + .PARAMETER Force + Specifies whether or not to overwrite the registry key with the given path with the new + value if it is already present. +#> +function Set-TargetResource +{ + [CmdletBinding(SupportsShouldProcess = $true)] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $Key, + + [Parameter(Mandatory = $true)] + [ValidateNotNull()] + [System.String] + [AllowEmptyString()] + $ValueName, + + [Parameter()] + [ValidateSet('Present', 'Absent')] + [System.String] + $Ensure = 'Present', + + [Parameter()] + [ValidateNotNull()] + [System.String[]] + $ValueData = @(), + + [Parameter()] + [ValidateSet('String', 'Binary', 'DWord', 'QWord', 'MultiString', 'ExpandString')] + [System.String] + $ValueType = 'String', + + [Parameter()] + [System.Boolean] + $Hex = $false, + + [Parameter()] + [System.Boolean] + $Force = $false + ) + + Write-Verbose -Message ($script:localizedData.SetTargetResourceStartMessage -f $Key) + + # Retrieve the registry key at the specified path + $registryKey = Get-RegistryKey -RegistryKeyPath $Key -WriteAccessAllowed + + # Check if the registry key exists + if ($null -eq $registryKey) + { + Write-Verbose -Message ($script:localizedData.RegistryKeyDoesNotExist -f $Key) + + # Check if the user wants the registry key to exist + if ($Ensure -eq 'Present') + { + Write-Verbose -Message ($script:localizedData.CreatingRegistryKey -f $Key) + $registryKey = New-RegistryKey -RegistryKeyPath $Key + } + } + + # Check if the registry key exists + if ($null -ne $registryKey) + { + Write-Verbose -Message ($script:localizedData.RegistryKeyExists -f $Key) + + $valueNameSpecified = (-not [System.String]::IsNullOrEmpty($ValueName)) -or $PSBoundParameters.ContainsKey('ValueType') -or $PSBoundParameters.ContainsKey('ValueData') + + # Check if the user wants to set a registry key value + if ($valueNameSpecified) + { + # Retrieve the display name of the specified registry key value + $valueDisplayName = Get-RegistryKeyValueDisplayName -RegistryKeyValueName $ValueName + + # Retrieve the existing registry key value + $actualRegistryKeyValue = Get-RegistryKeyValue -RegistryKey $registryKey -RegistryKeyValueName $ValueName + + # Check if the user wants to add/modify or remove the registry key value + if ($Ensure -eq 'Present') + { + # Convert the specified registry key value to the specified type + $expectedRegistryKeyValue = switch ($ValueType) + { + 'Binary' { ConvertTo-Binary -RegistryKeyValue $ValueData; break } + 'DWord' { ConvertTo-DWord -RegistryKeyValue $ValueData -Hex $Hex; break } + 'MultiString' { ConvertTo-MultiString -RegistryKeyValue $ValueData; break } + 'QWord' { ConvertTo-QWord -RegistryKeyValue $ValueData -Hex $Hex; break } + default { ConvertTo-String -RegistryKeyValue $ValueData} + } + + # Retrieve the name of the registry key + $registryKeyName = Get-RegistryKeyName -RegistryKey $registryKey + + # Check if the registry key value exists + if ($null -eq $actualRegistryKeyValue) + { + # If the registry key value does not exist, set the new value + Write-Verbose -Message ($script:localizedData.SettingRegistryKeyValue -f $valueDisplayName, $Key) + $null = Set-RegistryKeyValue -RegistryKeyName $registryKeyName -RegistryKeyValueName $ValueName -RegistryKeyValue $expectedRegistryKeyValue -ValueType $ValueType + } + else + { + # If the registry key value exists, retrieve its type + $actualValueType = Get-RegistryKeyValueType -RegistryKey $registryKey -RegistryKeyValueName $ValueName + + $ShouldOverwriteRegistryKeyValue = $false + + # Check if the specified type of the registry key value matches the retrieved type of the registry key value + if ($PSBoundParameters.ContainsKey('ValueType') -and ($ValueType -ne $actualValueType)) + { + # If the specified type of the registry key value does not matches the retrieved type of the registry key value, should overwrite the value + $ShouldOverwriteRegistryKeyValue = $true + } + # Check if the specified registry key value matches the retrieved registry key value + elseif (-not (Test-RegistryKeyValuesMatch -ExpectedRegistryKeyValue $expectedRegistryKeyValue -ActualRegistryKeyValue $actualRegistryKeyValue -RegistryKeyValueType $ValueType)) + { + # If the specified registry key value does not matches the retrieved registry key value, should overwrite the value + $ShouldOverwriteRegistryKeyValue = $true + } + + # Check if the registry key value should be overwritten + if ($false -eq $ShouldOverwriteRegistryKeyValue) + { + # No change is needed + Write-Verbose -Message ($script:localizedData.RegistryKeyValueAlreadySet -f $valueDisplayName, $Key) + } + else + { + # Check if the user wants to overwrite the value + if (-not $Force) + { + # If the user does not want to overwrite the value, throw an error + New-InvalidOperationException -Message ($script:localizedData.CannotOverwriteExistingRegistryKeyValueWithoutForce -f $Key, $valueDisplayName) + } + else + { + # If the user does want to overwrite the value, overwrite the value + Write-Verbose -Message ($script:localizedData.OverwritingRegistryKeyValue -f $valueDisplayName, $Key) + $null = Set-RegistryKeyValue -RegistryKeyName $registryKeyName -RegistryKeyValueName $ValueName -RegistryKeyValue $expectedRegistryKeyValue -ValueType $ValueType + } + } + } + } + else + { + # Check if the registry key value exists + if ($null -ne $actualRegistryKeyValue) + { + Write-Verbose -Message ($script:localizedData.RemovingRegistryKeyValue -f $valueDisplayName, $Key) + + # If the specified registry key value exists, check if the user specified a registry key value with a name to remove + if (-not [System.String]::IsNullOrEmpty($ValueName)) + { + # If the user specified a registry key value with a name to remove, remove the registry key value with the specified name + $null = Remove-RegistryKeyValue -RegistryKey $registryKey -RegistryKeyValueName $ValueName + } + else + { + # If the user did not specify a registry key value with a name to remove, remove the default registry key value + $null = Remove-DefaultRegistryKeyValue -RegistryKey $registryKey + } + } + } + } + else + { + # Check if the user wants to remove the registry key + if ($Ensure -eq 'Absent') + { + # Retrieve the number of subkeys the registry key has + $registryKeySubKeyCount = Get-RegistryKeySubKeyCount -RegistryKey $registryKey + + # Check if the registry key has subkeys and the user does not want to forcibly remove the registry key + if ($registryKeySubKeyCount -gt 0 -and -not $Force) + { + New-InvalidOperationException -Message ($script:localizedData.CannotRemoveExistingRegistryKeyWithSubKeysWithoutForce -f $Key) + } + else + { + # Remove the registry key + Write-Verbose -Message ($script:localizedData.RemovingRegistryKey -f $Key) + $null = Remove-RegistryKey -RegistryKey $registryKey + } + } + } + } + + Write-Verbose -Message ($script:localizedData.SetTargetResourceEndMessage -f $Key) +} + +<# + .SYNOPSIS + Tests if the Registry resource with the given key is in the specified state. + + .PARAMETER Key + The path of the registry key to test the state of. + This path must include the registry hive. + + .PARAMETER ValueName + The name of the registry value to check for. + Specify this property as an empty string ('') to check the default value of the registry key. + + .PARAMETER Ensure + Specifies whether or not the registry key and value should exist. + + To test that they exist, set this property to "Present". + To test that they do not exist, set the property to "Absent". + The default value is "Present". + + .PARAMETER ValueData + The data the registry key value should have. + + .PARAMETER ValueType + The type of the value. + + The supported types are: + String (REG_SZ) + Binary (REG-BINARY) + Dword 32-bit (REG_DWORD) + Qword 64-bit (REG_QWORD) + Multi-string (REG_MULTI_SZ) + Expandable string (REG_EXPAND_SZ) + + .PARAMETER Hex + Not used in Test-TargetResource. + + .PARAMETER Force + Not used in Test-TargetResource. +#> +function Test-TargetResource +{ + [CmdletBinding()] + [OutputType([System.Boolean])] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $Key, + + [Parameter(Mandatory = $true)] + [AllowEmptyString()] + [ValidateNotNull()] + [System.String] + $ValueName, + + [Parameter()] + [ValidateSet('Present', 'Absent')] + [System.String] + $Ensure = 'Present', + + [Parameter()] + [ValidateNotNull()] + [System.String[]] + $ValueData = @(), + + [Parameter()] + [ValidateSet('String', 'Binary', 'DWord', 'QWord', 'MultiString', 'ExpandString')] + [System.String] + $ValueType = 'String', + + [Parameter()] + [System.Boolean] + $Hex = $false, + + [Parameter()] + [System.Boolean] + $Force = $false + ) + + Write-Verbose -Message ($script:localizedData.TestTargetResourceStartMessage -f $Key) + + $registryResourceInDesiredState = $false + + $getTargetResourceParameters = @{ + Key = $Key + ValueName = $ValueName + } + + if ($PSBoundParameters.ContainsKey('ValueType')) + { + $getTargetResourceParameters['ValueType'] = $ValueType + } + + if ($PSBoundParameters.ContainsKey('ValueData')) + { + $getTargetResourceParameters['ValueData'] = $ValueData + } + + $registryResource = Get-TargetResource @getTargetResourceParameters + + # Check if the user specified a value name to retrieve + $valueNameSpecified = (-not [System.String]::IsNullOrEmpty($ValueName)) -or $PSBoundParameters.ContainsKey('ValueType') -or $PSBoundParameters.ContainsKey('ValueData') + + if ($valueNameSpecified) + { + $valueDisplayName = Get-RegistryKeyValueDisplayName -RegistryKeyValueName $ValueName + + if ($registryResource.Ensure -eq 'Absent') + { + Write-Verbose -Message ($script:localizedData.RegistryKeyValueDoesNotExist -f $Key, $valueDisplayName) + $registryResourceInDesiredState = $Ensure -eq 'Absent' + } + else + { + Write-Verbose -Message ($script:localizedData.RegistryKeyValueExists -f $Key, $valueDisplayName) + + if ($Ensure -eq 'Absent') + { + $registryResourceInDesiredState = $false + } + elseif ($PSBoundParameters.ContainsKey('ValueType') -and $ValueType -ne $registryResource.ValueType) + { + Write-Verbose -Message ($script:localizedData.RegistryKeyValueTypeDoesNotMatch -f $valueDisplayName, $Key, $ValueType, $registryResource.ValueType) + + $registryResourceInDesiredState = $false + } + elseif ($PSBoundParameters.ContainsKey('ValueData')) + { + # Need to get the actual registry key value since Get-TargetResource returns + $registryKey = Get-RegistryKey -RegistryKeyPath $Key + $actualRegistryKeyValue = Get-RegistryKeyValue -RegistryKey $registryKey -RegistryKeyValueName $ValueName + + if (-not $PSBoundParameters.ContainsKey('ValueType') -and $null -ne $registryResource.ValueType) + { + $ValueType = $registryResource.ValueType + } + + # Convert the specified registry key value to the specified type + $expectedRegistryKeyValue = switch ($ValueType) + { + 'Binary' { ConvertTo-Binary -RegistryKeyValue $ValueData; break } + 'DWord' { ConvertTo-DWord -RegistryKeyValue $ValueData -Hex $Hex; break } + 'MultiString' { ConvertTo-MultiString -RegistryKeyValue $ValueData; break } + 'QWord' { ConvertTo-QWord -RegistryKeyValue $ValueData -Hex $Hex; break } + default { ConvertTo-String -RegistryKeyValue $ValueData; break } + } + + if (-not (Test-RegistryKeyValuesMatch -ExpectedRegistryKeyValue $expectedRegistryKeyValue -ActualRegistryKeyValue $actualRegistryKeyValue -RegistryKeyValueType $ValueType)) + { + Write-Verbose -Message ($script:localizedData.RegistryKeyValueDoesNotMatch -f $valueDisplayName, $Key, $ValueData, $registryResource.ValueData) + + $registryResourceInDesiredState = $false + } + else + { + $registryResourceInDesiredState = $true + } + } + else + { + $registryResourceInDesiredState = $true + } + } + } + else + { + if ($registryResource.Ensure -eq 'Present') + { + Write-Verbose -Message ($script:localizedData.RegistryKeyExists -f $Key) + $registryResourceInDesiredState = $Ensure -eq 'Present' + } + else + { + Write-Verbose -Message ($script:localizedData.RegistryKeyDoesNotExist -f $Key) + $registryResourceInDesiredState = $Ensure -eq 'Absent' + } + } + + Write-Verbose -Message ($script:localizedData.TestTargetResourceEndMessage -f $Key) + + return $registryResourceInDesiredState +} + +<# + .SYNOPSIS + Retrieves the root of the specified path. + + .PARAMETER Path + The path to retrieve the root of. +#> +function Get-PathRoot +{ + [OutputType([System.String])] + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $Path + ) + + if ($Path.Contains('\')) + { + $pathRoot = $Path.Split('\')[0] + } + else + { + $pathRoot = $Path + } + + return $pathRoot +} + +<# + .SYNOPSIS + Converts the specified registry drive root to its corresponding registry drive name. + + .PARAMETER RegistryDriveRoot + The registry drive root to convert. +#> +function ConvertTo-RegistryDriveName +{ + [OutputType([System.String])] + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $RegistryDriveRoot + ) + + $registryDriveName = $null + + if ($script:registryDriveRoots.ContainsValue($RegistryDriveRoot)) + { + foreach ($registryDriveRootsKey in $script:registryDriveRoots.Keys) + { + if ($script:registryDriveRoots[$registryDriveRootsKey] -ieq $RegistryDriveRoot) + { + $registryDriveName = $registryDriveRootsKey + break + } + } + } + + return $registryDriveName +} + +<# + .SYNOPSIS + Retrieves the name of the registry drive at the root of the the specified registry key path. + + .PARAMETER RegistryKeyPath + The registry key path to retrieve the registry drive name from. +#> +function Get-RegistryDriveName +{ + [OutputType([System.String])] + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $RegistryKeyPath + ) + + $registryKeyPathRoot = Get-PathRoot -Path $RegistryKeyPath + $registryKeyPathRoot = $registryKeyPathRoot.TrimEnd('\') + + if ($registryKeyPathRoot.Contains(':')) + { + $registryDriveName = $registryKeyPathRoot.TrimEnd(':') + + if (-not $script:registryDriveRoots.ContainsKey($registryDriveName)) + { + New-InvalidArgumentException -ArgumentName 'Key' -Message ($script:localizedData.InvalidRegistryDrive -f $registryDriveName) + } + } + else + { + $registryDriveName = ConvertTo-RegistryDriveName -RegistryDriveRoot $registryKeyPathRoot + + if ([System.String]::IsNullOrEmpty($registryDriveName)) + { + New-InvalidArgumentException -ArgumentName 'Key' -Message ($script:localizedData.InvalidRegistryDrive -f $registryKeyPathRoot) + } + } + + return $registryDriveName +} + +<# + .SYNOPSIS + Mounts the registry drive with the specified name. + + .PARAMETER RegistryKeyName + The name of the registry drive to mount. +#> +function Mount-RegistryDrive +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $RegistryDriveName + ) + + $registryDriveInfo = Get-PSDrive -Name $RegistryDriveName -ErrorAction 'SilentlyContinue' + + if ($null -eq $registryDriveInfo) + { + $newPSDriveParameters = @{ + Name = $RegistryDriveName + Root = $script:registryDriveRoots[$RegistryDriveName] + PSProvider = 'Registry' + Scope = 'Script' + } + + $registryDriveInfo = New-PSDrive @newPSDriveParameters + } + + # Validate that the specified PSDrive is valid + if (($null -eq $registryDriveInfo) -or ($null -eq $registryDriveInfo.Provider) -or ($registryDriveInfo.Provider.Name -ine 'Registry')) + { + New-InvalidOperationException -Message ($script:localizedData.RegistryDriveCouldNotBeMounted -f $RegistryDriveName) + } +} + +<# + .SYNOPSIS + Opens the specified registry sub key under the specified registry parent key. + This is a wrapper function for unit testing. + + .PARAMETER ParentKey + The parent registry key which contains the sub key to open. + + .PARAMETER SubKey + The sub key to open. + + .PARAMETER WriteAccessAllowed + Specifies whether or not to open the sub key with permissions to write to it. +#> +function Open-RegistrySubKey +{ + [OutputType([Microsoft.Win32.RegistryKey])] + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [Microsoft.Win32.RegistryKey] + $ParentKey, + + [Parameter(Mandatory = $true)] + [System.String] + [AllowEmptyString()] + $SubKey, + + [Parameter()] + [System.Management.Automation.SwitchParameter] + $WriteAccessAllowed + ) + + return $ParentKey.OpenSubKey($SubKey, $WriteAccessAllowed) +} + +<# + .SYNOPSIS + Opens and retrieves the registry key at the specified path. + + .PARAMETER RegistryKeyPath + The path to the registry key to open. + The path must include the registry drive. + + .PARAMETER WriteAccessAllowed + Specifies whether or not to open the key with permissions to write to it. + + .NOTES + This method is used instead of Get-Item so that there is no ambiguity between + forward slashes as path separators vs literal characters in a key name + (which is valid in the registry). + +#> +function Get-RegistryKey +{ + [OutputType([Microsoft.Win32.RegistryKey])] + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $RegistryKeyPath, + + [Parameter()] + [System.Management.Automation.SwitchParameter] + $WriteAccessAllowed + ) + + # Parse the registry drive from the specified registry key path + $registryDriveName = Get-RegistryDriveName -RegistryKeyPath $RegistryKeyPath + + # Mount the registry drive if needed + Mount-RegistryDrive -RegistryDriveName $registryDriveName + + # Retrieve the registry drive key + $registryDriveKey = Get-Item -LiteralPath ($registryDriveName + ':') + + # Parse the registry drive subkey from the specified registry key path + $indexOfBackSlashInPath = $RegistryKeyPath.IndexOf('\') + if ($indexOfBackSlashInPath -ge 0 -and $indexOfBackSlashInPath -lt ($RegistryKeyPath.Length - 1)) + { + $registryDriveSubKey = $RegistryKeyPath.Substring($RegistryKeyPath.IndexOf('\') + 1) + } + else + { + $registryDriveSubKey = '' + } + + # Open the registry drive subkey + $registryKey = Open-RegistrySubKey -ParentKey $registryDriveKey -SubKey $registryDriveSubKey -WriteAccessAllowed:$WriteAccessAllowed + + # Return the opened registry key + return $registryKey +} + +<# + .SYNOPSIS + Retrieves the display name of the default registry key value if needed. + + .PARAMETER RegistryKeyValueName + The name of the registry key value to retrieve the display name of. +#> +function Get-RegistryKeyValueDisplayName +{ + [OutputType([System.String])] + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [AllowNull()] + [AllowEmptyString()] + [System.String] + $RegistryKeyValueName + ) + + $registryKeyValueDisplayName = $RegistryKeyValueName + + if ([System.String]::IsNullOrEmpty($RegistryKeyValueName)) + { + $registryKeyValueDisplayName = $script:localizedData.DefaultValueDisplayName + } + + return $registryKeyValueDisplayName +} + +<# + .SYNOPSIS + Retrieves the registry key value with the specified name from the specified registry key. + This is a wrapper function for unit testing. + + .PARAMETER RegistryKey + The registry key to retrieve the value from. + + .PARAMETER RegistryKeyValueName + The name of the registry key value to retrieve. +#> +function Get-RegistryKeyValue +{ + [OutputType([System.Object[]])] + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [Microsoft.Win32.RegistryKey] + $RegistryKey, + + [Parameter(Mandatory = $true)] + [ValidateNotNull()] + [AllowEmptyString()] + [System.String] + $RegistryKeyValueName + ) + + $registryValueOptions = [Microsoft.Win32.RegistryValueOptions]::DoNotExpandEnvironmentNames + $registryKeyValue = $RegistryKey.GetValue($RegistryKeyValueName, $null, $registryValueOptions) + return ,$registryKeyValue +} + +<# + .SYNOPSIS + Retrieves the type of the registry key value with the specified name from the the specified + registry key. + This is a wrapper function for unit testing. + + .PARAMETER RegistryKey + The registry key to retrieve the type of the value from. + + .PARAMETER RegistryKeyValueName + The name of the registry key value to retrieve the type of. +#> +function Get-RegistryKeyValueType +{ + [OutputType([System.Type])] + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [Microsoft.Win32.RegistryKey] + $RegistryKey, + + [Parameter(Mandatory = $true)] + [ValidateNotNull()] + [AllowEmptyString()] + [System.String] + $RegistryKeyValueName + ) + + return $RegistryKey.GetValueKind($RegistryKeyValueName) +} + +<# + .SYNOPSIS + Converts the specified byte array to a hex string. + + .PARAMETER ByteArray + The byte array to convert. +#> +function Convert-ByteArrayToHexString +{ + [OutputType([System.String])] + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNull()] + [AllowEmptyCollection()] + [System.Object[]] + $ByteArray + ) + + $hexString = '' + + foreach ($byte in $ByteArray) + { + $hexString += ('{0:x2}' -f $byte) + } + + return $hexString +} + +<# + .SYNOPSIS + Converts the specified registry key value to a readable string. + + .PARAMETER RegistryKeyValue + The registry key value to convert. + + .PARAMETER RegistryKeyValueType + The type of the registry key value to convert. +#> +function ConvertTo-ReadableString +{ + [OutputType([System.String])] + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [AllowNull()] + [AllowEmptyCollection()] + [System.Object[]] + $RegistryKeyValue, + + [Parameter(Mandatory = $true)] + [ValidateSet('String', 'Binary', 'DWord', 'QWord', 'MultiString', 'ExpandString')] + [System.String] + $RegistryKeyValueType + ) + + $registryKeyValueAsString = [System.String]::Empty + + if ($null -ne $RegistryKeyValue) + { + # For Binary type data, convert the received bytes back to a readable hex-string + if ($RegistryKeyValueType -eq 'Binary') + { + $RegistryKeyValue = Convert-ByteArrayToHexString -ByteArray $RegistryKeyValue + } + + if ($RegistryKeyValueType -ne 'MultiString') + { + $RegistryKeyValue = [System.String[]] @() + $RegistryKeyValue + } + + if ($RegistryKeyValue.Count -eq 1 -and -not [System.String]::IsNullOrEmpty($RegistryKeyValue[0])) + { + $registryKeyValueAsString = $RegistryKeyValue[0].ToString() + } + elseif ($RegistryKeyValue.Count -gt 1) + { + $registryKeyValueAsString = "($($RegistryKeyValue -join ', '))" + } + } + + return $registryKeyValueAsString +} + +<# + .SYNOPSIS + Creates a new subkey with the specified name under the specified registry key. + This is a wrapper function for unit testing. + + .PARAMETER ParentRegistryKey + The parent registry key to create the new subkey under. + + .PARAMETER SubKeyName + The name of the new subkey to create. +#> +function New-RegistrySubKey +{ + [OutputType([Microsoft.Win32.RegistryKey])] + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [Microsoft.Win32.RegistryKey] + $ParentRegistryKey, + + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $SubKeyName + ) + + return $ParentRegistryKey.CreateSubKey($SubKeyName) +} + +<# + .SYNOPSIS + Creates a new registry key at the specified registry key path. + + .PARAMETER RegistryKeyPath + The path at which to create the registry key. + This path must include the registry drive. +#> +function New-RegistryKey +{ + [OutputType([Microsoft.Win32.RegistryKey])] + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $RegistryKeyPath + ) + + # registry key names can contain forward slashes, so we can't use Split-Path here (it will split on /) + $lastSep = $RegistryKeyPath.LastIndexOf('\') + $parentRegistryKeyPath = $RegistryKeyPath.Substring(0, $lastSep) + $newRegistryKeyName = $RegistryKeyPath.Substring($lastSep + 1) + + $parentRegistryKey = Get-RegistryKey -RegistryKeyPath $parentRegistryKeyPath -WriteAccessAllowed + + if ($null -eq $parentRegistryKey) + { + # If the parent registry key does not exist, create it + $parentRegistryKey = New-RegistryKey -RegistryKeyPath $parentRegistryKeyPath + } + + $newRegistryKey = New-RegistrySubKey -ParentRegistryKey $parentRegistryKey -SubKeyName $newRegistryKeyName + + return $newRegistryKey +} + +<# + .SYNOPSIS + Retrieves the name of the specified registry key. + This is a wrapper function for unit testing. + + .PARAMETER RegistryKey + The registry key to retrieve the name of. +#> +function Get-RegistryKeyName +{ + [OutputType([System.String])] + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [Microsoft.Win32.RegistryKey] + $RegistryKey + ) + + return $RegistryKey.Name +} + +<# + .SYNOPSIS + Converts the specified registry key value to a byte array for the Binary registry type. + + .PARAMETER RegistryKeyValue + The registry key value to convert. +#> +function ConvertTo-Binary +{ + [OutputType([System.Byte[]])] + [CmdletBinding()] + param + ( + [Parameter()] + [AllowNull()] + [AllowEmptyCollection()] + [System.String[]] + $RegistryKeyValue + ) + + if (($null -ne $RegistryKeyValue) -and ($RegistryKeyValue.Count -gt 1)) + { + New-InvalidArgumentException -ArgumentName 'ValueData' -Message ($script:localizedData.ArrayNotAllowedForExpectedType -f 'Binary') + } + + $binaryRegistryKeyValue = [System.Byte[]] @() + + if (($null -ne $RegistryKeyValue) -and ($RegistryKeyValue.Count -eq 1) -and (-not [System.String]::IsNullOrEmpty($RegistryKeyValue[0]))) + { + $singleRegistryKeyValue = $RegistryKeyValue[0] + + if ($singleRegistryKeyValue.StartsWith('0x')) + { + $singleRegistryKeyValue = $singleRegistryKeyValue.Substring('0x'.Length) + } + + if (($singleRegistryKeyValue.Length % 2) -ne 0) + { + $singleRegistryKeyValue = $singleRegistryKeyValue.PadLeft($singleRegistryKeyValue.Length + 1, '0') + } + + try + { + for ($singleRegistryKeyValueIndex = 0 ; $singleRegistryKeyValueIndex -lt ($singleRegistryKeyValue.Length - 1) ; $singleRegistryKeyValueIndex = $singleRegistryKeyValueIndex + 2) + { + $binaryRegistryKeyValue += [System.Byte]::Parse($singleRegistryKeyValue.Substring($singleRegistryKeyValueIndex, 2), 'HexNumber') + } + } + catch + { + New-InvalidArgumentException -ArgumentName 'ValueData' -Message ($script:localizedData.BinaryDataNotInHexFormat -f $singleRegistryKeyValue) + } + } + + return $binaryRegistryKeyValue +} + +<# + .SYNOPSIS + Converts the specified registry key value to an Int32 for the DWord registry type. + + .PARAMETER RegistryKeyValue + The registry key value to convert. +#> +function ConvertTo-DWord +{ + [OutputType([System.Int32])] + [CmdletBinding()] + param + ( + [Parameter()] + [AllowNull()] + [AllowEmptyCollection()] + [System.String[]] + $RegistryKeyValue, + + [Parameter()] + [System.Boolean] + $Hex = $false + ) + + if (($null -ne $RegistryKeyValue) -and ($RegistryKeyValue.Count -gt 1)) + { + New-InvalidArgumentException -ArgumentName 'ValueData' -Message ($script:localizedData.ArrayNotAllowedForExpectedType -f 'Dword') + } + + $dwordRegistryKeyValue = [System.Int32] 0 + + if (($null -ne $RegistryKeyValue) -and ($RegistryKeyValue.Count -eq 1) -and (-not [System.String]::IsNullOrEmpty($RegistryKeyValue[0]))) + { + $singleRegistryKeyValue = $RegistryKeyValue[0] + + if ($Hex) + { + if ($singleRegistryKeyValue.StartsWith('0x')) + { + $singleRegistryKeyValue = $singleRegistryKeyValue.Substring('0x'.Length) + } + + $currentCultureInfo = [System.Globalization.CultureInfo]::CurrentCulture + $referenceValue = $null + + if ([System.Int32]::TryParse($singleRegistryKeyValue, 'HexNumber', $currentCultureInfo, [ref] $referenceValue)) + { + $dwordRegistryKeyValue = $referenceValue + } + else + { + New-InvalidArgumentException -ArgumentName 'ValueData' -Message ($script:localizedData.DWordDataNotInHexFormat -f $singleRegistryKeyValue) + } + } + else + { + $dwordRegistryKeyValue = [System.Int32]::Parse($singleRegistryKeyValue) + } + } + + return $dwordRegistryKeyValue +} + +<# + .SYNOPSIS + Converts the specified registry key value to a string array for the MultiString registry type. + + .PARAMETER RegistryKeyValue + The registry key value to convert. +#> +function ConvertTo-MultiString +{ + [OutputType([System.String[]])] + [CmdletBinding()] + param + ( + [Parameter()] + [AllowNull()] + [AllowEmptyCollection()] + [System.String[]] + $RegistryKeyValue + ) + + $multiStringRegistryKeyValue = [System.String[]] @() + + if (($null -ne $RegistryKeyValue) -and ($RegistryKeyValue.Length -gt 0)) + { + $multiStringRegistryKeyValue = [System.String[]] $RegistryKeyValue + } + + return $multiStringRegistryKeyValue +} + +<# + .SYNOPSIS + Converts the specified registry key value to an Int64 for the QWord registry type. + + .PARAMETER RegistryKeyValue + The registry key value to convert. +#> +function ConvertTo-QWord +{ + [OutputType([System.Int64])] + [CmdletBinding()] + param + ( + [Parameter()] + [AllowNull()] + [AllowEmptyCollection()] + [System.String[]] + $RegistryKeyValue, + + [Parameter()] + [System.Boolean] + $Hex = $false + ) + + if (($null -ne $RegistryKeyValue) -and ($RegistryKeyValue.Count -gt 1)) + { + New-InvalidArgumentException -ArgumentName 'ValueData' -Message ($script:localizedData.ArrayNotAllowedForExpectedType -f 'Qword') + } + + $qwordRegistryKeyValue = [System.Int64] 0 + + if (($null -ne $RegistryKeyValue) -and ($RegistryKeyValue.Count -eq 1) -and (-not [System.String]::IsNullOrEmpty($RegistryKeyValue[0]))) + { + $singleRegistryKeyValue = $RegistryKeyValue[0] + + if ($Hex) + { + if ($singleRegistryKeyValue.StartsWith('0x')) + { + $singleRegistryKeyValue = $singleRegistryKeyValue.Substring('0x'.Length) + } + + $currentCultureInfo = [System.Globalization.CultureInfo]::CurrentCulture + $referenceValue = $null + + if ([System.Int64]::TryParse($singleRegistryKeyValue, 'HexNumber', $currentCultureInfo, [ref] $referenceValue)) + { + $qwordRegistryKeyValue = $referenceValue + } + else + { + New-InvalidArgumentException -ArgumentName 'ValueData' -Message ($script:localizedData.QWordDataNotInHexFormat -f $singleRegistryKeyValue) + } + } + else + { + $qwordRegistryKeyValue = [System.Int64]::Parse($singleRegistryKeyValue) + } + } + + return $qwordRegistryKeyValue +} + +<# + .SYNOPSIS + Converts the specified registry key value to a string for the String or ExpandString registry types. + + .PARAMETER RegistryKeyValue + The registry key value to convert. +#> +function ConvertTo-String +{ + [OutputType([System.String])] + [CmdletBinding()] + param + ( + [Parameter()] + [AllowNull()] + [AllowEmptyCollection()] + [System.String[]] + $RegistryKeyValue + ) + + if (($null -ne $RegistryKeyValue) -and ($RegistryKeyValue.Count -gt 1)) + { + New-InvalidArgumentException -ArgumentName 'ValueData' -Message ($script:localizedData.ArrayNotAllowedForExpectedType -f 'String or ExpandString') + } + + $registryKeyValueAsString = [System.String]::Empty + + if (($null -ne $RegistryKeyValue) -and ($RegistryKeyValue.Count -eq 1)) + { + $registryKeyValueAsString = [System.String] $RegistryKeyValue[0] + } + + return $registryKeyValueAsString +} + +<# + .SYNOPSIS + Sets the specified registry key value with the specified name to the specified value. + This is a wrapper function for unit testing. + + .PARAMETER RegistryKeyName + The name of the registry key that the value to set is under. + + .PARAMETER RegistryKeyValueName + The name of the registry key value to set. + + .PARAMETER RegistryKeyValue + The new value to set the registry key value to. + + .PARAMETER RegistryKeyValueType + The type of the new value to set the registry key value to. +#> +function Set-RegistryKeyValue +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $RegistryKeyName, + + [Parameter(Mandatory = $true)] + [ValidateNotNull()] + [AllowEmptyString()] + [System.String] + $RegistryKeyValueName, + + [Parameter(Mandatory = $true)] + [AllowNull()] + [System.Object] + $RegistryKeyValue, + + [Parameter(Mandatory = $true)] + [ValidateSet('String', 'Binary', 'DWord', 'QWord', 'MultiString', 'ExpandString')] + [System.String] + $ValueType + ) + + if ($ValueType -eq 'Binary') + { + $RegistryKeyValue = [System.Byte[]] $RegistryKeyValue + } + elseif ($ValueType -eq 'MultiString') + { + $RegistryKeyValue = [System.String[]] $RegistryKeyValue + } + + $null = [Microsoft.Win32.Registry]::SetValue($RegistryKeyName, $RegistryKeyValueName, $RegistryKeyValue, $ValueType) +} + +<# + .SYNOPSIS + Tests if the actual registry key value matches the expected registry key value. + + .PARAMETER ExpectedRegistryKeyValue + The expected registry key value to test against. + + .PARAMETER ActualRegistryKeyValue + The actual registry key value to test. + + .PARAMETER RegistryKeyValueType + The type of the registry key values. +#> +function Test-RegistryKeyValuesMatch +{ + [OutputType([System.Boolean])] + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [AllowNull()] + [System.Object] + $ExpectedRegistryKeyValue, + + [Parameter(Mandatory = $true)] + [AllowNull()] + [System.Object] + $ActualRegistryKeyValue, + + [Parameter(Mandatory = $true)] + [ValidateSet('String', 'Binary', 'DWord', 'QWord', 'MultiString', 'ExpandString')] + [System.String] + $RegistryKeyValueType + ) + + $registryKeyValuesMatch = $true + + if ($RegistryKeyValueType -eq 'Multistring' -or $RegistryKeyValueType -eq 'Binary') + { + if ($null -eq $ExpectedRegistryKeyValue) + { + $ExpectedRegistryKeyValue = @() + } + + if ($null -eq $ActualRegistryKeyValue) + { + $ActualRegistryKeyValue = @() + } + + $registryKeyValuesMatch = $null -eq (Compare-Object -ReferenceObject $ExpectedRegistryKeyValue -DifferenceObject $ActualRegistryKeyValue) + } + else + { + if ($null -eq $ExpectedRegistryKeyValue) + { + $ExpectedRegistryKeyValue = '' + } + + if ($null -eq $ActualRegistryKeyValue) + { + $ActualRegistryKeyValue = '' + } + + $registryKeyValuesMatch = $ExpectedRegistryKeyValue -ieq $ActualRegistryKeyValue + } + + return $registryKeyValuesMatch +} + +<# + .SYNOPSIS + Removes the specified registry key and child subkeys recursively. + + .PARAMETER RegistryKey + The registry key to remove. +#> +function Remove-RegistryKey +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [Microsoft.Win32.RegistryKey] + $RegistryKey + ) + + $parentKeyName = Split-Path -Path $RegistryKey.Name -Parent + $targetKeyName = Split-Path -Path $RegistryKey.Name -Leaf + + $parentRegistryKey = Get-RegistryKey -RegistryKeyPath $parentKeyName -WriteAccessAllowed + + $null = $parentRegistryKey.DeleteSubKeyTree($targetKeyName) +} + +<# + .SYNOPSIS + Removes the specified value of the specified registry key. + This is a wrapper function for unit testing. + + .PARAMETER RegistryKey + The registry key to remove the default value of. +#> +function Remove-RegistryKeyValue +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [Microsoft.Win32.RegistryKey] + $RegistryKey, + + [Parameter(Mandatory = $true)] + [ValidateNotNull()] + [AllowEmptyString()] + [System.String] + $RegistryKeyValueName + ) + + $null = $RegistryKey.DeleteValue($RegistryKeyValueName) +} + +<# + .SYNOPSIS + Removes the default value of the specified registry key. + This is a wrapper function for unit testing. + + .PARAMETER RegistryKey + The registry key to remove the default value of. +#> +function Remove-DefaultRegistryKeyValue +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [Microsoft.Win32.RegistryKey] + $RegistryKey + ) + + $null = $RegistryKey.DeleteValue('') +} + +<# + .SYNOPSIS + Retrieves the number of subkeys under the specified registry key. + This is a wrapper function for unit testing. + + .PARAMETER RegistryKey + The registry key to retrieve the subkeys of. +#> +function Get-RegistryKeySubKeyCount +{ + [OutputType([System.Int32])] + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [Microsoft.Win32.RegistryKey] + $RegistryKey + ) + + return $RegistryKey.SubKeyCount +} diff --git a/deployment/dsc/azshcihost/xPSDesiredStateConfiguration/9.1.0/DSCResources/DSC_xRegistryResource/DSC_xRegistryResource.schema.mof b/deployment/dsc/azshcihost/xPSDesiredStateConfiguration/9.1.0/DSCResources/DSC_xRegistryResource/DSC_xRegistryResource.schema.mof new file mode 100644 index 0000000..be99b6f --- /dev/null +++ b/deployment/dsc/azshcihost/xPSDesiredStateConfiguration/9.1.0/DSCResources/DSC_xRegistryResource/DSC_xRegistryResource.schema.mof @@ -0,0 +1,11 @@ +[ClassVersion("1.0.0"), FriendlyName("xRegistry")] +class DSC_xRegistryResource : OMI_BaseResource +{ + [Key, Description("The path of the registry key to add, modify, or remove. This path must include the registry hive/drive.")] String Key; + [Key, Description("The name of the registry value. To add or remove a registry key, specify this property as an empty string without specifying ValueType or ValueData. To modify or remove the default value of a registry key, specify this property as an empty string while also specifying ValueType or ValueData.")] String ValueName; + [Write, Description("The data the specified registry key value should have as a string or an array of strings (MultiString only).")] String ValueData[]; + [Write, Description("The type the specified registry key value should have."), ValueMap{"String", "Binary", "DWord", "QWord", "MultiString", "ExpandString"},Values{"String", "Binary", "DWord", "QWord", "MultiString", "ExpandString"}] String ValueType; + [Write, Description("Specifies whether or not the registry key or value should exist. To add or modify a registry key or value, set this property to Present. To remove a registry key or value, set the property to Absent."), ValueMap{"Present", "Absent"},Values{"Present", "Absent"}] String Ensure; + [Write, Description("Specifies whether or not the specified DWord or QWord registry key data is provided in a hexadecimal format. Not valid for types other than DWord and QWord. The default value is $false.")] Boolean Hex; + [Write, Description("Specifies whether or not to overwrite the specified registry key value if it already has a value or whether or not to delete a registry key that has subkeys. The default value is $false.")] Boolean Force; +}; diff --git a/deployment/dsc/azshcihost/xPSDesiredStateConfiguration/9.1.0/DSCResources/DSC_xRegistryResource/en-US/DSC_xRegistryResource.schema.mfl b/deployment/dsc/azshcihost/xPSDesiredStateConfiguration/9.1.0/DSCResources/DSC_xRegistryResource/en-US/DSC_xRegistryResource.schema.mfl new file mode 100644 index 0000000..6380f55 --- /dev/null +++ b/deployment/dsc/azshcihost/xPSDesiredStateConfiguration/9.1.0/DSCResources/DSC_xRegistryResource/en-US/DSC_xRegistryResource.schema.mfl @@ -0,0 +1,11 @@ +[Description("Provides a mechanism to manage registry keys and values on a target node.") : Amended,AMENDMENT, LOCALE("MS_409")] +class DSC_xRegistryResource : OMI_BaseResource +{ + [Key,Description("The path of the registry key to add, modify, or remove. This path must include the registry hive/drive.") : Amended] String Key; + [Key,Description("The name of the registry value. To add or remove a registry key, specify this property as an empty string without specifying ValueType or ValueData. To modify or remove the default value of a registry key, specify this property as an empty string while also specifying ValueType or ValueData.") : Amended] String ValueName; + [Description("The data the specified registry key value should have as a string or an array of strings (MultiString only).") : Amended] String ValueData[]; + [Description("The type the specified registry key value should have.") : Amended] String ValueType; + [Description("Specifies whether or not the registry key or value should exist. To add or modify a registry key or value, set this property to Present. To remove a registry key or value, set the property to Absent.") : Amended] String Ensure; + [Description("Specifies whether or not the specified DWord or QWord registry key data is provided in a hexadecimal format. Not valid for types other than DWord and QWord. The default value is $false.") : Amended] Boolean Hex; + [Description("Specifies whether or not to overwrite the specified registry key value if it already has a value or whether or not to delete a registry key that has subkeys. The default value is $false.") : Amended] Boolean Force; +}; diff --git a/deployment/dsc/azshcihost/xPSDesiredStateConfiguration/9.1.0/DSCResources/DSC_xRegistryResource/en-US/DSC_xRegistryResource.strings.psd1 b/deployment/dsc/azshcihost/xPSDesiredStateConfiguration/9.1.0/DSCResources/DSC_xRegistryResource/en-US/DSC_xRegistryResource.strings.psd1 new file mode 100644 index 0000000..411f650 --- /dev/null +++ b/deployment/dsc/azshcihost/xPSDesiredStateConfiguration/9.1.0/DSCResources/DSC_xRegistryResource/en-US/DSC_xRegistryResource.strings.psd1 @@ -0,0 +1,35 @@ +# Localized resources for DSC_xRegistryResource + +ConvertFrom-StringData @' + DefaultValueDisplayName = (Default) + + GetTargetResourceStartMessage = Get-TargetResource is starting for Registry resource with Key {0} + GetTargetResourceEndMessage = Get-TargetResource has finished for Registry resource with Key {0} + RegistryKeyDoesNotExist = The registry key at path {0} does not exist. + RegistryKeyExists = The registry key at path {0} exists. + RegistryKeyValueDoesNotExist = The registry key at path {0} does not have a value named {1}. + RegistryKeyValueExists = The registry key at path {0} has a value named {1}. + + SetTargetResourceStartMessage = Set-TargetResource is starting for Registry resource with Key {0} + SetTargetResourceEndMessage = Set-TargetResource has finished for Registry resource with Key {0} + CreatingRegistryKey = Creating registry key at path {0}... + SettingRegistryKeyValue = Setting the value {0} under the registry key at path {1}... + OverwritingRegistryKeyValue = Overwriting the value {0} under the registry key at path {1}... + RemovingRegistryKey = Removing registry key at path {0}... + RegistryKeyValueAlreadySet = The value {0} under the registry key at path {1} has already been set to the specified value. + RemovingRegistryKeyValue = Removing the value {0} from the registry key at path {1}... + + TestTargetResourceStartMessage = Test-TargetResource is starting for Registry resource with Key {0} + TestTargetResourceEndMessage = Test-TargetResource has finished for Registry resource with Key {0} + RegistryKeyValueTypeDoesNotMatch = The type of the value {0} under the registry key at path {1} does not match the expected type. Expected {2} but was {3}. + RegistryKeyValueDoesNotMatch = The value {0} under the registry key at path {1} does not match the expected value. Expected {2} but was {3}. + + CannotRemoveExistingRegistryKeyWithSubKeysWithoutForce = The registry key at path {0} has subkeys. To remove this registry key please specifiy the Force parameter as $true. + CannotOverwriteExistingRegistryKeyValueWithoutForce = The registry key at path {0} already has a value with the name {1}. To overwrite this registry key value please specifiy the Force parameter as $true. + ArrayNotAllowedForExpectedType = The specified value data has been declared as a string array, but the registry key type {0} cannot be converted from an array. Please declare the value data as only one string or use the registry type MultiString. + DWordDataNotInHexFormat = The specified registry key value data {0} is not in the correct hex format to parse as an Int32 (dword). + QWordDataNotInHexFormat = The specified registry key value data {0} is not in the correct hex format to parse as an Int64 (qword). + BinaryDataNotInHexFormat = The specified registry key value data {0} is not in the correct hex format to parse as a Byte array (Binary). + InvalidRegistryDrive = The registry drive {0} is invalid. Please update the Key parameter to include a valid registry drive. + RegistryDriveCouldNotBeMounted = The registry drive with the abbreviation {0} could not be mounted. +'@ diff --git a/deployment/dsc/azshcihost/xPSDesiredStateConfiguration/9.1.0/DSCResources/DSC_xRemoteFile/DSC_xRemoteFile.psm1 b/deployment/dsc/azshcihost/xPSDesiredStateConfiguration/9.1.0/DSCResources/DSC_xRemoteFile/DSC_xRemoteFile.psm1 new file mode 100644 index 0000000..133a8b7 --- /dev/null +++ b/deployment/dsc/azshcihost/xPSDesiredStateConfiguration/9.1.0/DSCResources/DSC_xRemoteFile/DSC_xRemoteFile.psm1 @@ -0,0 +1,851 @@ +$errorActionPreference = 'Stop' +Set-StrictMode -Version 'Latest' + +$modulePath = Join-Path -Path (Split-Path -Path (Split-Path -Path $PSScriptRoot -Parent) -Parent) -ChildPath 'Modules' + +# Import the shared modules +Import-Module -Name (Join-Path -Path $modulePath ` + -ChildPath (Join-Path -Path 'xPSDesiredStateConfiguration.Common' ` + -ChildPath 'xPSDesiredStateConfiguration.Common.psm1')) + +# Import Localization Strings +$script:localizedData = Get-LocalizedData -ResourceName 'DSC_xRemoteFile' + +# Path where cache will be stored. It's cleared whenever LCM gets new configuration. +$script:cacheLocation = "$env:ProgramData\Microsoft\Windows\PowerShell\Configuration\BuiltinProvCache\DSC_xRemoteFile" + +<# + .SYNOPSIS + The Get-TargetResource function is used to fetch the status of file + specified in DestinationPath on the target machine. + + .PARAMETER DestinationPath + Path under which downloaded or copied file should be accessible after + operation. + + .PARAMETER Uri + Uri of a file which should be copied or downloaded. This parameter + supports HTTP and HTTPS values. + + .PARAMETER ChecksumType + The algorithm used to calculate the checksum of the file. +#> +function Get-TargetResource +{ + [CmdletBinding()] + [OutputType([System.Collections.Hashtable])] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $DestinationPath, + + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $Uri, + + [Parameter()] + [System.String] + [ValidateSet('None', 'SHA1', 'SHA256', 'SHA384', 'SHA512', 'MACTripleDES', 'MD5', 'RIPEMD160')] + $ChecksumType = 'None' + + ) + + # Check whether DestinationPath is existing file + $ensure = 'Absent' + $pathItemType = Get-PathItemType -Path $DestinationPath + $checksumValue = '' + + switch ($pathItemType) + { + 'File' + { + Write-Verbose -Message ($script:localizedData.DestinationPathIsExistingFile -f $DestinationPath) + $ensure = 'Present' + + if ($ChecksumType -ine 'None') + { + $getFileHash = Get-FileHash -Path $DestinationPath -Algorithm $ChecksumType + $checksumValue = $getFileHash.Hash + } + } + + 'Directory' + { + Write-Verbose -Message ($script:localizedData.DestinationPathIsExistingPath -f $DestinationPath) + + # If it's existing directory, let's check whether expectedDestinationPath exists + $uriFileName = Split-Path -Path $Uri -Leaf + $expectedDestinationPath = Join-Path -Path $DestinationPath -ChildPath $uriFileName + + if (Test-Path -Path $expectedDestinationPath) + { + Write-Verbose -Message ($script:localizedData.FileExistsInDestinationPath -f $uriFileName) + $ensure = 'Present' + + if ($ChecksumType -ine 'None') + { + $getFileHash = Get-FileHash -Path $expectedDestinationPath -Algorithm $ChecksumType + $checksumValue = $getFileHash.Hash + } + } + } + + 'Other' + { + Write-Verbose -Message ($script:localizedData.DestinationPathUnknownType -f $DestinationPath, $pathItemType) + } + + 'NotExists' + { + Write-Verbose -Message ($script:localizedData.DestinationPathDoesNotExist -f $DestinationPath) + } + } + + return @{ + DestinationPath = $DestinationPath + Uri = $Uri + Ensure = $ensure + Checksum = $checksumValue + } +} + +<# + .SYNOPSIS + The Set-TargetResource function is used to download file found under + Uri location to DestinationPath. Additional parameters can be specified + to configure web request. + + .PARAMETER DestinationPath + Path under which downloaded or copied file should be accessible after + operation. + + .PARAMETER Uri + Uri of a file which should be copied or downloaded. This parameter + supports HTTP and HTTPS values. + + .PARAMETER UserAgent + User agent for the web request. + + .PARAMETER Headers + Headers of the web request. + + .PARAMETER Credential + Specifies a user account that has permission to send the request. + + .PARAMETER MatchSource + A boolean value to indicate whether the remote file should be re-downloaded + if the file in the DestinationPath was modified locally. The default value + is true. + + .PARAMETER TimeoutSec + Specifies how long the request can be pending before it times out. + + .PARAMETER Proxy + Uses a proxy server for the request, rather than connecting directly + to the Internet resource. Should be the URI of a network proxy server + (e.g 'http://10.20.30.1'). + + .PARAMETER ProxyCredential + Specifies a user account that has permission to use the proxy server that + is specified by the Proxy parameter. + + .PARAMETER Checksum + Specifies the expected checksum value of downloaded file. + + .PARAMETER ChecksumType + The algorithm used to calculate the checksum of the file. +#> +function Set-TargetResource +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $DestinationPath, + + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $Uri, + + [Parameter()] + [System.String] + $UserAgent, + + [Parameter()] + [Microsoft.Management.Infrastructure.CimInstance[]] + $Headers, + + [Parameter()] + [System.Management.Automation.Credential()] + [System.Management.Automation.PSCredential] + $Credential, + + [Parameter()] + [System.Boolean] + $MatchSource = $true, + + [Parameter()] + [System.Uint32] + $TimeoutSec, + + [Parameter()] + [System.String] + $Proxy, + + [Parameter()] + [System.Management.Automation.Credential()] + [System.Management.Automation.PSCredential] + $ProxyCredential, + + [Parameter()] + [System.String] + [ValidateSet('None', 'SHA1', 'SHA256', 'SHA384', 'SHA512', 'MACTripleDES', 'MD5', 'RIPEMD160')] + $ChecksumType = 'None', + + [Parameter()] + [System.String] + $Checksum + ) + + # Validate Uri + if (-not (Test-UriScheme -Uri $Uri -Scheme 'http|https|file')) + { + $errorMessage = $script:localizedData.InvalidWebUriError -f $Uri + New-InvalidDataException ` + -ErrorId 'UriValidationFailure' ` + -ErrorMessage $errorMessage + } + + # Validate DestinationPath scheme + if (-not (Test-UriScheme -Uri $DestinationPath -Scheme 'file')) + { + $errorMessage = $script:localizedData.InvalidDestinationPathSchemeError -f $DestinationPath + New-InvalidDataException ` + -ErrorId 'DestinationPathSchemeValidationFailure' ` + -ErrorMessage $errorMessage + } + + # Validate DestinationPath is not UNC path + if ($DestinationPath.StartsWith('\\')) + { + $errorMessage = $script:localizedData.DestinationPathIsUncError -f $DestinationPath + New-InvalidDataException ` + -ErrorId 'DestinationPathIsUncFailure' ` + -ErrorMessage $errorMessage + } + + # Validate DestinationPath does not contain invalid characters + @('*', '?', '"', '<', '>', '|') | ForEach-Object -Process { + if ($DestinationPath.Contains($_)) + { + $errorMessage = $script:localizedData.DestinationPathHasInvalidCharactersError -f $DestinationPath + New-InvalidDataException ` + -ErrorId 'DestinationPathHasInvalidCharactersError' ` + -ErrorMessage $errorMessage + } + } + + # Validate DestinationPath does not end with / or \ (Invoke-WebRequest requirement) + if ($DestinationPath.EndsWith('/') -or $DestinationPath.EndsWith('\')) + { + $errorMessage = $script:localizedData.DestinationPathEndsWithInvalidCharacterError -f $DestinationPath + New-InvalidDataException ` + -ErrorId 'DestinationPathEndsWithInvalidCharacterError' ` + -ErrorMessage $errorMessage + } + + # Check whether DestinationPath's parent directory exists. Create if it doesn't. + $destinationPathParent = Split-Path -Path $DestinationPath -Parent + + if (-not (Test-Path $destinationPathParent)) + { + $null = New-Item -ItemType Directory -Path $destinationPathParent -Force + } + + # Check whether DestinationPath's leaf is an existing folder + $uriFileName = Split-Path -Path $Uri -Leaf + + if (Test-Path $DestinationPath -PathType Container) + { + $DestinationPath = Join-Path -Path $DestinationPath -ChildPath $uriFileName + } + + # Remove ChecksumType and Checksum from parameters as they are not parameters of Invoke-WebRequest. + $null = $PSBoundParameters.Remove('ChecksumType') + $null = $PSBoundParameters.Remove('Checksum') + + # Remove DestinationPath and MatchSource from parameters as they are not parameters of Invoke-WebRequest + $null = $PSBoundParameters.Remove('DestinationPath') + $null = $PSBoundParameters.Remove('MatchSource') + + # Convert headers to hashtable + $null = $PSBoundParameters.Remove('Headers') + $headersHashtable = $null + + if ($null -ne $Headers) + { + $headersHashtable = Convert-KeyValuePairArrayToHashtable -Array $Headers + } + + # Invoke web request + try + { + $currentProgressPreference = $ProgressPreference + $ProgressPreference = 'SilentlyContinue' + + Write-Verbose -Message ($script:localizedData.DownloadingURI -f $DestinationPath, $URI) + $count = 0 + $success = $false + + do + { + try + { + $count++ + Invoke-WebRequest ` + @PSBoundParameters ` + -Headers $headersHashtable ` + -OutFile $DestinationPath + $success = $true + } + catch [System.Exception] + { + Write-Verbose -Message ($script:localizedData.DownloadingFailedRetry -f $URI, $count, $_.Exception.Message) + + if ($count -gt 5) + { + # Inside catch variable $_ is not the exception itself, but a System.Management.Automation.ErrorRecord that contains the actual Exception + throw $_.Exception + } + + Start-Sleep -Seconds 5 + } + } + while ($success -eq $false) + } + catch [System.OutOfMemoryException] + { + $errorMessage = $script:localizedData.DownloadOutOfMemoryException -f $_ + New-InvalidDataException ` + -ErrorId 'SystemOutOfMemoryException' ` + -ErrorMessage $errorMessage + } + catch [System.Exception] + { + $errorMessage = $script:localizedData.DownloadException -f $_ + New-InvalidDataException ` + -ErrorId 'SystemException' ` + -ErrorMessage $errorMessage + } + finally + { + $ProgressPreference = $currentProgressPreference + } + + # Check checksum + if ($ChecksumType -ine 'None' -and -not [String]::IsNullOrEmpty($Checksum)) + { + $fileHashSplat = @{ + Path = $DestinationPath + Algorithm = $ChecksumType + } + + $getFileHash = Get-FileHash @fileHashSplat + $fileHash = $getFileHash.Hash + + if ($fileHash -ine $Checksum) + { + # the checksum failed + $errorMessage = $script:localizedData.ChecksumDoesNotMatch -f $Checksum, $fileHash + New-InvalidDataException ` + -ErrorId 'ChecksumDoesNotMatch' ` + -ErrorMessage $errorMessage + } + } + + # Update cache + if (Test-Path -Path $DestinationPath) + { + $downloadedFile = Get-Item -Path $DestinationPath + $lastWriteTime = $downloadedFile.LastWriteTimeUtc + $filesize = $downloadedFile.Length + $inputObject = @{ } + $inputObject['LastWriteTime'] = $lastWriteTime + $inputObject['FileSize'] = $filesize + Update-Cache -DestinationPath $DestinationPath -Uri $Uri -InputObject $inputObject + } +} + +<# + .SYNOPSIS + The Test-TargetResource function is used to validate if the DestinationPath + exists on the machine. + + .PARAMETER DestinationPath + Path under which downloaded or copied file should be accessible after + operation. + + .PARAMETER Uri + Uri of a file which should be copied or downloaded. This parameter + supports HTTP and HTTPS values. + + .PARAMETER UserAgent + User agent for the web request. + + .PARAMETER Headers + Headers of the web request. + + .PARAMETER Credential + Specifies a user account that has permission to send the request. + + .PARAMETER MatchSource + A boolean value to indicate whether the remote file should be re-downloaded + if the file in the DestinationPath was modified locally. The default value + is true. + + .PARAMETER TimeoutSec + Specifies how long the request can be pending before it times out. + + .PARAMETER Proxy + Uses a proxy server for the request, rather than connecting directly + to the Internet resource. Should be the URI of a network proxy server + (e.g 'http://10.20.30.1'). + + .PARAMETER ProxyCredential + Specifies a user account that has permission to use the proxy server that + is specified by the Proxy parameter. + + .PARAMETER Checksum + Specifies the expected checksum value of downloaded file. + + .PARAMETER ChecksumType + The algorithm used to calculate the checksum of the file. +#> +function Test-TargetResource +{ + [CmdletBinding()] + [OutputType([System.Boolean])] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $DestinationPath, + + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $Uri, + + [Parameter()] + [System.String] + $UserAgent, + + [Parameter()] + [Microsoft.Management.Infrastructure.CimInstance[]] + $Headers, + + [Parameter()] + [System.Management.Automation.Credential()] + [System.Management.Automation.PSCredential] + $Credential, + + [Parameter()] + [System.Boolean] + $MatchSource = $true, + + [Parameter()] + [System.Uint32] + $TimeoutSec, + + [Parameter()] + [System.String] + $Proxy, + + [Parameter()] + [System.Management.Automation.Credential()] + [System.Management.Automation.PSCredential] + $ProxyCredential, + + [Parameter()] + [System.String] + [ValidateSet('None', 'SHA1', 'SHA256', 'SHA384', 'SHA512', 'MACTripleDES', 'MD5', 'RIPEMD160')] + $ChecksumType = 'None', + + [Parameter()] + [System.String] + $Checksum + ) + + # Check whether DestinationPath points to existing file or directory + $fileExists = $false + $uriFileName = Split-Path -Path $Uri -Leaf + $pathItemType = Get-PathItemType -Path $DestinationPath + + switch ($pathItemType) + { + 'File' + { + Write-Verbose -Message ($script:localizedData.DestinationPathIsExistingFile -f $DestinationPath) + + if ($MatchSource) + { + $file = Get-Item -Path $DestinationPath + # Getting cache. It's cleared every time user runs Start-DscConfiguration + $cache = Get-Cache -DestinationPath $DestinationPath -Uri $Uri + + if ($null -ne $cache ` + -and ($cache.LastWriteTime -eq $file.LastWriteTimeUtc) ` + -and ($cache.FileSize -eq $file.Length)) + { + Write-Verbose -Message $script:localizedData.CacheReflectsCurrentState + $fileExists = $true + } + else + { + Write-Verbose -Message $script:localizedData.CacheIsEmptyOrNotMatchCurrentState + } + } + else + { + Write-Verbose -Message $script:localizedData.MatchSourceFalse + $fileExists = $true + } + + if ($ChecksumType -ine 'None' ` + -and -not [String]::IsNullOrEmpty($Checksum) ` + -and $fileExists -eq $true) + { + $fileHashSplat = @{ + Path = $DestinationPath + Algorithm = $ChecksumType + } + $getFileHash = Get-FileHash @fileHashSplat + $fileHash = $getFileHash.Hash + + if ($fileHash -ieq $Checksum) + { + $fileExists = $true + } + else + { + # The checksum does not match. The file may match what is in the cached data. Resetting it to false. + $fileExists = $false + } + } + } + + 'Directory' + { + Write-Verbose -Message ($script:localizedData.DestinationPathIsExistingPath -f $DestinationPath) + + $expectedDestinationPath = Join-Path -Path $DestinationPath -ChildPath $uriFileName + + if (Test-Path -Path $expectedDestinationPath) + { + if ($MatchSource) + { + $file = Get-Item -Path $expectedDestinationPath + $cache = Get-Cache -DestinationPath $expectedDestinationPath -Uri $Uri + + if ($null -ne $cache -and ($cache.LastWriteTime -eq $file.LastWriteTimeUtc)) + { + Write-Verbose -Message $script:localizedData.CacheReflectsCurrentState + $fileExists = $true + } + else + { + Write-Verbose -Message $script:localizedData.CacheIsEmptyOrNotMatchCurrentState + } + } + else + { + Write-Verbose -Message $script:localizedData.MatchSourceFalse + $fileExists = $true + } + + if ($ChecksumType -ine 'None' ` + -and -not [String]::IsNullOrEmpty($Checksum) ` + -and $fileExists -eq $true) + { + $fileHashSplat = @{ + Path = $expectedDestinationPath + Algorithm = $ChecksumType + } + $getFileHash = Get-FileHash @fileHashSplat + $fileHash = $getFileHash.Hash + + if ($fileHash -ieq $Checksum) + { + $fileExists = $true + } + else + { + # The checksum does not match. The file may match what is in the cached data. Resetting it to false. + $fileExists = $false + } + } + } + } + + 'Other' + { + Write-Verbose -Message ($script:localizedData.DestinationPathUnknownType -f $DestinationPath, $pathItemType) + } + + 'NotExists' + { + Write-Verbose -Message ($script:localizedData.DestinationPathDoesNotExist -f $DestinationPath) + } + } + + $result = $fileExists + + return $result +} + +<# + .SYNOPSIS + Checks whether given URI represents specific scheme. + + .DESCRIPTION + Most common schemes: file, http, https, ftp + We can also specify logical expressions like: [http|https] + + .PARAMETER Uri + The path of the item to test the scheme of. + + .PARAMETER Scheme + The type of scheme to test the item is. +#> +function Test-UriScheme +{ + [CmdletBinding()] + [OutputType([System.Boolean])] + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $Uri, + + [Parameter(Mandatory = $true)] + [System.String] + $Scheme + ) + + $newUri = $Uri -as [System.URI] + + return ($null -ne $newUri.AbsoluteURI -and $newUri.Scheme -match $Scheme) +} + +<# + .SYNOPSIS + Gets type of the item which path points to. + + .PARAMETER Path + The path of the item to return the item type of. + + .OUTPUTS + File, Directory, Other or NotExists. +#> +function Get-PathItemType +{ + [CmdletBinding()] + [OutputType([System.String])] + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $Path + ) + + $type = $null + + # Check whether path exists + if (Test-Path -Path $path) + { + # Check type of the path + $pathItem = Get-Item -Path $Path + $pathItemType = $pathItem.GetType().Name + + if ($pathItemType -eq 'FileInfo') + { + $type = 'File' + } + elseif ($pathItemType -eq 'DirectoryInfo') + { + $type = 'Directory' + } + else + { + $type = 'Other' + } + } + else + { + $type = 'NotExists' + } + + return $type +} + +<# + .SYNOPSIS + Converts CimInstance array of type KeyValuePair to hashtable + + .PARAMETER Array + The array of KeyValuePairs to convert to a hashtable. +#> +function Convert-KeyValuePairArrayToHashtable +{ + [CmdletBinding()] + [OutputType([System.Collections.Hashtable])] + param + ( + [Parameter(Mandatory = $true)] + [Microsoft.Management.Infrastructure.CimInstance[]] + $Array + ) + + $hashtable = @{ } + + foreach ($item in $Array) + { + $hashtable += @{ + $item.Key = $item.Value + } + } + + return $hashtable +} + +<# + .SYNOPSIS + Gets cache for specific DestinationPath and Uri. + + .PARAMETER DestinationPath + The path to the cache. + + .PARAMETER Uri + The URI of the file to get the cache content for. +#> +function Get-Cache +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $DestinationPath, + + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $Uri + ) + + $cacheContent = $null + $key = Get-CacheKey -DestinationPath $DestinationPath -Uri $Uri + $path = Join-Path -Path $script:cacheLocation -ChildPath $key + + Write-Verbose -Message ($script:localizedData.CacheLookingForPath -f $Path) + + if (-not (Test-Path -Path $path)) + { + Write-Verbose -Message ($script:localizedData.CacheNotFoundForPath -f $DestinationPath, $Uri, $Key) + + $cacheContent = $null + } + else + { + $cacheContent = Import-Clixml -Path $path + Write-Verbose -Message ($script:localizedData.CacheFoundForPath -f $DestinationPath, $Uri, $Key) + } + + return $cacheContent +} + +<# + .SYNOPSIS + Creates or updates cache for specific DestinationPath and Uri. + + .PARAMETER DestinationPath + The path to the cache. + + .PARAMETER Uri + The URI of the file to update the cache for. + + .PARAMETER Uri + The content of the file to update in the cache. +#> +function Update-Cache +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $DestinationPath, + + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $Uri, + + [Parameter(Mandatory = $true)] + [System.Object] + $InputObject + ) + + $key = Get-CacheKey -DestinationPath $DestinationPath -Uri $Uri + $path = Join-Path -Path $script:cacheLocation -ChildPath $key + + if (-not (Test-Path -Path $script:cacheLocation)) + { + $null = New-Item -ItemType Directory -Path $script:cacheLocation + } + + Write-Verbose -Message ($script:localizedData.UpdatingCache -f $DestinationPath, $Uri, $Key) + + Export-Clixml -Path $path -InputObject $InputObject -Force +} + +<# + .SYNOPSIS + Returns cache key for given parameters. + + .PARAMETER DestinationPath + The path to the cache. + + .PARAMETER Uri + The URI of the file to get the cache key for. +#> +function Get-CacheKey +{ + [CmdletBinding()] + [OutputType([System.String])] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $DestinationPath, + + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $Uri + ) + + return [System.String]::Join('', @($DestinationPath, $Uri)).GetHashCode().ToString() +} + +Export-ModuleMember -Function Get-TargetResource, Set-TargetResource, Test-TargetResource diff --git a/deployment/dsc/azshcihost/xPSDesiredStateConfiguration/9.1.0/DSCResources/DSC_xRemoteFile/DSC_xRemoteFile.schema.mof b/deployment/dsc/azshcihost/xPSDesiredStateConfiguration/9.1.0/DSCResources/DSC_xRemoteFile/DSC_xRemoteFile.schema.mof new file mode 100644 index 0000000..b879fdc --- /dev/null +++ b/deployment/dsc/azshcihost/xPSDesiredStateConfiguration/9.1.0/DSCResources/DSC_xRemoteFile/DSC_xRemoteFile.schema.mof @@ -0,0 +1,17 @@ + +[ClassVersion("1.0.0.0"), FriendlyName("xRemoteFile")] +class DSC_xRemoteFile : OMI_BaseResource +{ + [Key, Description("Path under which downloaded or copied file should be accessible after operation.")] String DestinationPath; + [Required, Description("Uri of a file which should be copied or downloaded. This parameter supports HTTP and HTTPS values.")] String Uri; + [Write, Description("User agent for the web request.")] String UserAgent; + [Write, Description("Headers of the web request."), EmbeddedInstance("MSFT_KeyValuePair")] String Headers[]; + [Write, Description("Specifies a user account that has permission to send the request."), EmbeddedInstance("MSFT_Credential")] String Credential; + [Write, Description("A boolean value to indicate whether the remote file should be re-downloaded if the file in the DestinationPath was modified locally. The default value is true.")] Boolean MatchSource; + [Write, Description("Specifies the checksum type to verify the downloaded file"), ValueMap{"None","SHA1","SHA256","SHA384","SHA512","MACTripleDES","MD5","RIPEMD160"}, Values{"None","SHA1","SHA256","SHA384","SHA512","MACTripleDES","MD5","RIPEMD160"}] String ChecksumType; + [Write, Description("Specifies the checksum value to compare against the downloaded file")] String Checksum; + [Write, Description("Specifies how long the request can be pending before it times out.")] Uint32 TimeoutSec; + [Write, Description("Uses a proxy server for the request, rather than connecting directly to the Internet resource. Should be the URI of a network proxy server (e.g 'http://10.20.30.1').")] String Proxy; + [Write, Description("Specifies a user account that has permission to use the proxy server that is specified by the Proxy parameter."), EmbeddedInstance("MSFT_Credential")] String ProxyCredential; + [Read, Description("Says whether DestinationPath exists on the machine."), ValueMap{"Present", "Absent"}, Values{"Present", "Absent"}] String Ensure; +}; diff --git a/deployment/dsc/azshcihost/xPSDesiredStateConfiguration/9.1.0/DSCResources/DSC_xRemoteFile/en-us/DSC_xRemoteFile.strings.psd1 b/deployment/dsc/azshcihost/xPSDesiredStateConfiguration/9.1.0/DSCResources/DSC_xRemoteFile/en-us/DSC_xRemoteFile.strings.psd1 new file mode 100644 index 0000000..34db460 --- /dev/null +++ b/deployment/dsc/azshcihost/xPSDesiredStateConfiguration/9.1.0/DSCResources/DSC_xRemoteFile/en-us/DSC_xRemoteFile.strings.psd1 @@ -0,0 +1,26 @@ +# Localized resources for DSC_xRemoteFile + +ConvertFrom-StringData @' + DestinationPathIsExistingFile=DestinationPath '{0}' is existing file on the machine. + DestinationPathIsExistingPath=DestinationPath '{0}' is existing directory on the machine. + FileExistsInDestinationPath=File '{0}' exists in DestinationPath. + DestinationPathUnknownType=DestinationPath '{0}' has unknown type '{1}'. + DestinationPathDoesNotExist=DestinationPath '{0}' doesn't exist on the machine. + InvalidWebUriError=Specified URI is not valid: "{0}". Only http, https or file paths are accepted. + InvalidDestinationPathSchemeError=Specified DestinationPath is not valid: "{0}". DestinationPath should be absolute path. + DestinationPathIsUncError=Specified DestinationPath is not valid: "{0}". DestinationPath should be local path instead of UNC path. + DestinationPathHasInvalidCharactersError=Specified DestinationPath is not valid: "{0}". DestinationPath should be contains following characters: * ? " < > | + DestinationPathEndsWithInvalidCharacterError=Specified DestinationPath is not valid: "{0}". DestinationPath should not end with / or \\ + DownloadOutOfMemoryException=Invoking web request failed with OutOfMemoryException- Possible cause is the requested file being too big. {0} + DownloadException=Invoking web request failed with error. {0} + DownloadingURI=Downloading '{1}' to '{0}'. + DownloadingFailedRetry=Download of '{0}' failed on attempt {1} with this error: {2} + CacheReflectsCurrentState=Cache reflects current state. No need for downloading file. + CacheIsEmptyOrNotMatchCurrentState=Cache is empty or it doesn't reflect current state. File will be downloaded. + MatchSourceFalse=MatchSource is false. No need for downloading file. + CacheLookingForPath=Looking for cache path '{0}'. + CacheNotFoundForPath=No cache found for DestinationPath '{0}' and Uri '{1}' CacheKey '{2}'. + CacheFoundForPath=Found cache found for DestinationPath '{0}' and Uri '{1}' CacheKey '{2}'. + ChecksumDoesNotMatch=Checksum does not match specified value - Desired: '{0}', Actual: '{1}'. + UpdatingCache=Updating cache for DestinationPath '{0}' and Uri '{1}' CacheKey '{2}'. +'@ diff --git a/deployment/dsc/azshcihost/xPSDesiredStateConfiguration/9.1.0/DSCResources/DSC_xScriptResource/DSC_xScriptResource.psm1 b/deployment/dsc/azshcihost/xPSDesiredStateConfiguration/9.1.0/DSCResources/DSC_xScriptResource/DSC_xScriptResource.psm1 new file mode 100644 index 0000000..3185a5a --- /dev/null +++ b/deployment/dsc/azshcihost/xPSDesiredStateConfiguration/9.1.0/DSCResources/DSC_xScriptResource/DSC_xScriptResource.psm1 @@ -0,0 +1,288 @@ +$errorActionPreference = 'Stop' +Set-StrictMode -Version 'Latest' + +$modulePath = Join-Path -Path (Split-Path -Path (Split-Path -Path $PSScriptRoot -Parent) -Parent) -ChildPath 'Modules' + +# Import the shared modules +Import-Module -Name (Join-Path -Path $modulePath ` + -ChildPath (Join-Path -Path 'xPSDesiredStateConfiguration.Common' ` + -ChildPath 'xPSDesiredStateConfiguration.Common.psm1')) + +# Import Localization Strings +$script:localizedData = Get-LocalizedData -ResourceName 'DSC_xScriptResource' + +<# + .SYNOPSIS + Runs the given get script. + Should return a hashtable. + + .PARAMETER GetScript + A string that can be used to create a PowerShell script block that retrieves the + current state of the resource. + + .PARAMETER SetScript + Not used in Get-TargetResource. + + .PARAMETER TestScript + Not used in Get-TargetResource. + + .PARAMETER Credential + The credential of the user account to run the script under if needed. +#> +function Get-TargetResource +{ + [OutputType([System.Collections.Hashtable])] + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $GetScript, + + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $SetScript, + + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $TestScript, + + [Parameter()] + [ValidateNotNull()] + [System.Management.Automation.PSCredential] + [System.Management.Automation.Credential()] + $Credential + ) + + Write-Verbose -Message $script:localizedData.GetTargetResourceStartVerboseMessage + + $invokeScriptParameters = @{ + ScriptBlock = [System.Management.Automation.ScriptBlock]::Create($GetScript) + } + + if ($PSBoundParameters.ContainsKey('Credential')) + { + $invokeScriptParameters['Credential'] = $Credential + } + + $invokeScriptResult = Invoke-Script @invokeScriptParameters + + if ($invokeScriptResult -is [System.Management.Automation.ErrorRecord]) + { + New-InvalidOperationException -Message $script:localizedData.GetScriptThrewError -ErrorRecord $invokeScriptResult + } + + $invokeScriptResultAsHashTable = $invokeScriptResult -as [System.Collections.Hashtable] + + if ($null -eq $invokeScriptResultAsHashTable) + { + New-InvalidArgumentException -ArgumentName 'TestScript' -Message $script:localizedData.GetScriptDidNotReturnHashtable + } + + Write-Verbose -Message $script:localizedData.GetTargetResourceEndVerboseMessage + + return $invokeScriptResultAsHashTable +} + +<# + .SYNOPSIS + Runs the given set script. + Should not return. + + .PARAMETER GetScript + Not used in Set-TargetResource. + + .PARAMETER SetScript + A string that can be used to create a PowerShell script block that sets the resource + to the desired state. + + .PARAMETER TestScript + Not used in Set-TargetResource. + + .PARAMETER Credential + The credential of the user account to run the script under if needed. +#> +function Set-TargetResource +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $GetScript, + + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $SetScript, + + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $TestScript, + + [Parameter()] + [ValidateNotNull()] + [System.Management.Automation.PSCredential] + [System.Management.Automation.Credential()] + $Credential + ) + + Write-Verbose -Message $script:localizedData.SetTargetResourceStartVerboseMessage + + $invokeScriptParameters = @{ + ScriptBlock = [System.Management.Automation.ScriptBlock]::Create($SetScript) + } + + if ($PSBoundParameters.ContainsKey('Credential')) + { + $invokeScriptParameters['Credential'] = $Credential + } + + $invokeScriptResult = Invoke-Script @invokeScriptParameters + + if ($invokeScriptResult -is [System.Management.Automation.ErrorRecord]) + { + New-InvalidOperationException -Message $script:localizedData.SetScriptThrewError -ErrorRecord $invokeScriptResult + } + + Write-Verbose -Message $script:localizedData.SetTargetResourceEndVerboseMessage +} + +<# + .SYNOPSIS + Runs the given test script. + Should return true if the resource is in the desired state and false otherwise. + + .PARAMETER GetScript + Not used in Test-TargetResource. + + .PARAMETER SetScript + Not used in Test-TargetResource. + + .PARAMETER TestScript + A string that can be used to create a PowerShell script block that validates whether + or not the resource is in the desired state. + + .PARAMETER Credential + The credential of the user account to run the script under if needed. +#> +function Test-TargetResource +{ + [OutputType([System.Boolean])] + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $GetScript, + + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $SetScript, + + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $TestScript, + + [Parameter()] + [ValidateNotNull()] + [System.Management.Automation.PSCredential] + [System.Management.Automation.Credential()] + $Credential + ) + + Write-Verbose -Message $script:localizedData.TestTargetResourceStartVerboseMessage + + $invokeScriptParameters = @{ + ScriptBlock = [System.Management.Automation.ScriptBlock]::Create($TestScript) + } + + if ($PSBoundParameters.ContainsKey('Credential')) + { + $invokeScriptParameters['Credential'] = $Credential + } + + $invokeScriptResult = Invoke-Script @invokeScriptParameters + + # If the script is returing multiple objects, then we consider the last object to be the result of the script execution. + if ($invokeScriptResult -is [System.Object[]] -and $invokeScriptResult.Count -gt 0) + { + $invokeScriptResult = $invokeScriptResult[$invokeScriptResult.Count - 1] + } + + if ($invokeScriptResult -is [System.Management.Automation.ErrorRecord]) + { + New-InvalidOperationException -Message $script:localizedData.TestScriptThrewError -ErrorRecord $invokeScriptResult + } + + if ($null -eq $invokeScriptResult -or -not ($invokeScriptResult -is [System.Boolean])) + { + New-InvalidArgumentException -ArgumentName 'TestScript' -Message $script:localizedData.TestScriptDidNotReturnBoolean + } + + Write-Verbose -Message $script:localizedData.TestTargetResourceEndVerboseMessage + + return $invokeScriptResult +} + +<# + .SYNOPSIS + Invokes the given script block. + + The output of the script will be returned unless the script throws an error. + If the script throws an error, the ErrorRecord will be returned rather than thrown. + + .PARAMETER ScriptBlock + The script block to invoke. + + .PARAMETER Credential + The credential to run the script under if needed. +#> +function Invoke-Script +{ + [OutputType([System.Object])] + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [System.Management.Automation.ScriptBlock] + $ScriptBlock, + + [Parameter()] + [ValidateNotNull()] + [System.Management.Automation.PSCredential] + [System.Management.Automation.Credential()] + $Credential + ) + + $scriptResult = $null + + try + { + Write-Verbose -Message ($script:localizedData.ExecutingScriptMessage -f $ScriptBlock) + + if ($null -ne $Credential) + { + $scriptResult = Invoke-Command -ScriptBlock $ScriptBlock -Credential $Credential -ComputerName . + } + else + { + $scriptResult = & $ScriptBlock + } + } + catch + { + # Surfacing the error thrown by the execution of the script + $scriptResult = $_ + } + + return $scriptResult +} diff --git a/deployment/dsc/azshcihost/xPSDesiredStateConfiguration/9.1.0/DSCResources/DSC_xScriptResource/DSC_xScriptResource.schema.mof b/deployment/dsc/azshcihost/xPSDesiredStateConfiguration/9.1.0/DSCResources/DSC_xScriptResource/DSC_xScriptResource.schema.mof new file mode 100644 index 0000000..c42994d --- /dev/null +++ b/deployment/dsc/azshcihost/xPSDesiredStateConfiguration/9.1.0/DSCResources/DSC_xScriptResource/DSC_xScriptResource.schema.mof @@ -0,0 +1,10 @@ + +[ClassVersion("1.0.0"),FriendlyName("xScript")] +class DSC_xScriptResource : OMI_BaseResource +{ + [Key, Description("A string that can be used to create a PowerShell script block that retrieves the current state of the resource.")] String GetScript; + [Key, Description("A string that can be used to create a PowerShell script block that sets the resource to the desired state.")] String SetScript; + [Key, Description("A string that can be used to create a PowerShell script block that validates whether or not the resource is in the desired state.")] String TestScript; + [Write, Description("The credential of the user account to run the script under if needed."), EmbeddedInstance("MSFT_Credential")] String Credential; + [Read, Description("The result from the GetScript script block.")] String Result; +}; diff --git a/deployment/dsc/azshcihost/xPSDesiredStateConfiguration/9.1.0/DSCResources/DSC_xScriptResource/en-US/DSC_xScriptResource.schema.mfl b/deployment/dsc/azshcihost/xPSDesiredStateConfiguration/9.1.0/DSCResources/DSC_xScriptResource/en-US/DSC_xScriptResource.schema.mfl new file mode 100644 index 0000000..17e6515 --- /dev/null +++ b/deployment/dsc/azshcihost/xPSDesiredStateConfiguration/9.1.0/DSCResources/DSC_xScriptResource/en-US/DSC_xScriptResource.schema.mfl @@ -0,0 +1,9 @@ +[Description("This resource is used to perform Get, Set and Test functionality on the DSC managed nodes through Powershell scripts. \n") : Amended,AMENDMENT, LOCALE("ms_409")] +class DSC_xScriptResource : OMI_BaseResource +{ + [Key,Description("A string that can be used to create a PowerShell script block that retrieves the current state of the resource.") : Amended] String GetScript; + [Key,Description("A string that can be used to create a PowerShell script block that sets the resource to the desired state.") : Amended] String SetScript; + [Key,Description("A string that can be used to create a PowerShell script block that validates whether or not the resource is in the desired state.") : Amended] String TestScript; + [Description("The credential of the user account to run the script under if needed.") : Amended] String Credential; + [Read, Description("The result from the GetScript script block.") : Amended] String DisplayName; +}; diff --git a/deployment/dsc/azshcihost/xPSDesiredStateConfiguration/9.1.0/DSCResources/DSC_xScriptResource/en-US/DSC_xScriptResource.strings.psd1 b/deployment/dsc/azshcihost/xPSDesiredStateConfiguration/9.1.0/DSCResources/DSC_xScriptResource/en-US/DSC_xScriptResource.strings.psd1 new file mode 100644 index 0000000..db44aca --- /dev/null +++ b/deployment/dsc/azshcihost/xPSDesiredStateConfiguration/9.1.0/DSCResources/DSC_xScriptResource/en-US/DSC_xScriptResource.strings.psd1 @@ -0,0 +1,16 @@ +# Localized resources for DSC_xScriptResource + +ConvertFrom-StringData @' + GetTargetResourceStartVerboseMessage = Begin executing get script. + GetScriptThrewError = The get script threw an error. + GetScriptDidNotReturnHashtable = The get script did not return a hashtable. + GetTargetResourceEndVerboseMessage = End executing get script. + SetTargetResourceStartVerboseMessage = Begin executing set script. + SetScriptThrewError = The set script threw an error. + SetTargetResourceEndVerboseMessage = End executing set script. + TestTargetResourceStartVerboseMessage = Begin executing test script. + TestScriptThrewError = The test script threw an error. + TestScriptDidNotReturnBoolean = The test script did not return a boolean. + TestTargetResourceEndVerboseMessage = End executing test script. + ExecutingScriptMessage = Executing script: {0} +'@ diff --git a/deployment/dsc/azshcihost/xPSDesiredStateConfiguration/9.1.0/DSCResources/DSC_xServiceResource/DSC_xServiceResource.psm1 b/deployment/dsc/azshcihost/xPSDesiredStateConfiguration/9.1.0/DSCResources/DSC_xServiceResource/DSC_xServiceResource.psm1 new file mode 100644 index 0000000..5a29d06 --- /dev/null +++ b/deployment/dsc/azshcihost/xPSDesiredStateConfiguration/9.1.0/DSCResources/DSC_xServiceResource/DSC_xServiceResource.psm1 @@ -0,0 +1,1870 @@ +<# + Error codes and their meanings for Invoke-CimMethod on a Win32_Service can be found here: + https://msdn.microsoft.com/en-us/library/aa384901(v=vs.85).aspx +#> + +$errorActionPreference = 'Stop' +Set-StrictMode -Version 'Latest' + +$modulePath = Join-Path -Path (Split-Path -Path (Split-Path -Path $PSScriptRoot -Parent) -Parent) -ChildPath 'Modules' + +# Import the shared modules +Import-Module -Name (Join-Path -Path $modulePath ` + -ChildPath (Join-Path -Path 'xPSDesiredStateConfiguration.Common' ` + -ChildPath 'xPSDesiredStateConfiguration.Common.psm1')) + +# Import Localization Strings +$script:localizedData = Get-LocalizedData -ResourceName 'DSC_xServiceResource' + +<# + .SYNOPSIS + Retrieves the current status of the service resource with the given name. + + .PARAMETER Name + The name of the service to retrieve the status of. + + This may be different from the service's display name. + To retrieve a list of all services with their names and current states, use the Get-Service + cmdlet. + + .NOTES + BuiltInAccount, Credential and GroupManagedServiceAccount parameters output the user used + to run the service to the BuiltinAccount property, Evaluating if the account is a gMSA would + mean doing a call to active directory to verify, as the property returned by the ciminstance + is just a string. In a production scenario that would mean that every xService test will check + with AD every 15 minutes if the account is a gMSA. That's not desireable, so we output Credential + and GroupManagedServiceAccount without evaluating what kind of user is supplied. +#> +function Get-TargetResource +{ + [OutputType([System.Collections.Hashtable])] + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $Name + ) + + $service = Get-Service -Name $Name -ErrorAction 'SilentlyContinue' + + if ($null -ne $service) + { + Write-Verbose -Message ($script:localizedData.ServiceExists -f $Name) + + $serviceCimInstance = Get-ServiceCimInstance -ServiceName $Name + + $dependencies = @() + + foreach ($serviceDependedOn in $service.ServicesDependedOn) + { + if ($null -ne $serviceDependedOn -and $null -ne $serviceDependedOn.Name) + { + $dependencies += $serviceDependedOn.Name.ToString() + } + else + { + Write-Warning -Message ($script:localizedData.CorruptDependency -f $Name) + } + } + + $startupType = ConvertTo-StartupTypeString -StartMode $serviceCimInstance.StartMode + + $builtInAccount = switch ($serviceCimInstance.StartName) + { + 'NT Authority\NetworkService' { 'NetworkService'; break } + 'NT Authority\LocalService' { 'LocalService'; break } + default { $serviceCimInstance.StartName } + } + + $serviceResource = @{ + Name = $Name + Ensure = 'Present' + Path = $serviceCimInstance.PathName + StartupType = $startupType + BuiltInAccount = $builtInAccount + State = $service.Status.ToString() + DisplayName = $service.DisplayName + Description = $serviceCimInstance.Description + DesktopInteract = $serviceCimInstance.DesktopInteract + Dependencies = $dependencies + } + } + else + { + Write-Verbose -Message ($script:localizedData.ServiceDoesNotExist -f $Name) + $serviceResource = @{ + Name = $Name + Ensure = 'Absent' + } + } + + return $serviceResource +} + +<# + .SYNOPSIS + Creates, modifies, or deletes the service with the given name. + + .PARAMETER Name + The name of the service to create, modify, or delete. + + This may be different from the service's display name. + To retrieve a list of all services with their names and current states, use the Get-Service + cmdlet. + + .PARAMETER Ensure + Specifies whether the service should exist or not. + + Set this property to Present to create or modify a service. + Set this property to Absent to delete a service. + + The default value is Present. + + .PARAMETER Path + The path to the executable the service should run. + Required when creating a service. + + The user account specified by BuiltInAccount or Credential must have access to this path in + order to start the service. + + .PARAMETER StartupType + The startup type the service should have. + + .PARAMETER BuiltInAccount + The built-in account the service should start under. + + Cannot be specified at the same time as Credential or GroupManagedServiceAccount. + + The user account specified by this property must have access to the service executable path + defined by Path in order to start the service. + + .PARAMETER GroupManagedServiceAccount + The Group Managed Service Account the service should start under. The GMSA + must be provided in DOMAIN\gMSA$ format or UPN format gMSA$@domain.fqdn. + + Cannot be specified at the same time as BuilInAccount or Credential. + + .PARAMETER DesktopInteract + Indicates whether or not the service should be able to communicate with a window on the + desktop. + + Must be false for services not running as LocalSystem. + The default value is false. + + .PARAMETER State + The state the service should be in. + The default value is Running. + + To disregard the state that the service is in, specify this property as Ignore. + + .PARAMETER DisplayName + The display name the service should have. + + .PARAMETER Description + The description the service should have. + + .PARAMETER Dependencies + An array of the names of the dependencies the service should have. + + .PARAMETER StartupTimeout + The time to wait for the service to start in milliseconds. + The default value is 30000 (30 seconds). + + .PARAMETER TerminateTimeout + The time to wait for the service to stop in milliseconds. + The default value is 30000 (30 seconds). + + .PARAMETER Credential + The credential of the user account the service should start under. + + Cannot be specified at the same time as BuiltInAccount. + The user specified by this credential will automatically be granted the Log on as a Service + right. + + The user account specified by this property must have access to the service executable path + defined by Path in order to start the service. + + .NOTES + SupportsShouldProcess is enabled because Invoke-CimMethod calls ShouldProcess. + Here are the paths through which Set-TargetResource calls Invoke-CimMethod: + + Set-TargetResource --> Set-ServicePath --> Invoke-CimMethod + --> Set-ServiceProperty --> Set-ServiceDependency --> Invoke-CimMethod + --> Set-ServiceAccountProperty --> Invoke-CimMethod + --> Set-ServiceStartupType --> Invoke-CimMethod +#> +function Set-TargetResource +{ + [CmdletBinding(SupportsShouldProcess = $true)] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $Name, + + [Parameter()] + [ValidateSet('Present', 'Absent')] + [System.String] + $Ensure = 'Present', + + [Parameter()] + [ValidateNotNullOrEmpty()] + [System.String] + $Path, + + [Parameter()] + [ValidateSet('Automatic', 'Manual', 'Disabled')] + [System.String] + $StartupType, + + [Parameter()] + [ValidateSet('LocalSystem', 'LocalService', 'NetworkService')] + [System.String] + $BuiltInAccount, + + [Parameter()] + [System.String] + $GroupManagedServiceAccount, + + [Parameter()] + [ValidateSet('Running', 'Stopped', 'Ignore')] + [System.String] + $State = 'Running', + + [Parameter()] + [System.Boolean] + $DesktopInteract = $false, + + [Parameter()] + [ValidateNotNullOrEmpty()] + [System.String] + $DisplayName, + + [Parameter()] + [ValidateNotNullOrEmpty()] + [System.String] + $Description, + + [Parameter()] + [System.String[]] + [AllowEmptyCollection()] + $Dependencies, + + [Parameter()] + [System.UInt32] + $StartupTimeout = 30000, + + [Parameter()] + [System.UInt32] + $TerminateTimeout = 30000, + + [Parameter()] + [ValidateNotNull()] + [System.Management.Automation.PSCredential] + [System.Management.Automation.Credential()] + $Credential + ) + + if ($PSBoundParameters.ContainsKey('StartupType')) + { + Assert-NoStartupTypeStateConflict -ServiceName $Name -StartupType $StartupType -State $State + } + + if (($PSBoundParameters.ContainsKey('BuiltInAccount') -and $PSBoundParameters.ContainsKey('Credential')) -or + ($PSBoundParameters.ContainsKey('BuiltInAccount') -and $PSBoundParameters.ContainsKey('GroupManagedServiceAccount')) -or + ($PSBoundParameters.ContainsKey('GroupManagedServiceAccount') -and $PSBoundParameters.ContainsKey('Credential')) + ) + { + $errorMessage = $script:localizedData.CredentialParametersAreMutallyExclusive -f $Name + New-InvalidArgumentException -ArgumentName 'BuiltInAccount / Credential / GroupManagedServiceAccount' -Message $errorMessage + } + + $service = Get-Service -Name $Name -ErrorAction 'SilentlyContinue' + + if ($Ensure -eq 'Absent') + { + if ($null -eq $service) + { + Write-Verbose -Message $script:localizedData.ServiceAlreadyAbsent + } + else + { + Write-Verbose -Message ($script:localizedData.RemovingService -f $Name) + + Stop-ServiceWithTimeout -ServiceName $Name -TerminateTimeout $TerminateTimeout + Remove-ServiceWithTimeout -Name $Name -TerminateTimeout $TerminateTimeout + } + } + else + { + $serviceRestartNeeded = $false + + # Create new service or update the service path if needed + if ($null -eq $service) + { + if ($PSBoundParameters.ContainsKey('Path')) + { + Write-Verbose -Message ($script:localizedData.CreatingService -f $Name, $Path) + $null = New-Service -Name $Name -BinaryPathName $Path + } + else + { + $errorMessage = $script:localizedData.ServiceDoesNotExistPathMissingError -f $Name + New-InvalidArgumentException -ArgumentName 'Path' -Message $errorMessage + } + } + else + { + if ($PSBoundParameters.ContainsKey('Path')) + { + $serviceRestartNeeded = Set-ServicePath -ServiceName $Name -Path $Path + } + } + + # Update the properties of the service if needed + $setServicePropertyParameters = @{} + + $servicePropertyParameterNames = @( 'StartupType', 'BuiltInAccount', 'Credential', 'GroupManagedServiceAccount', 'DesktopInteract', 'DisplayName', 'Description', 'Dependencies' ) + + foreach ($servicePropertyParameterName in $servicePropertyParameterNames) + { + if ($PSBoundParameters.ContainsKey($servicePropertyParameterName)) + { + $setServicePropertyParameters[$servicePropertyParameterName] = $PSBoundParameters[$servicePropertyParameterName] + } + } + + if ($setServicePropertyParameters.Count -gt 0) + { + Write-Verbose -Message ($script:localizedData.EditingServiceProperties -f $Name) + Set-ServiceProperty -ServiceName $Name @setServicePropertyParameters + } + + # Update service state if needed + if ($State -eq 'Stopped') + { + Stop-ServiceWithTimeout -ServiceName $Name -TerminateTimeout $TerminateTimeout + } + elseif ($State -eq 'Running') + { + if ($serviceRestartNeeded) + { + Write-Verbose -Message ($script:localizedData.RestartingService -f $Name) + Stop-ServiceWithTimeout -ServiceName $Name -TerminateTimeout $TerminateTimeout + } + + Start-ServiceWithTimeout -ServiceName $Name -StartupTimeout $StartupTimeout + } + } +} + +<# + .SYNOPSIS + Tests if the service with the given name has the specified property values. + + .PARAMETER Name + The name of the service to test. + + This may be different from the service's display name. + To retrieve a list of all services with their names and current states, use the Get-Service + cmdlet. + + .PARAMETER Ensure + Specifies whether the service should exist or not. + + Set this property to Present to test if a service exists. + Set this property to Absent to test if a service does not exist. + + The default value is Present. + + .PARAMETER Path + The path to the executable the service should be running. + + .PARAMETER StartupType + The startup type the service should have. + + .PARAMETER BuiltInAccount + The built-in account the service should start under. + + Cannot be specified at the same time as Credential or GroupManagedServiceAccount. + + .PARAMETER GroupManagedServiceAccount + The Group Managed Service Account the service should start under. The GMSA + must be provided in DOMAIN\gMSA$ format or UPN format gMSA$@domain.fqdn. + + Cannot be specified at the same time as BuilInAccount or Credential. + + .PARAMETER DesktopInteract + Indicates whether or not the service should be able to communicate with a window on the + desktop. + + Should be false for services not running as LocalSystem. + The default value is false. + + .PARAMETER State + The state the service should be in. + The default value is Running. + + To disregard the state that the service is in, specify this property as Ignore. + + .PARAMETER DisplayName + The display name the service should have. + + .PARAMETER Description + The description the service should have. + + .PARAMETER Dependencies + An array of the names of the dependencies the service should have. + + .PARAMETER StartupTimeout + Not used in Test-TargetResource. + + .PARAMETER TerminateTimeout + Not used in Test-TargetResource. + + .PARAMETER Credential + The credential the service should be running under. + + Cannot be specified at the same time as BuiltInAccount. +#> +function Test-TargetResource +{ + [OutputType([System.Boolean])] + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $Name, + + [Parameter()] + [ValidateSet('Present', 'Absent')] + [System.String] + $Ensure = 'Present', + + [Parameter()] + [ValidateNotNullOrEmpty()] + [System.String] + $Path, + + [Parameter()] + [ValidateSet('Automatic', 'Manual', 'Disabled')] + [System.String] + $StartupType, + + [Parameter()] + [ValidateSet('LocalSystem', 'LocalService', 'NetworkService')] + [System.String] + $BuiltInAccount, + + [Parameter()] + [System.String] + $GroupManagedServiceAccount, + + [Parameter()] + [System.Boolean] + $DesktopInteract = $false, + + [Parameter()] + [ValidateSet('Running', 'Stopped', 'Ignore')] + [System.String] + $State = 'Running', + + [Parameter()] + [ValidateNotNull()] + [System.String] + $DisplayName, + + [Parameter()] + [System.String] + [AllowEmptyString()] + $Description, + + [Parameter()] + [System.String[]] + [AllowEmptyCollection()] + $Dependencies, + + [Parameter()] + [System.UInt32] + $StartupTimeout = 30000, + + [Parameter()] + [System.UInt32] + $TerminateTimeout = 30000, + + [Parameter()] + [ValidateNotNull()] + [System.Management.Automation.PSCredential] + [System.Management.Automation.Credential()] + $Credential + ) + + if ($PSBoundParameters.ContainsKey('StartupType')) + { + Assert-NoStartupTypeStateConflict -ServiceName $Name -StartupType $StartupType -State $State + } + + if (($PSBoundParameters.ContainsKey('BuiltInAccount') -and $PSBoundParameters.ContainsKey('Credential')) -or + ($PSBoundParameters.ContainsKey('BuiltInAccount') -and $PSBoundParameters.ContainsKey('GroupManagedServiceAccount')) -or + ($PSBoundParameters.ContainsKey('GroupManagedServiceAccount') -and $PSBoundParameters.ContainsKey('Credential')) + ) + { + $errorMessage = $script:localizedData.CredentialParametersAreMutallyExclusive -f $Name + New-InvalidArgumentException -ArgumentName 'BuiltInAccount / Credential / GroupManagedServiceAccount' -Message $errorMessage + } + + $serviceResource = Get-TargetResource -Name $Name + + if ($serviceResource.Ensure -eq 'Absent') + { + Write-Verbose -Message ($script:localizedData.ServiceDoesNotExist -f $Name) + + if ($StartupType -eq 'Disabled') + { + return $true + } + + return ($Ensure -eq 'Absent') + } + else + { + Write-Verbose -Message ($script:localizedData.ServiceExists -f $Name) + + if ($Ensure -eq 'Absent') + { + return $false + } + + # Check the service path + if ($PSBoundParameters.ContainsKey('Path')) + { + $pathsMatch = Test-PathsMatch -ExpectedPath $Path -ActualPath $serviceResource.Path + + if (-not $pathsMatch) + { + Write-Verbose -Message ($script:localizedData.ServicePathDoesNotMatch -f $Name, $Path, $serviceResource.Path) + return $false + } + } + + # Check the service display name + if ($PSBoundParameters.ContainsKey('DisplayName') -and $serviceResource.DisplayName -ine $DisplayName) + { + Write-Verbose -Message ($script:localizedData.ServicePropertyDoesNotMatch -f 'DisplayName', $Name, $DisplayName, $serviceResource.DisplayName) + return $false + } + + # Check the service description + if ($PSBoundParameters.ContainsKey('Description') -and $serviceResource.Description -ine $Description) + { + Write-Verbose -Message ($script:localizedData.ServicePropertyDoesNotMatch -f 'Description', $Name, $Description, $serviceResource.Description) + return $false + } + + # Check the service dependencies + if ($PSBoundParameters.ContainsKey('Dependencies')) + { + $serviceDependenciesDoNotMatch = $false + + if ($null -eq $serviceResource.Dependencies -xor $null -eq $Dependencies) + { + $serviceDependenciesDoNotMatch = $true + } + elseif ($null -ne $serviceResource.Dependencies -and $null -ne $Dependencies) + { + $mismatchedDependencies = Compare-Object -ReferenceObject $serviceResource.Dependencies -DifferenceObject $Dependencies + $serviceDependenciesDoNotMatch = $null -ne $mismatchedDependencies + } + + if ($serviceDependenciesDoNotMatch) + { + $expectedDependenciesString = $Dependencies -join ',' + $actualDependenciesString = $serviceResource.Dependencies -join ',' + + Write-Verbose -Message ($script:localizedData.ServicePropertyDoesNotMatch -f 'Dependencies', $Name, $expectedDependenciesString, $actualDependenciesString) + return $false + } + } + + # Check the service desktop interation setting + if ($PSBoundParameters.ContainsKey('DesktopInteract') -and $serviceResource.DesktopInteract -ine $DesktopInteract) + { + Write-Verbose -Message ($script:localizedData.ServicePropertyDoesNotMatch -f 'DesktopInteract', $Name, $DesktopInteract, $serviceResource.DesktopInteract) + return $false + } + + # Check the service account properties + if ($PSBoundParameters.ContainsKey('BuiltInAccount') -and $serviceResource.BuiltInAccount -ine $BuiltInAccount) + { + Write-Verbose -Message ($script:localizedData.ServicePropertyDoesNotMatch -f 'BuiltInAccount', $Name, $BuiltInAccount, $serviceResource.BuiltInAccount) + return $false + } + elseif ($PSBoundParameters.ContainsKey('GroupManagedServiceAccount')) + { + $expectedStartName = ConvertTo-StartName -Username $GroupManagedServiceAccount + + if ($serviceResource.BuiltInAccount -ine $expectedStartName) + { + Write-Verbose -Message ($script:localizedData.GroupManagedServiceCredentialDoesNotMatch -f $Name, $GroupManagedServiceAccount, $serviceResource.BuiltInAccount) + return $false + } + } + elseif ($PSBoundParameters.ContainsKey('Credential')) + { + $expectedStartName = ConvertTo-StartName -Username $Credential.UserName + + if ($serviceResource.BuiltInAccount -ine $expectedStartName) + { + Write-Verbose -Message ($script:localizedData.ServiceCredentialDoesNotMatch -f $Name, $Credential.UserName, $serviceResource.BuiltInAccount) + return $false + } + } + + # Check the service startup type + if ($PSBoundParameters.ContainsKey('StartupType') -and $serviceResource.StartupType -ine $StartupType) + { + Write-Verbose -Message ($script:localizedData.ServicePropertyDoesNotMatch -f 'StartupType', $Name, $StartupType, $serviceResource.StartupType) + return $false + } + + # Check the service state + if ($State -ne 'Ignore' -and $serviceResource.State -ine $State) + { + Write-Verbose -Message ($script:localizedData.ServicePropertyDoesNotMatch -f 'State', $Name, $State, $serviceResource.State) + return $false + } + } + + return $true +} + +<# + .SYNOPSIS + Retrieves the CIM instance of the service with the given name. + + .PARAMETER ServiceName + The name of the service to get the CIM instance of. +#> +function Get-ServiceCimInstance +{ + [OutputType([Microsoft.Management.Infrastructure.CimInstance])] + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullorEmpty()] + $ServiceName + ) + + return Get-CimInstance -ClassName 'Win32_Service' -Filter "Name='$ServiceName'" +} + +<# + .SYNOPSIS + Converts the StartMode value returned in a CIM instance of a service to the format + expected by this resource. + + .PARAMETER StartMode + The StartMode value to convert. +#> +function ConvertTo-StartupTypeString +{ + [OutputType([System.String])] + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [ValidateSet('Auto', 'Manual', 'Disabled')] + [System.String] + $StartMode + ) + + if ($StartMode -eq 'Auto') + { + return 'Automatic' + } + + return $StartMode +} + +<# + .SYNOPSIS + Throws an invalid argument error if the given service startup type conflicts with the given + service state. + + .PARAMETER ServiceName + The name of the service for the error message. + + .PARAMETER StartupType + The service startup type to check. + + .PARAMETER State + The service state to check. +#> +function Assert-NoStartupTypeStateConflict +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $ServiceName, + + [Parameter(Mandatory = $true)] + [ValidateSet('Automatic', 'Manual', 'Disabled')] + [System.String] + $StartupType, + + [Parameter(Mandatory = $true)] + [ValidateSet('Running', 'Stopped', 'Ignore')] + [System.String] + $State + ) + + if ($State -eq 'Stopped') + { + if ($StartupType -eq 'Automatic') + { + # Cannot stop a service and set it to start automatically at the same time + $errorMessage = $script:localizedData.StartupTypeStateConflict -f $ServiceName, $StartupType, $State + New-InvalidArgumentException -ArgumentName 'StartupType and State' -Message $errorMessage + } + } + elseif ($State -eq 'Running') + { + if ($StartupType -eq 'Disabled') + { + # Cannot start a service and disable it at the same time + $errorMessage = $script:localizedData.StartupTypeStateConflict -f $ServiceName, $StartupType, $State + New-InvalidArgumentException -ArgumentName 'StartupType and State' -Message $errorMessage + } + } +} + +<# + .SYNOPSIS + Tests if the two given paths match. + + .PARAMETER ExpectedPath + The expected path to test against. + + .PARAMETER ActualPath + The actual path to test. +#> +function Test-PathsMatch +{ + [OutputType([System.Boolean])] + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $ExpectedPath, + + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $ActualPath + ) + + return (0 -eq [System.String]::Compare($ExpectedPath, $ActualPath, [System.Globalization.CultureInfo]::CurrentUICulture)) +} + +<# + .SYNOPSIS + Converts the given username to the string version of it that would be expected in a + service's StartName property. + + .PARAMETER Username + The username to convert. +#> +function ConvertTo-StartName +{ + [OutputType([System.String])] + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $Username + ) + + $startName = $Username + + if ($Username -ieq 'NetworkService' -or $Username -ieq 'LocalService') + { + $startName = "NT Authority\$Username" + } + elseif (-not $Username.Contains('\') -and -not $Username.Contains('@')) + { + $startName = ".\$Username" + } + elseif ($Username.StartsWith("$env:computerName\")) + { + $startName = $Username.Replace($env:computerName, '.') + } + + return $startName +} + +<# + .SYNOPSIS + Sets the executable path of the service with the given name. + Returns a boolean specifying whether a restart is needed or not. + + .PARAMETER ServiceName + The name of the service to set the path of. + + .PARAMETER Path + The path to set for the service. + + .NOTES + SupportsShouldProcess is enabled because Invoke-CimMethod calls ShouldProcess. + This function calls Invoke-CimMethod directly. +#> +function Set-ServicePath +{ + [OutputType([System.Boolean])] + [CmdletBinding(SupportsShouldProcess = $true)] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $ServiceName, + + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $Path + ) + + $serviceCimInstance = Get-ServiceCimInstance -ServiceName $ServiceName + + $pathsMatch = Test-PathsMatch -ExpectedPath $Path -ActualPath $serviceCimInstance.PathName + + if ($pathsMatch) + { + Write-Verbose -Message ($script:localizedData.ServicePathMatches -f $ServiceName) + return $false + } + else + { + Write-Verbose -Message ($script:localizedData.ServicePathDoesNotMatch -f $ServiceName) + + $changeServiceArguments = @{ + PathName = $Path + } + + $changeServiceResult = Invoke-CimMethod ` + -InputObject $serviceCimInstance ` + -MethodName 'Change' ` + -Arguments $changeServiceArguments + + if ($changeServiceResult.ReturnValue -ne 0) + { + $serviceChangePropertyString = $changeServiceArguments.Keys -join ', ' + $errorMessage = $script:localizedData.InvokeCimMethodFailed -f 'Change', $ServiceName, $serviceChangePropertyString, $changeServiceResult.ReturnValue + New-InvalidArgumentException -ArgumentName 'Path' -Message $errorMessage + } + + return $true + } +} + +<# + .SYNOPSIS + Sets the dependencies of the service with the given name. + + .PARAMETER ServiceName + The name of the service to set the dependencies of. + + .PARAMETER Dependencies + The names of the dependencies to set for the service. + + .NOTES + SupportsShouldProcess is enabled because Invoke-CimMethod calls ShouldProcess. + This function calls Invoke-CimMethod directly. +#> +function Set-ServiceDependency +{ + [CmdletBinding(SupportsShouldProcess = $true)] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $ServiceName, + + [Parameter(Mandatory = $true)] + [System.String[]] + [AllowEmptyCollection()] + $Dependencies + ) + + $service = Get-Service -Name $ServiceName -ErrorAction 'SilentlyContinue' + + $serviceDependenciesMatch = $true + + $noActualServiceDependencies = $null -eq $service.ServicesDependedOn -or 0 -eq $service.ServicesDependedOn.Count + $noExpectedServiceDependencies = $null -eq $Dependencies -or 0 -eq $Dependencies.Count + + if ($noActualServiceDependencies -xor $noExpectedServiceDependencies) + { + $serviceDependenciesMatch = $false + } + elseif (-not $noActualServiceDependencies -and -not $noExpectedServiceDependencies) + { + $mismatchedDependencies = Compare-Object -ReferenceObject $service.ServicesDependedOn.Name -DifferenceObject $Dependencies + $serviceDependenciesMatch = $null -eq $mismatchedDependencies + } + + if ($serviceDependenciesMatch) + { + Write-Verbose -Message ($script:localizedData.ServiceDepdenciesMatch -f $ServiceName) + } + else + { + Write-Verbose -Message ($script:localizedData.ServiceDepdenciesDoNotMatch -f $ServiceName) + + $serviceCimInstance = Get-ServiceCimInstance -ServiceName $ServiceName + + $changeServiceArguments = @{ + ServiceDependencies = $Dependencies + } + + $changeServiceResult = Invoke-CimMethod ` + -InputObject $serviceCimInstance ` + -MethodName 'Change' ` + -Arguments $changeServiceArguments + + if ($changeServiceResult.ReturnValue -ne 0) + { + $serviceChangePropertyString = $changeServiceArguments.Keys -join ', ' + $errorMessage = $script:localizedData.InvokeCimMethodFailed -f 'Change', $ServiceName, $serviceChangePropertyString, $changeServiceResult.ReturnValue + New-InvalidArgumentException -Message $errorMessage -ArgumentName 'Dependencies' + } + } +} + +<# + .SYNOPSIS + Grants the 'Log on as a service' right to the user with the given username. + + .PARAMETER Username + The username of the user to grant 'Log on as a service' right to +#> +function Grant-LogOnAsServiceRight +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $Username + ) + + $logOnAsServiceText = @" + namespace LogOnAsServiceHelper + { + using Microsoft.Win32.SafeHandles; + using System; + using System.Runtime.ConstrainedExecution; + using System.Runtime.InteropServices; + using System.Security; + + public class NativeMethods + { + #region constants + // from ntlsa.h + private const int POLICY_LOOKUP_NAMES = 0x00000800; + private const int POLICY_CREATE_ACCOUNT = 0x00000010; + private const uint ACCOUNT_ADJUST_SYSTEM_ACCESS = 0x00000008; + private const uint ACCOUNT_VIEW = 0x00000001; + private const uint SECURITY_ACCESS_SERVICE_LOGON = 0x00000010; + + // from LsaUtils.h + private const uint STATUS_OBJECT_NAME_NOT_FOUND = 0xC0000034; + + // from lmcons.h + private const int UNLEN = 256; + private const int DNLEN = 15; + + // Extra characteres for '\', '@' etc. + private const int EXTRA_LENGTH = 3; + #endregion constants + + #region interop structures + /// <summary> + /// Used to open a policy, but not containing anything meaqningful + /// </summary> + [StructLayout(LayoutKind.Sequential)] + private struct LSA_OBJECT_ATTRIBUTES + { + public UInt32 Length; + public IntPtr RootDirectory; + public IntPtr ObjectName; + public UInt32 Attributes; + public IntPtr SecurityDescriptor; + public IntPtr SecurityQualityOfService; + + public void Initialize() + { + this.Length = 0; + this.RootDirectory = IntPtr.Zero; + this.ObjectName = IntPtr.Zero; + this.Attributes = 0; + this.SecurityDescriptor = IntPtr.Zero; + this.SecurityQualityOfService = IntPtr.Zero; + } + } + + /// <summary> + /// LSA string + /// </summary> + [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] + private struct LSA_UNICODE_STRING + { + internal ushort Length; + internal ushort MaximumLength; + [MarshalAs(UnmanagedType.LPWStr)] + internal string Buffer; + + internal void Set(string src) + { + this.Buffer = src; + this.Length = (ushort)(src.Length * sizeof(char)); + this.MaximumLength = (ushort)(this.Length + sizeof(char)); + } + } + + /// <summary> + /// Structure used as the last parameter for LSALookupNames + /// </summary> + [StructLayout(LayoutKind.Sequential)] + private struct LSA_TRANSLATED_SID2 + { + public uint Use; + public IntPtr SID; + public int DomainIndex; + public uint Flags; + }; + #endregion interop structures + + #region safe handles + /// <summary> + /// Handle for LSA objects including Policy and Account + /// </summary> + private class LsaSafeHandle : SafeHandleZeroOrMinusOneIsInvalid + { + [DllImport("advapi32.dll")] + private static extern uint LsaClose(IntPtr ObjectHandle); + + /// <summary> + /// Prevents a default instance of the LsaPolicySafeHAndle class from being created. + /// </summary> + private LsaSafeHandle(): base(true) + { + } + + /// <summary> + /// Calls NativeMethods.CloseHandle(handle) + /// </summary> + /// <returns>the return of NativeMethods.CloseHandle(handle)</returns> + [ReliabilityContract(Consistency.WillNotCorruptState, Cer.MayFail)] + protected override bool ReleaseHandle() + { + long returnValue = LsaSafeHandle.LsaClose(this.handle); + return returnValue != 0; + + } + } + + /// <summary> + /// Handle for IntPtrs returned from Lsa calls that have to be freed with + /// LsaFreeMemory + /// </summary> + private class SafeLsaMemoryHandle : SafeHandleZeroOrMinusOneIsInvalid + { + [DllImport("advapi32")] + internal static extern int LsaFreeMemory(IntPtr Buffer); + + private SafeLsaMemoryHandle() : base(true) { } + + private SafeLsaMemoryHandle(IntPtr handle) + : base(true) + { + SetHandle(handle); + } + + private static SafeLsaMemoryHandle InvalidHandle + { + get { return new SafeLsaMemoryHandle(IntPtr.Zero); } + } + + override protected bool ReleaseHandle() + { + return SafeLsaMemoryHandle.LsaFreeMemory(handle) == 0; + } + + internal IntPtr Memory + { + get + { + return this.handle; + } + } + } + #endregion safe handles + + #region interop function declarations + /// <summary> + /// Opens LSA Policy + /// </summary> + [DllImport("advapi32.dll", SetLastError = true, PreserveSig = true)] + private static extern uint LsaOpenPolicy( + IntPtr SystemName, + ref LSA_OBJECT_ATTRIBUTES ObjectAttributes, + uint DesiredAccess, + out LsaSafeHandle PolicyHandle + ); + + /// <summary> + /// Convert the name into a SID which is used in remaining calls + /// </summary> + [DllImport("advapi32", CharSet = CharSet.Unicode, SetLastError = true), SuppressUnmanagedCodeSecurityAttribute] + private static extern uint LsaLookupNames2( + LsaSafeHandle PolicyHandle, + uint Flags, + uint Count, + LSA_UNICODE_STRING[] Names, + out SafeLsaMemoryHandle ReferencedDomains, + out SafeLsaMemoryHandle Sids + ); + + /// <summary> + /// Opens the LSA account corresponding to the user's SID + /// </summary> + [DllImport("advapi32.dll", SetLastError = true, PreserveSig = true)] + private static extern uint LsaOpenAccount( + LsaSafeHandle PolicyHandle, + IntPtr Sid, + uint Access, + out LsaSafeHandle AccountHandle); + + /// <summary> + /// Creates an LSA account corresponding to the user's SID + /// </summary> + [DllImport("advapi32.dll", SetLastError = true, PreserveSig = true)] + private static extern uint LsaCreateAccount( + LsaSafeHandle PolicyHandle, + IntPtr Sid, + uint Access, + out LsaSafeHandle AccountHandle); + + /// <summary> + /// Gets the LSA Account access + /// </summary> + [DllImport("advapi32.dll", SetLastError = true, PreserveSig = true)] + private static extern uint LsaGetSystemAccessAccount( + LsaSafeHandle AccountHandle, + out uint SystemAccess); + + /// <summary> + /// Sets the LSA Account access + /// </summary> + [DllImport("advapi32.dll", SetLastError = true, PreserveSig = true)] + private static extern uint LsaSetSystemAccessAccount( + LsaSafeHandle AccountHandle, + uint SystemAccess); + #endregion interop function declarations + + /// <summary> + /// Sets the Log On As A Service Policy for <paramref name="userName"/>, if not already set. + /// </summary> + /// <param name="userName">the user name we want to allow logging on as a service</param> + /// <exception cref="ArgumentNullException">If the <paramref name="userName"/> is null or empty.</exception> + /// <exception cref="InvalidOperationException">In the following cases: + /// Failure opening the LSA Policy. + /// The <paramref name="userName"/> is too large. + /// Failure looking up the user name. + /// Failure opening LSA account (other than account not found). + /// Failure creating LSA account. + /// Failure getting LSA account policy access. + /// Failure setting LSA account policy access. + /// </exception> + public static void SetLogOnAsServicePolicy(string userName) + { + if (String.IsNullOrEmpty(userName)) + { + throw new ArgumentNullException("userName"); + } + + LSA_OBJECT_ATTRIBUTES objectAttributes = new LSA_OBJECT_ATTRIBUTES(); + objectAttributes.Initialize(); + + // All handles are delcared in advance so they can be closed on finally + LsaSafeHandle policyHandle = null; + SafeLsaMemoryHandle referencedDomains = null; + SafeLsaMemoryHandle sids = null; + LsaSafeHandle accountHandle = null; + + try + { + uint status = LsaOpenPolicy( + IntPtr.Zero, + ref objectAttributes, + POLICY_LOOKUP_NAMES | POLICY_CREATE_ACCOUNT, + out policyHandle); + + if (status != 0) + { + throw new InvalidOperationException("CannotOpenPolicyErrorMessage"); + } + + // Unicode strings have a maximum length of 32KB. We don't want to create + // LSA strings with more than that. User lengths are much smaller so this check + // ensures userName's length is useful + if (userName.Length > UNLEN + DNLEN + EXTRA_LENGTH) + { + throw new InvalidOperationException("UserNameTooLongErrorMessage"); + } + + LSA_UNICODE_STRING lsaUserName = new LSA_UNICODE_STRING(); + lsaUserName.Set(userName); + + LSA_UNICODE_STRING[] names = new LSA_UNICODE_STRING[1]; + names[0].Set(userName); + + status = LsaLookupNames2( + policyHandle, + 0, + 1, + new LSA_UNICODE_STRING[] { lsaUserName }, + out referencedDomains, + out sids); + + if (status != 0) + { + throw new InvalidOperationException("CannotLookupNamesErrorMessage"); + } + + LSA_TRANSLATED_SID2 sid = (LSA_TRANSLATED_SID2)Marshal.PtrToStructure(sids.Memory, typeof(LSA_TRANSLATED_SID2)); + + status = LsaOpenAccount(policyHandle, + sid.SID, + ACCOUNT_VIEW | ACCOUNT_ADJUST_SYSTEM_ACCESS, + out accountHandle); + + uint currentAccess = 0; + + if (status == 0) + { + status = LsaGetSystemAccessAccount(accountHandle, out currentAccess); + + if (status != 0) + { + throw new InvalidOperationException("CannotGetAccountAccessErrorMessage"); + } + + } + else if (status == STATUS_OBJECT_NAME_NOT_FOUND) + { + status = LsaCreateAccount( + policyHandle, + sid.SID, + ACCOUNT_ADJUST_SYSTEM_ACCESS, + out accountHandle); + + if (status != 0) + { + throw new InvalidOperationException("CannotCreateAccountAccessErrorMessage"); + } + } + else + { + throw new InvalidOperationException("CannotOpenAccountErrorMessage"); + } + + if ((currentAccess & SECURITY_ACCESS_SERVICE_LOGON) == 0) + { + status = LsaSetSystemAccessAccount( + accountHandle, + currentAccess | SECURITY_ACCESS_SERVICE_LOGON); + if (status != 0) + { + throw new InvalidOperationException("CannotSetAccountAccessErrorMessage"); + } + } + } + finally + { + if (policyHandle != null) { policyHandle.Close(); } + if (referencedDomains != null) { referencedDomains.Close(); } + if (sids != null) { sids.Close(); } + if (accountHandle != null) { accountHandle.Close(); } + } + } + } + } +"@ + + try + { + $null = [LogOnAsServiceHelper.NativeMethods] + } + catch + { + $logOnAsServiceText = $logOnAsServiceText.Replace('CannotOpenPolicyErrorMessage', ` + $script:localizedData.CannotOpenPolicyErrorMessage) + $logOnAsServiceText = $logOnAsServiceText.Replace('UserNameTooLongErrorMessage', ` + $script:localizedData.UserNameTooLongErrorMessage) + $logOnAsServiceText = $logOnAsServiceText.Replace('CannotLookupNamesErrorMessage', ` + $script:localizedData.CannotLookupNamesErrorMessage) + $logOnAsServiceText = $logOnAsServiceText.Replace('CannotOpenAccountErrorMessage', ` + $script:localizedData.CannotOpenAccountErrorMessage) + $logOnAsServiceText = $logOnAsServiceText.Replace('CannotCreateAccountAccessErrorMessage', ` + $script:localizedData.CannotCreateAccountAccessErrorMessage) + $logOnAsServiceText = $logOnAsServiceText.Replace('CannotGetAccountAccessErrorMessage', ` + $script:localizedData.CannotGetAccountAccessErrorMessage) + $logOnAsServiceText = $logOnAsServiceText.Replace('CannotSetAccountAccessErrorMessage', ` + $script:localizedData.CannotSetAccountAccessErrorMessage) + $null = Add-Type $logOnAsServiceText -PassThru + } + + if ($Username.StartsWith('.\')) + { + $Username = $Username.Substring(2) + } + + try + { + [LogOnAsServiceHelper.NativeMethods]::SetLogOnAsServicePolicy($Username) + } + catch + { + $errorMessage = $script:localizedData.ErrorSettingLogOnAsServiceRightsForUser -f $Username, $_.Exception.Message + New-InvalidOperationException -Message $errorMessage + } +} + +<# + .SYNOPSIS + Sets the service properties involving the account the service is running under. + (StartName, StartPassword, DesktopInteract) + + .PARAMETER ServiceName + The name of the service to change the start name of. + + .PARAMETER BuiltInAccount + The name of the built-in account to run the service under. + This value will overwrite the Credential value if Credential is also declared. + + .PARAMETER Credential + The user credential to run the service under. + BuiltInAccount will overwrite this value if BuiltInAccount is also declared. + + .PARAMETER DesktopInteract + Indicates whether or not the service should be able to communicate with a window on the + desktop. + + Must be false for services not running as LocalSystem. + + .NOTES + DesktopInteract is included here because it can only be enabled when the service startup + account name is LocalSystem. In order not to run into a conflict where one property has + been updated before the other, both are updated here at the same time. + + SupportsShouldProcess is enabled because Invoke-CimMethod calls ShouldProcess. + This function calls Invoke-CimMethod directly. +#> +function Set-ServiceAccountProperty +{ + [CmdletBinding(SupportsShouldProcess = $true)] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $ServiceName, + + [Parameter()] + [System.String] + [ValidateSet('LocalSystem', 'LocalService', 'NetworkService')] + $BuiltInAccount, + + [Parameter()] + [System.String] + $GroupManagedServiceAccount, + + [Parameter()] + [System.Management.Automation.PSCredential] + [System.Management.Automation.Credential()] + $Credential, + + [Parameter()] + [System.Boolean] + $DesktopInteract + ) + + $serviceCimInstance = Get-ServiceCimInstance -ServiceName $ServiceName + + $changeServiceArguments = @{} + + if ($PSBoundParameters.ContainsKey('BuiltInAccount')) + { + $startName = ConvertTo-StartName -Username $BuiltInAccount + + if ($serviceCimInstance.StartName -ine $startName) + { + $changeServiceArguments['StartName'] = $startName + $changeServiceArguments['StartPassword'] = '' + } + } + elseif ($PSBoundParameters.ContainsKey('GroupManagedServiceAccount')) + { + $startName = ConvertTo-StartName -Username $GroupManagedServiceAccount + + if ($serviceCimInstance.StartName -ine $startName) + { + Grant-LogOnAsServiceRight -Username $startName + + $changeServiceArguments['StartName'] = $startName + } + } + elseif ($PSBoundParameters.ContainsKey('Credential')) + { + $startName = ConvertTo-StartName -Username $Credential.UserName + + if ($serviceCimInstance.StartName -ine $startName) + { + Grant-LogOnAsServiceRight -Username $startName + + $changeServiceArguments['StartName'] = $startName + $changeServiceArguments['StartPassword'] = $Credential.GetNetworkCredential().Password + } + } + + if ($PSBoundParameters.ContainsKey('DesktopInteract')) + { + if ($serviceCimInstance.DesktopInteract -ne $DesktopInteract) + { + $changeServiceArguments['DesktopInteract'] = $DesktopInteract + } + } + + if ($changeServiceArguments.Count -gt 0) + { + $changeServiceResult = Invoke-CimMethod -InputObject $ServiceCimInstance -MethodName 'Change' -Arguments $changeServiceArguments + + if ($changeServiceResult.ReturnValue -ne 0) + { + $serviceChangePropertyString = $changeServiceArguments.Keys -join ', ' + $errorMessage = $script:localizedData.InvokeCimMethodFailed -f 'Change', $ServiceName, $serviceChangePropertyString, $changeServiceResult.ReturnValue + New-InvalidArgumentException -ArgumentName 'BuiltInAccount, Credential, or DesktopInteract' -Message $errorMessage + } + } +} + +<# + .SYNOPSIS + Sets the startup type of the service with the given name. + + .PARAMETER ServiceName + The name of the service to set the startup type of. + + .PARAMETER StartupType + The startup type value to set for the service. + + .NOTES + SupportsShouldProcess is enabled because Invoke-CimMethod calls ShouldProcess. + This function calls Invoke-CimMethod directly. +#> +function Set-ServiceStartupType +{ + [CmdletBinding(SupportsShouldProcess = $true)] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $ServiceName, + + [Parameter(Mandatory = $true)] + [ValidateSet('Automatic', 'Manual', 'Disabled')] + [System.String] + $StartupType + ) + + $serviceCimInstance = Get-ServiceCimInstance -ServiceName $ServiceName + $serviceStartupType = ConvertTo-StartupTypeString -StartMode $serviceCimInstance.StartMode + + if ($serviceStartupType -ieq $StartupType) + { + Write-Verbose -Message ($script:localizedData.ServiceStartupTypeMatches -f $ServiceName) + } + else + { + Write-Verbose -Message ($script:localizedData.ServiceStartupTypeDoesNotMatch -f $ServiceName) + + $changeServiceArguments = @{ + StartMode = $StartupType + } + + $changeResult = Invoke-CimMethod ` + -InputObject $serviceCimInstance ` + -MethodName 'Change' ` + -Arguments $changeServiceArguments + + if ($changeResult.ReturnValue -ne 0) + { + $serviceChangePropertyString = $changeServiceArguments.Keys -join ', ' + $errorMessage = $script:localizedData.InvokeCimMethodFailed -f 'Change', $ServiceName, $serviceChangePropertyString, $changeResult.ReturnValue + New-InvalidArgumentException -ArgumentName 'StartupType' -Message $errorMessage + } + } +} + +<# + .SYNOPSIS + Sets the service with the given name to have the specified properties. + + .PARAMETER Name + The name of the service to set the properties of. + + .PARAMETER DisplayName + The display name the service should have. + + .PARAMETER Description + The description the service should have. + + .PARAMETER Dependencies + The names of the dependencies the service should have. + + .PARAMETER BuiltInAccount + The built-in account the service should start under. + + Cannot be specified at the same time as Credential or GroupManagedServiceAccount. + + .PARAMETER GroupManagedServiceAccount + The Group Managed Service Account that is used to run the service. + + Cannot be specified at the same time as BuiltInAccount or Credential. + + .PARAMETER Credential + The credential of the user account the service should start under. + + Cannot be specified at the same time as BuiltInAccount or GroupManagedServiceAccount. + The user specified by this credential will automatically be granted the Log on as a Service + right. + + .PARAMETER DesktopInteract + Indicates whether or not the service should be able to communicate with a window on the desktop. + + .PARAMETER StartupType + The startup type the service should have. + + .NOTES + SupportsShouldProcess is enabled because Invoke-CimMethod calls ShouldProcess. + Here are the paths through which Set-ServiceProperty calls Invoke-CimMethod: + + Set-ServiceProperty --> Set-ServiceDependency --> Invoke-CimMethod + --> Set-ServieceAccountProperty --> Invoke-CimMethod + --> Set-ServiceStartupType --> Invoke-CimMethod +#> +function Set-ServiceProperty +{ + [CmdletBinding(SupportsShouldProcess = $true)] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $ServiceName, + + [Parameter()] + [ValidateSet('Automatic', 'Manual', 'Disabled')] + [System.String] + $StartupType, + + [Parameter()] + [ValidateSet('LocalSystem', 'LocalService', 'NetworkService')] + [System.String] + $BuiltInAccount, + + [Parameter()] + [System.String] + $GroupManagedServiceAccount, + + [Parameter()] + [System.Boolean] + $DesktopInteract, + + [Parameter()] + [ValidateNotNullOrEmpty()] + [System.String] + $DisplayName, + + [Parameter()] + [ValidateNotNullOrEmpty()] + [System.String] + $Description, + + [Parameter()] + [System.String[]] + [AllowEmptyCollection()] + $Dependencies, + + [Parameter()] + [ValidateNotNull()] + [System.Management.Automation.PSCredential] + [System.Management.Automation.Credential()] + $Credential + ) + + # Update display name and/or description if needed + $serviceCimInstance = Get-ServiceCimInstance -ServiceName $ServiceName + + $setServiceParameters = @{} + + if ($PSBoundParameters.ContainsKey('DisplayName') -and $serviceCimInstance.DisplayName -ine $DisplayName) + { + $setServiceParameters['DisplayName'] = $DisplayName + } + + if ($PSBoundParameters.ContainsKey('Description') -and $serviceCimInstance.Description -ine $Description) + { + $setServiceParameters['Description'] = $Description + } + + if ($setServiceParameters.Count -gt 0) + { + $null = Set-Service -Name $ServiceName @setServiceParameters + } + + # Update service dependencies if needed + if ($PSBoundParameters.ContainsKey('Dependencies')) + { + Set-ServiceDependency -ServiceName $ServiceName -Dependencies $Dependencies + } + + # Update service account properties if needed + $setServiceAccountPropertyParameters = @{} + + if ($PSBoundParameters.ContainsKey('BuiltInAccount')) + { + $setServiceAccountPropertyParameters['BuiltInAccount'] = $BuiltInAccount + } + elseif ($PSBoundParameters.ContainsKey('GroupManagedServiceAccount')) + { + $setServiceAccountPropertyParameters['GroupManagedServiceAccount'] = $GroupManagedServiceAccount + } + elseif ($PSBoundParameters.ContainsKey('Credential')) + { + $setServiceAccountPropertyParameters['Credential'] = $Credential + } + + if ($PSBoundParameters.ContainsKey('DesktopInteract')) + { + $setServiceAccountPropertyParameters['DesktopInteract'] = $DesktopInteract + } + + if ($setServiceAccountPropertyParameters.Count -gt 0) + { + Set-ServiceAccountProperty -ServiceName $ServiceName @setServiceAccountPropertyParameters + } + + # Update startup type + if ($PSBoundParameters.ContainsKey('StartupType')) + { + Set-ServiceStartupType -ServiceName $ServiceName -StartupType $StartupType + } +} + +<# + .SYNOPSIS + Deletes the service with the given name. + + This is a wrapper function for unit testing. + + .PARAMETER Name + The name of the service to delete. +#> +function Remove-Service +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $Name + ) + + & 'sc.exe' 'delete' $Name +} + +<# + .SYNOPSIS + Deletes the service with the given name and waits for the service to be deleted. + + .PARAMETER Name + The name of the service to delete. + + .PARAMETER TerminateTimeout + The time to wait for the service to be deleted in milliseconds. +#> +function Remove-ServiceWithTimeout +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $Name, + + [Parameter(Mandatory = $true)] + [System.UInt32] + $TerminateTimeout + ) + + Remove-Service -Name $Name + + $serviceDeleted = $false + $start = [System.DateTime]::Now + + while (-not $serviceDeleted -and ([System.DateTime]::Now - $start).TotalMilliseconds -lt $TerminateTimeout) + { + $service = Get-Service -Name $Name -ErrorAction 'SilentlyContinue' + + if ($null -eq $service) + { + $serviceDeleted = $true + } + else + { + Write-Verbose -Message ($script:localizedData.WaitingForServiceDeletion -f $Name) + Start-Sleep -Seconds 1 + } + } + + if ($serviceDeleted) + { + Write-Verbose -Message ($script:localizedData.ServiceDeletionSucceeded -f $Name) + } + else + { + New-InvalidOperationException -Message ($script:localizedData.ServiceDeletionFailed -f $Name) + } +} + +<# + .SYNOPSIS + Waits for the service with the given name to reach the given state within the given time + span. + + This is a wrapper function for unit testing. + + .PARAMETER ServiceName + The name of the service that should be in the given state. + + .PARAMETER State + The state the service should be in. + + .PARAMETER WaitTimeSpan + A time span of how long to wait for the service to reach the desired state. +#> +function Wait-ServiceStateWithTimeout +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $ServiceName, + + [Parameter(Mandatory = $true)] + [System.ServiceProcess.ServiceControllerStatus] + $State, + + [Parameter(Mandatory = $true)] + [System.TimeSpan] + $WaitTimeSpan + ) + + $service = Get-Service -Name $ServiceName -ErrorAction 'SilentlyContinue' + $Service.WaitForStatus($State, $WaitTimeSpan) +} + +<# + .SYNOPSIS + Starts the service with the given name, if it is not already running, and waits for the + service to be running. + + .PARAMETER ServiceName + The name of the service to start. + + .PARAMETER StartupTimeout + The time to wait for the service to be running in milliseconds. +#> +function Start-ServiceWithTimeout +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $ServiceName, + + [Parameter(Mandatory = $true)] + [System.UInt32] + $StartupTimeout + ) + + Start-Service -Name $ServiceName + $waitTimeSpan = New-Object -TypeName 'TimeSpan' -ArgumentList (0, 0, 0, 0, $StartupTimeout) + Wait-ServiceStateWithTimeout -ServiceName $ServiceName -State 'Running' -WaitTimeSpan $waitTimeSpan +} + +<# + .SYNOPSIS + Stops the service with the given name, if it is not already stopped, and waits for the + service to be stopped. + + .PARAMETER ServiceName + The name of the service to stop. + + .PARAMETER TerminateTimeout + The time to wait for the service to be stopped in milliseconds. +#> +function Stop-ServiceWithTimeout +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $ServiceName, + + [Parameter(Mandatory = $true)] + [System.UInt32] + $TerminateTimeout + ) + + Stop-Service -Name $ServiceName + $waitTimeSpan = New-Object -TypeName 'TimeSpan' -ArgumentList (0, 0, 0, 0, $TerminateTimeout) + Wait-ServiceStateWithTimeout -ServiceName $ServiceName -State 'Stopped' -WaitTimeSpan $waitTimeSpan +} diff --git a/deployment/dsc/azshcihost/xPSDesiredStateConfiguration/9.1.0/DSCResources/DSC_xServiceResource/DSC_xServiceResource.schema.mof b/deployment/dsc/azshcihost/xPSDesiredStateConfiguration/9.1.0/DSCResources/DSC_xServiceResource/DSC_xServiceResource.schema.mof new file mode 100644 index 0000000..2876dbe --- /dev/null +++ b/deployment/dsc/azshcihost/xPSDesiredStateConfiguration/9.1.0/DSCResources/DSC_xServiceResource/DSC_xServiceResource.schema.mof @@ -0,0 +1,19 @@ + +[ClassVersion("1.0.0"),FriendlyName("xService")] +class DSC_xServiceResource : OMI_BaseResource +{ + [Key,Description("Indicates the service name. Note that sometimes this is different from the display name. You can get a list of the services and their current state with the Get-Service cmdlet.")] String Name; + [Write,Description("Ensures that the service is present or absent. Defaults to Present."),ValueMap{"Present", "Absent"},Values{"Present", "Absent"}] String Ensure; + [Write,Description("The path to the service executable file.")] String Path; + [Write,Description("Indicates the startup type for the service."),ValueMap{"Automatic", "Manual", "Disabled"},Values{"Automatic", "Manual", "Disabled"}] String StartupType; + [Write,Description("Indicates the sign-in account to use for the service."),ValueMap{"LocalSystem", "LocalService", "NetworkService"},Values{"LocalSystem", "LocalService", "NetworkService"}] String BuiltInAccount; + [Write,Description("The Group Managed Service Account to run the service under.")] String GroupManagedServiceAccount; + [Write,Description("The credential to run the service under."),EmbeddedInstance("MSFT_Credential")] String Credential; + [Write,Description("The service can create or communicate with a window on the desktop. Must be false for services not running as LocalSystem. Defaults to False.")] Boolean DesktopInteract; + [Write,Description("Indicates the state you want to ensure for the service. Defaults to Running."),ValueMap{"Running", "Stopped", "Ignore"},Values{"Running", "Stopped", "Ignore"}] String State; + [Write,Description("The display name of the service.")] String DisplayName; + [Write,Description("The description of the service.")] String Description; + [Write,Description("An array of strings indicating the names of the dependencies of the service.")] String Dependencies[]; + [Write,Description("The time to wait for the service to start in milliseconds. Defaults to 30000.")] uint32 StartupTimeout; + [Write,Description("The time to wait for the service to stop in milliseconds. Defaults to 30000.")] uint32 TerminateTimeout; +}; diff --git a/deployment/dsc/azshcihost/xPSDesiredStateConfiguration/9.1.0/DSCResources/DSC_xServiceResource/en-US/DSC_xServiceResource.schema.mfl b/deployment/dsc/azshcihost/xPSDesiredStateConfiguration/9.1.0/DSCResources/DSC_xServiceResource/en-US/DSC_xServiceResource.schema.mfl new file mode 100644 index 0000000..69532e1 --- /dev/null +++ b/deployment/dsc/azshcihost/xPSDesiredStateConfiguration/9.1.0/DSCResources/DSC_xServiceResource/en-US/DSC_xServiceResource.schema.mfl @@ -0,0 +1,16 @@ +[Description("This resource is used to manage services.") : Amended,AMENDMENT, LOCALE("MS_409")] +class DSC_xServiceResource : OMI_BaseResource +{ + [key,Description("The name for the service") : Amended] string Name; + [Description("An enumerated value that describes if the service is expected to be running on the machine.\nRunning {default} \nStopped \n") : Amended] string State; + [Description("An enumerated value that describes the service start type.\nAutomatic \nManual \nDisabled \n") : Amended] string StartupType; + [Description("An enumerated value that describes the built in account the service runs under.\nLocalSystem \nLocalService \nNetworkService \n") : Amended] string BuiltInAccount; + [Description("The optional GroupManagedServiceAccount the service runs under, GroupManagedServiceAccount, BuiltInAccount and Credential are mutually exclusive") : Amended] string GroupManagedServiceAccount; + [Description("The optional credentials the service runs under") : Amended] string Credential; + [Description("The service status") : Amended] string Status; + [Description("The service display name") : Amended] string DisplayName; + [Description("The service description") : Amended] string Description; + [Description("The path to the service executable file") : Amended] string Path; + [Description("A list of service dependencies") : Amended] string Dependencies[]; + [Description("An enumerated value that checks whether a service already exists and creates it if it does not exist on the machine.\nPresent {default} \nAbsent \n") : Amended] string Ensure; +}; diff --git a/deployment/dsc/azshcihost/xPSDesiredStateConfiguration/9.1.0/DSCResources/DSC_xServiceResource/en-US/DSC_xServiceResource.strings.psd1 b/deployment/dsc/azshcihost/xPSDesiredStateConfiguration/9.1.0/DSCResources/DSC_xServiceResource/en-US/DSC_xServiceResource.strings.psd1 new file mode 100644 index 0000000..6bfb21e --- /dev/null +++ b/deployment/dsc/azshcihost/xPSDesiredStateConfiguration/9.1.0/DSCResources/DSC_xServiceResource/en-US/DSC_xServiceResource.strings.psd1 @@ -0,0 +1,40 @@ +<# + Localized resources for DSC_xServiceResource + Strings underneath the blank line are for Grant-LogOnAsServiceRight. +#> + +ConvertFrom-StringData @' + ServiceExists = Service {0} exists. + ServiceDoesNotExist = Service {0} does not exist. + CredentialParametersAreMutallyExclusive = BuiltInAccount, Credential and GroupManagedServiceAccount are mutually exclusive. Please specify only one of these parameters for service {0}. + ServiceAlreadyAbsent = Service {0} is already absent. No change required. + ServiceDoesNotExistPathMissingError = The service '{0}' does not exist, but Path was not specified. Please specify the path to the executable the service should run to create a new service. + CreatingService = Creating new service {0}... + EditingServiceProperties = Editing the properties of service {0}... + RemovingService = Removing the service {0}... + RestartingService = Restarting the service {0}... + ServicePathMatches = The path of service {0} matches the expected path. + ServicePathDoesNotMatch = The path of service {0} does not match the expected path. + ServiceDepdenciesMatch = The dependencies of service {0} match the expected dependencies. + ServiceDepdenciesDoNotMatch = The dependencies of service {0} do not match the expected dependencies. + ServiceStartupTypeMatches = The start mode of service {0} matches the expected start mode. + ServiceStartupTypeDoesNotMatch = The start mode of service {0} does not match the expected start mode. + ServicePropertyDoesNotMatch = The service property {0} of service {1} does not match the expected value. The expected value is {2}. The actual value is {3}. + ServiceCredentialDoesNotMatch = The start name of service {0} does not match the expected username from the given credential. The expected value is {1}. The actual value is {2}. + GroupManagedServiceCredentialDoesNotMatch = The start name of service {0} does not match the expected username from the given Group Managed Service Account. The expected value is {1}. The actual value is {2}. + ServiceDeletionSucceeded = The service {0} has been successfully deleted. + ServiceDeletionFailed = Failed to delete service {0}. + WaitingForServiceDeletion = Waiting for service {0} to be deleted. + ErrorSettingLogOnAsServiceRightsForUser = Error granting user {0} the right to log on as a service. Error message: '{1}' + StartupTypeStateConflict = Service {0} cannot have a startup type of {1} and a state of {2} at the same time. + InvokeCimMethodFailed = The CIM method {0} failed on service {1} while attempting to update the {2} property(s) with the error code {3}. + + CannotOpenPolicyErrorMessage = Cannot open policy manager. + UserNameTooLongErrorMessage = User name is too long. + CannotLookupNamesErrorMessage = Failed to lookup user name. + CannotOpenAccountErrorMessage = Failed to open policy for user. + CannotCreateAccountAccessErrorMessage = Failed to create policy for user. + CannotGetAccountAccessErrorMessage = Failed to get user policy rights. + CannotSetAccountAccessErrorMessage = Failed to set user policy rights. + CorruptDependency = Service '{0}' has a corrupt dependency. For more information, inspect the registry value at HKLM:\\SYSTEM\\CurrentControlSet\\Services\\{0}\\DependOnService. +'@ diff --git a/deployment/dsc/azshcihost/xPSDesiredStateConfiguration/9.1.0/DSCResources/DSC_xUserResource/DSC_xUserResource.psm1 b/deployment/dsc/azshcihost/xPSDesiredStateConfiguration/9.1.0/DSCResources/DSC_xUserResource/DSC_xUserResource.psm1 new file mode 100644 index 0000000..a35c829 --- /dev/null +++ b/deployment/dsc/azshcihost/xPSDesiredStateConfiguration/9.1.0/DSCResources/DSC_xUserResource/DSC_xUserResource.psm1 @@ -0,0 +1,1397 @@ +# User name and password needed for this resource and Write-Verbose Used in helper functions +[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingUserNameAndPassWordParams', '')] +[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSDSCUseVerboseMessageInDSCResource', '')] +param () + +$errorActionPreference = 'Stop' +Set-StrictMode -Version 'Latest' + +$modulePath = Join-Path -Path (Split-Path -Path (Split-Path -Path $PSScriptRoot -Parent) -Parent) -ChildPath 'Modules' + +# Import the shared modules +Import-Module -Name (Join-Path -Path $modulePath ` + -ChildPath (Join-Path -Path 'xPSDesiredStateConfiguration.Common' ` + -ChildPath 'xPSDesiredStateConfiguration.Common.psm1')) + +# Import Localization Strings +$script:localizedData = Get-LocalizedData -ResourceName 'DSC_xUserResource' + +if (-not (Test-IsNanoServer)) +{ + Add-Type -AssemblyName 'System.DirectoryServices.AccountManagement' +} + +<# + .SYNOPSIS + Retrieves the user with the given username + + .PARAMETER UserName + The name of the user to retrieve. +#> +function Get-TargetResource +{ + [OutputType([System.Collections.Hashtable])] + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $UserName + ) + + if (Test-IsNanoServer) + { + Get-TargetResourceOnNanoServer @PSBoundParameters + } + else + { + Get-TargetResourceOnFullSKU @PSBoundParameters + } +} + +<# + .SYNOPSIS + Creates, modifies, or deletes a user. + + .PARAMETER UserName + The name of the user to create, modify, or delete. + + .PARAMETER Ensure + Specifies whether the user should exist or not. + By default this is set to Present. + + .PARAMETER FullName + The (optional) full name or display name of the user. + If not provided this value will remain blank. + + .PARAMETER Description + Optional description for the user. + + .PARAMETER Password + The desired password for the user. + + .PARAMETER Disabled + Specifies whether the user should be disabled or not. + By default this is set to $false + + .PARAMETER PasswordNeverExpires + Specifies whether the password should ever expire or not. + By default this is set to $false + + .PARAMETER PasswordChangeRequired + Specifies whether the user must reset their password or not. + By default this is set to $false + + .PARAMETER PasswordChangeNotAllowed + Specifies whether the user is allowed to change their password or not. + By default this is set to $false + + .NOTES + If Ensure is set to 'Present' then the password parameter is required. +#> +function Set-TargetResource +{ + # Should process is called in a helper functions but not directly in Set-TargetResource + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSShouldProcess', '')] + [CmdletBinding(SupportsShouldProcess = $true)] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $UserName, + + [Parameter()] + [ValidateSet('Present', 'Absent')] + [System.String] + $Ensure = 'Present', + + [Parameter()] + [System.String] + $FullName, + + [Parameter()] + [System.String] + $Description, + + [Parameter()] + [ValidateNotNullOrEmpty()] + [System.Management.Automation.PSCredential] + [System.Management.Automation.Credential()] + $Password, + + [Parameter()] + [System.Boolean] + $Disabled, + + [Parameter()] + [System.Boolean] + $PasswordNeverExpires, + + [Parameter()] + [System.Boolean] + $PasswordChangeRequired, + + [Parameter()] + [System.Boolean] + $PasswordChangeNotAllowed + ) + + if (Test-IsNanoServer) + { + Set-TargetResourceOnNanoServer @PSBoundParameters + } + else + { + Set-TargetResourceOnFullSKU @PSBoundParameters + } +} + +<# + .SYNOPSIS + Tests if a user is in the desired state. + + .PARAMETER UserName + The name of the user to test the state of. + + .PARAMETER Ensure + Specifies whether the user should exist or not. + By default this is set to Present + + .PARAMETER FullName + The full name/display name that the user should have. + If not provided, this value will not be tested. + + .PARAMETER Description + The description that the user should have. + If not provided, this value will not be tested. + + .PARAMETER Password + The password the user should have. + + .PARAMETER Disabled + Specifies whether the user account should be disabled or not. + + .PARAMETER PasswordNeverExpires + Specifies whether the password should ever expire or not. + + .PARAMETER PasswordChangeRequired + Not used in Test-TargetResource as there is no easy way to test this value. + + .PARAMETER PasswordChangeNotAllowed + Specifies whether the user should be allowed to change their password or not. +#> +function Test-TargetResource +{ + [OutputType([System.Boolean])] + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $UserName, + + [Parameter()] + [ValidateSet('Present', 'Absent')] + [System.String] + $Ensure = 'Present', + + [Parameter()] + [System.String] + $FullName, + + [Parameter()] + [System.String] + $Description, + + [Parameter()] + [ValidateNotNullOrEmpty()] + [System.Management.Automation.PSCredential] + [System.Management.Automation.Credential()] + $Password, + + [Parameter()] + [System.Boolean] + $Disabled, + + [Parameter()] + [System.Boolean] + $PasswordNeverExpires, + + [Parameter()] + [System.Boolean] + $PasswordChangeRequired, + + [Parameter()] + [System.Boolean] + $PasswordChangeNotAllowed + ) + + if (Test-IsNanoServer) + { + Test-TargetResourceOnNanoServer @PSBoundParameters + } + else + { + Test-TargetResourceOnFullSKU @PSBoundParameters + } +} + + +<# + .SYNOPSIS + Retrieves the user with the given username when on a full server + + .PARAMETER UserName + The name of the user to retrieve. +#> +function Get-TargetResourceOnFullSKU +{ + [OutputType([System.Collections.Hashtable])] + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $UserName + ) + + Set-StrictMode -Version Latest + + Assert-UserNameValid -UserName $UserName + + # Try to find a user by a name + $principalContext = New-Object ` + -TypeName System.DirectoryServices.AccountManagement.PrincipalContext ` + -ArgumentList ([System.DirectoryServices.AccountManagement.ContextType]::Machine) + + try + { + Write-Verbose -Message 'Starting Get-TargetResource on FullSKU' + $user = [System.DirectoryServices.AccountManagement.UserPrincipal]::FindByIdentity($principalContext, $UserName) + + if ($null -ne $user) + { + # The user is found. Return all user properties and Ensure='Present'. + $returnValue = @{ + UserName = $user.Name + Ensure = 'Present' + FullName = $user.DisplayName + Description = $user.Description + Disabled = -not $user.Enabled + PasswordNeverExpires = $user.PasswordNeverExpires + PasswordChangeRequired = $null + PasswordChangeNotAllowed = $user.UserCannotChangePassword + } + + return $returnValue + } + + # The user is not found. Return Ensure = Absent. + return @{ + UserName = $UserName + Ensure = 'Absent' + } + } + catch + { + New-ConnectionException -ErrorId 'MultipleMatches' -ErrorMessage ($script:localizedData.MultipleMatches + $_) + } + finally + { + if ($null -ne $user) + { + $user.Dispose() + } + + $principalContext.Dispose() + } +} + +<# + .SYNOPSIS + Creates, modifies, or deletes a user when on a full server. + + .PARAMETER UserName + The name of the user to create, modify, or delete. + + .PARAMETER Ensure + Specifies whether the user should exist or not. + By default this is set to Present + + .PARAMETER FullName + The (optional) full name or display name of the user. + If not provided this value will remain blank. + + .PARAMETER Description + Optional description for the user. + + .PARAMETER Password + The desired password for the user. + + .PARAMETER Disabled + Specifies whether the user should be disabled or not. + By default this is set to $false + + .PARAMETER PasswordNeverExpires + Specifies whether the password should ever expire or not. + By default this is set to $false + + .PARAMETER PasswordChangeRequired + Specifies whether the user must reset their password or not. + By default this is set to $false + + .PARAMETER PasswordChangeNotAllowed + Specifies whether the user is allowed to change their password or not. + By default this is set to $false + + .NOTES + If Ensure is set to 'Present' then the Password parameter is required. +#> +function Set-TargetResourceOnFullSKU +{ + [CmdletBinding(SupportsShouldProcess = $true)] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $UserName, + + [Parameter()] + [ValidateSet('Present', 'Absent')] + [System.String] + $Ensure = 'Present', + + [Parameter()] + [System.String] + $FullName, + + [Parameter()] + [System.String] + $Description, + + [Parameter()] + [ValidateNotNullOrEmpty()] + [System.Management.Automation.PSCredential] + [System.Management.Automation.Credential()] + $Password, + + [Parameter()] + [System.Boolean] + $Disabled, + + [Parameter()] + [System.Boolean] + $PasswordNeverExpires, + + [Parameter()] + [System.Boolean] + $PasswordChangeRequired, + + [Parameter()] + [System.Boolean] + $PasswordChangeNotAllowed + ) + + Set-StrictMode -Version Latest + + Write-Verbose -Message ($script:localizedData.ConfigurationStarted -f $UserName) + + Assert-UserNameValid -UserName $UserName + + # Try to find a user by name. + $principalContext = New-Object ` + -TypeName System.DirectoryServices.AccountManagement.PrincipalContext ` + -ArgumentList ([System.DirectoryServices.AccountManagement.ContextType]::Machine) + + try + { + try + { + $user = [System.DirectoryServices.AccountManagement.UserPrincipal]::FindByIdentity($principalContext, $UserName) + } + catch + { + New-InvalidOperationException -Message ($script:localizedData.MultipleMatches + $_) + } + + if ($Ensure -eq 'Present') + { + $whatIfShouldProcess = $true + $userExists = $false + $saveChanges = $false + + if ($null -eq $user) + { + # A user does not exist. Check WhatIf for adding a user + $whatIfShouldProcess = $pscmdlet.ShouldProcess($script:localizedData.UserWithName -f $UserName, $script:localizedData.AddOperation) + } + else + { + # A user exists + $userExists = $true + + # Check WhatIf for setting a user + $whatIfShouldProcess = $pscmdlet.ShouldProcess($script:localizedData.UserWithName -f $UserName, $script:localizedData.SetOperation) + } + + if ($whatIfShouldProcess) + { + if (-not $userExists) + { + # The user with the provided name does not exist so add a new user + $user = New-Object ` + -TypeName System.DirectoryServices.AccountManagement.UserPrincipal ` + -ArgumentList $principalContext + $user.Name = $UserName + $saveChanges = $true + } + + # Set user properties. + if ($PSBoundParameters.ContainsKey('FullName') -and ((-not $userExists) -or ($FullName -ne $user.DisplayName))) + { + $user.DisplayName = $FullName + $saveChanges = $true + } + else + { + if (-not $userExists) + { + # For a newly created user, set the DisplayName property to an empty string since by default DisplayName is set to user's name + $user.DisplayName = [System.String]::Empty + } + } + + if ($PSBoundParameters.ContainsKey('Description') -and ((-not $userExists) -or ($Description -ne $user.Description))) + { + $user.Description = $Description + $saveChanges = $true + } + + # Set the password regardless of the state of the user + if ($PSBoundParameters.ContainsKey('Password')) + { + $user.SetPassword($Password.GetNetworkCredential().Password) + $saveChanges = $true + } + + if ($PSBoundParameters.ContainsKey('Disabled') -and ((-not $userExists) -or ($Disabled -eq $user.Enabled))) + { + $user.Enabled = -not $Disabled + $saveChanges = $true + } + + if ($PSBoundParameters.ContainsKey('PasswordNeverExpires') -and ((-not $userExists) -or ($PasswordNeverExpires -ne $user.PasswordNeverExpires))) + { + $user.PasswordNeverExpires = $PasswordNeverExpires + $saveChanges = $true + } + + if ($PSBoundParameters.ContainsKey('PasswordChangeRequired')) + { + if ($PasswordChangeRequired) + { + # Expire the password which will force the user to change the password at the next logon + $user.ExpirePasswordNow() + $saveChanges = $true + } + } + + if ($PSBoundParameters.ContainsKey('PasswordChangeNotAllowed') -and ((-not $userExists) -or ($PasswordChangeNotAllowed -ne $user.UserCannotChangePassword))) + { + $user.UserCannotChangePassword = $PasswordChangeNotAllowed + $saveChanges = $true + + } + + if ($saveChanges) + { + $user.Save() + + # Send an operation success verbose message + if ($userExists) + { + Write-Verbose -Message ($script:localizedData.UserUpdated -f $UserName) + } + else + { + Write-Verbose -Message ($script:localizedData.UserCreated -f $UserName) + } + } + else + { + Write-Verbose -Message ($script:localizedData.NoConfigurationRequired -f $UserName) + } + } + } + else + { + # Ensure is set to 'Absent' + if ($null -ne $user) + { + # The user exists + if ($pscmdlet.ShouldProcess($script:localizedData.UserWithName -f $UserName, $script:localizedData.RemoveOperation)) + { + # Remove the user + $user.Delete() + } + + Write-Verbose -Message ($script:localizedData.UserRemoved -f $UserName) + } + else + { + Write-Verbose -Message ($script:localizedData.NoConfigurationRequiredUserDoesNotExist -f $UserName) + } + } + } + catch + { + New-InvalidOperationException -Message $_ + } + finally + { + if ($null -ne $user) + { + $user.Dispose() + } + + $principalContext.Dispose() + } + + Write-Verbose -Message ($script:localizedData.ConfigurationCompleted -f $UserName) +} + +<# + .SYNOPSIS + Tests if a user is in the desired state when on a full server. + + .PARAMETER UserName + The name of the user to test the state of. + + .PARAMETER Ensure + Specifies whether the user should exist or not. + By default this is set to Present + + .PARAMETER FullName + The full name/display name that the user should have. + If not provided, this value will not be tested. + + .PARAMETER Description + The description that the user should have. + If not provided, this value will not be tested. + + .PARAMETER Password + The password the user should have. + + .PARAMETER Disabled + Specifies whether the user account should be disabled or not. + + .PARAMETER PasswordNeverExpires + Specifies whether the password should ever expire or not. + + .PARAMETER PasswordChangeRequired + Not used in Test-TargetResource as there is no easy way to test this value. + + .PARAMETER PasswordChangeNotAllowed + Specifies whether the user should be allowed to change their password or not. +#> +function Test-TargetResourceOnFullSKU +{ + [OutputType([System.Boolean])] + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $UserName, + + [Parameter()] + [ValidateSet('Present', 'Absent')] + [System.String] + $Ensure = 'Present', + + [Parameter()] + [System.String] + $FullName, + + [Parameter()] + [System.String] + $Description, + + [Parameter()] + [ValidateNotNullOrEmpty()] + [System.Management.Automation.PSCredential] + [System.Management.Automation.Credential()] + $Password, + + [Parameter()] + [System.Boolean] + $Disabled, + + [Parameter()] + [System.Boolean] + $PasswordNeverExpires, + + [Parameter()] + [System.Boolean] + $PasswordChangeRequired, + + [Parameter()] + [System.Boolean] + $PasswordChangeNotAllowed + ) + + Set-StrictMode -Version Latest + + Assert-UserNameValid -UserName $UserName + + # Try to find a user by a name + $principalContext = New-Object System.DirectoryServices.AccountManagement.PrincipalContext ` + -ArgumentList ([System.DirectoryServices.AccountManagement.ContextType]::Machine) + + try + { + $user = [System.DirectoryServices.AccountManagement.UserPrincipal]::FindByIdentity($principalContext, $UserName) + if ($null -eq $user) + { + # A user with the provided name does not exist + Write-Verbose -Message ($script:localizedData.UserDoesNotExist -f $UserName) + + if ($Ensure -eq 'Absent') + { + return $true + } + else + { + return $false + } + } + + # A user with the provided name exists + Write-Verbose -Message ($script:localizedData.UserExists -f $UserName) + + # Validate separate properties + if ($Ensure -eq 'Absent') + { + # The Ensure property does not match + Write-Verbose -Message ($script:localizedData.PropertyMismatch -f 'Ensure', 'Absent', 'Present') + return $false + } + + if ($PSBoundParameters.ContainsKey('FullName') -and $FullName -ne $user.DisplayName) + { + # The FullName property does not match + Write-Verbose -Message ($script:localizedData.PropertyMismatch -f 'FullName', $FullName, $user.DisplayName) + return $false + } + + if ($PSBoundParameters.ContainsKey('Description') -and $Description -ne $user.Description) + { + # The Description property does not match + Write-Verbose -Message ($script:localizedData.PropertyMismatch -f 'Description', $Description, $user.Description) + return $false + } + + # Password + if ($PSBoundParameters.ContainsKey('Password')) + { + if (-not $principalContext.ValidateCredentials($UserName, $Password.GetNetworkCredential().Password)) + { + # The Password property does not match + Write-Verbose -Message ($script:localizedData.PasswordPropertyMismatch -f 'Password') + return $false + } + } + + if ($PSBoundParameters.ContainsKey('Disabled') -and $Disabled -eq $user.Enabled) + { + # The Disabled property does not match + Write-Verbose -Message ($script:localizedData.PropertyMismatch -f 'Disabled', $Disabled, $user.Enabled) + return $false + } + + if ($PSBoundParameters.ContainsKey('PasswordNeverExpires') -and $PasswordNeverExpires -ne $user.PasswordNeverExpires) + { + # The PasswordNeverExpires property does not match + Write-Verbose -Message ($script:localizedData.PropertyMismatch -f 'PasswordNeverExpires', $PasswordNeverExpires, $user.PasswordNeverExpires) + return $false + } + + if ($PSBoundParameters.ContainsKey('PasswordChangeNotAllowed') -and $PasswordChangeNotAllowed -ne $user.UserCannotChangePassword) + { + # The PasswordChangeNotAllowed property does not match + Write-Verbose -Message ($script:localizedData.PropertyMismatch -f 'PasswordChangeNotAllowed', $PasswordChangeNotAllowed, $user.UserCannotChangePassword) + return $false + } + } + catch + { + New-ConnectionException -ErrorId 'ConnectionError' -ErrorMessage ($script:localizedData.ConnectionError + $_) + } + + finally + { + if ($null -ne $user) + { + $user.Dispose() + } + + $principalContext.Dispose() + + } + + # All properties match + Write-Verbose -Message ($script:localizedData.AllUserPropertisMatch -f 'User', $UserName) + return $true +} + + +<# + .SYNOPSIS + Retrieves the user with the given username when on Nano Server. + + .PARAMETER UserName + The name of the user to retrieve. +#> +function Get-TargetResourceOnNanoServer +{ + [OutputType([System.Collections.Hashtable])] + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $UserName + ) + + Set-StrictMode -Version Latest + + Assert-UserNameValid -UserName $UserName + + # Try to find a user by a name + try + { + Write-Verbose -Message 'Starting Get-TargetResource on NanoServer' + [Microsoft.PowerShell.Commands.LocalUser] $user = Get-LocalUser -Name $UserName -ErrorAction Stop + } + catch [System.Exception] + { + if ($_.CategoryInfo.ToString().Contains('UserNotFoundException')) + { + # The user is not found + return @{ + UserName = $UserName + Ensure = 'Absent' + } + } + New-InvalidOperationException -ErrorRecord $_ + } + + # The user is found. Return all user properties and Ensure = 'Present'. + $returnValue = @{ + UserName = $user.Name + Ensure = 'Present' + FullName = $user.FullName + Description = $user.Description + Disabled = -not $user.Enabled + PasswordChangeRequired = $null + PasswordChangeNotAllowed = -not $user.UserMayChangePassword + } + + if ($user.PasswordExpires) + { + $returnValue.Add('PasswordNeverExpires', $false) + } + else + { + $returnValue.Add('PasswordNeverExpires', $true) + } + + return $returnValue +} + +<# + .SYNOPSIS + Creates, modifies, or deletes a user when on Nano Server. + + .PARAMETER UserName + The name of the user to create, modify, or delete. + + .PARAMETER Ensure + Specifies whether the user should exist or not. + By default this is set to Present + + .PARAMETER FullName + The (optional) full name or display name of the user. + If not provided this value will remain blank. + + .PARAMETER Description + Optional description for the user. + + .PARAMETER Password + The desired password for the user. + + .PARAMETER Disabled + Specifies whether the user should be disabled or not. + By default this is set to $false + + .PARAMETER PasswordNeverExpires + Specifies whether the password should ever expire or not. + By default this is set to $false + + .PARAMETER PasswordChangeRequired + Specifies whether the user must reset their password or not. + By default this is set to $false + + .PARAMETER PasswordChangeNotAllowed + Specifies whether the user is allowed to change their password or not. + By default this is set to $false + + .NOTES + If Ensure is set to 'Present' then the Password parameter is required. +#> +function Set-TargetResourceOnNanoServer +{ + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $UserName, + + [Parameter()] + [ValidateSet('Present', 'Absent')] + [System.String] + $Ensure = 'Present', + + [Parameter()] + [System.String] + $FullName, + + [Parameter()] + [System.String] + $Description, + + [Parameter()] + [ValidateNotNullOrEmpty()] + [System.Management.Automation.PSCredential] + [System.Management.Automation.Credential()] + $Password, + + [Parameter()] + [System.Boolean] + $Disabled, + + [Parameter()] + [System.Boolean] + $PasswordNeverExpires, + + [Parameter()] + [System.Boolean] + $PasswordChangeRequired, + + [Parameter()] + [System.Boolean] + $PasswordChangeNotAllowed + ) + + Set-StrictMode -Version Latest + + Write-Verbose -Message ($script:localizedData.ConfigurationStarted -f $UserName) + + Assert-UserNameValid -UserName $UserName + + # Try to find a user by a name. + $userExists = $false + + try + { + [Microsoft.PowerShell.Commands.LocalUser] $user = Get-LocalUser -Name $UserName -ErrorAction Stop + $userExists = $true + } + catch [System.Exception] + { + if ($_.CategoryInfo.ToString().Contains('UserNotFoundException')) + { + # The user is not found. + Write-Verbose -Message ($script:localizedData.UserDoesNotExist -f $UserName) + } + else + { + New-InvalidOperationException -ErrorRecord $_ + } + } + + if ($Ensure -eq 'Present') + { + # Ensure is set to 'Present' + + if (-not $userExists) + { + # The user with the provided name does not exist so add a new user + New-LocalUser -Name $UserName -NoPassword + Write-Verbose -Message ($script:localizedData.UserCreated -f $UserName) + } + + # Set user properties + if ($PSBoundParameters.ContainsKey('FullName')) + { + if (-not $userExists -or $FullName -ne $user.FullName) + { + if ($null -eq $FullName) + { + Set-LocalUser -Name $UserName -FullName ([System.String]::Empty) + } + else + { + Set-LocalUser -Name $UserName -FullName $FullName + } + } + } + else + { + if (-not $userExists) + { + # For a newly created user, set the DisplayName property to an empty string since by default DisplayName is set to user's name. + Set-LocalUser -Name $UserName -FullName ([System.String]::Empty) + } + } + + if ($PSBoundParameters.ContainsKey('Description') -and ((-not $userExists) -or ($Description -ne $user.Description))) + { + if ($null -eq $Description) + { + Set-LocalUser -Name $UserName -Description ([System.String]::Empty) + } + else + { + Set-LocalUser -Name $UserName -Description $Description + } + } + + # Set the password regardless of the state of the user + if ($PSBoundParameters.ContainsKey('Password')) + { + Set-LocalUser -Name $UserName -Password $Password.Password + } + + if ($PSBoundParameters.ContainsKey('Disabled') -and ((-not $userExists) -or ($Disabled -eq $user.Enabled))) + { + if ($Disabled) + { + Disable-LocalUser -Name $UserName + } + else + { + Enable-LocalUser -Name $UserName + } + } + + $existingUserPasswordNeverExpires = (($userExists) -and ($null -eq $user.PasswordExpires)) + if ($PSBoundParameters.ContainsKey('PasswordNeverExpires') -and ((-not $userExists) -or ($PasswordNeverExpires -ne $existingUserPasswordNeverExpires))) + { + Set-LocalUser -Name $UserName -PasswordNeverExpires:$passwordNeverExpires + } + + if ($PSBoundParameters.ContainsKey('PasswordChangeRequired') -and ($PasswordChangeRequired)) + { + Set-LocalUser -Name $UserName -AccountExpires ([System.DateTime]::Now) + } + + # NOTE: The parameter name and the property name have opposite meaning. + [System.Boolean] $expected = -not $PasswordChangeNotAllowed + $actual = $expected + + if ($userExists) + { + $actual = $user.UserMayChangePassword + } + + if ($PSBoundParameters.ContainsKey('PasswordChangeNotAllowed') -and ((-not $userExists) -or ($expected -ne $actual))) + { + Set-LocalUser -Name $UserName -UserMayChangePassword $expected + } + } + else + { + # Ensure is set to 'Absent' + if ($userExists) + { + # The user exists + Remove-LocalUser -Name $UserName + + Write-Verbose -Message ($script:localizedData.UserRemoved -f $UserName) + } + else + { + Write-Verbose -Message ($script:localizedData.NoConfigurationRequiredUserDoesNotExist -f $UserName) + } + } + + Write-Verbose -Message ($script:localizedData.ConfigurationCompleted -f $UserName) +} + +<# + .SYNOPSIS + Tests if a user is in the desired state when on Nano Server. + + .PARAMETER UserName + The name of the user to test the state of. + + .PARAMETER Ensure + Specifies whether the user should exist or not. + By default this is set to Present + + .PARAMETER FullName + The full name/display name that the user should have. + If not provided, this value will not be tested. + + .PARAMETER Description + The description that the user should have. + If not provided, this value will not be tested. + + .PARAMETER Password + The password the user should have. + + .PARAMETER Disabled + Specifies whether the user account should be disabled or not. + + .PARAMETER PasswordNeverExpires + Specifies whether the password should ever expire or not. + + .PARAMETER PasswordChangeRequired + Not used in Test-TargetResource as there is no easy way to test this value. + + .PARAMETER PasswordChangeNotAllowed + Specifies whether the user should be allowed to change their password or not. +#> +function Test-TargetResourceOnNanoServer +{ + [OutputType([System.Boolean])] + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $UserName, + + [Parameter()] + [ValidateSet('Present', 'Absent')] + [System.String] + $Ensure = 'Present', + + [Parameter()] + [System.String] + $FullName, + + [Parameter()] + [System.String] + $Description, + + [Parameter()] + [ValidateNotNullOrEmpty()] + [System.Management.Automation.PSCredential] + [System.Management.Automation.Credential()] + $Password, + + [Parameter()] + [System.Boolean] + $Disabled, + + [Parameter()] + [System.Boolean] + $PasswordNeverExpires, + + [Parameter()] + [System.Boolean] + $PasswordChangeRequired, + + [Parameter()] + [System.Boolean] + $PasswordChangeNotAllowed + ) + + Set-StrictMode -Version Latest + + Assert-UserNameValid -UserName $UserName + + # Try to find a user by a name + try + { + [Microsoft.PowerShell.Commands.LocalUser] $user = Get-LocalUser -Name $UserName -ErrorAction Stop + } + catch [System.Exception] + { + if ($_.CategoryInfo.ToString().Contains('UserNotFoundException')) + { + # The user is not found + if ($Ensure -eq 'Absent') + { + return $true + } + else + { + return $false + } + } + New-InvalidOperationException -ErrorRecord $_ + } + + # A user with the provided name exists + Write-Verbose -Message ($script:localizedData.UserExists -f $UserName) + + # Validate separate properties + if ($Ensure -eq 'Absent') + { + # The Ensure property does not match + Write-Verbose -Message ($script:localizedData.PropertyMismatch -f 'Ensure', 'Absent', 'Present') + return $false + } + + if ($PSBoundParameters.ContainsKey('FullName') -and $FullName -ne $user.FullName) + { + # The FullName property does not match + Write-Verbose -Message ($script:localizedData.PropertyMismatch -f 'FullName', $FullName, $user.FullName) + return $false + } + + if ($PSBoundParameters.ContainsKey('Description') -and $Description -ne $user.Description) + { + # The Description property does not match + Write-Verbose -Message ($script:localizedData.PropertyMismatch -f 'Description', $Description, $user.Description) + return $false + } + + if ($PSBoundParameters.ContainsKey('Password')) + { + if (-not (Test-CredentialsValidOnNanoServer -UserName $UserName -Password $Password.Password)) + { + # The Password property does not match + Write-Verbose -Message ($script:localizedData.PasswordPropertyMismatch -f 'Password') + return $false + } + } + + if ($PSBoundParameters.ContainsKey('Disabled') -and ($Disabled -eq $user.Enabled)) + { + # The Disabled property does not match + Write-Verbose -Message ($script:localizedData.PropertyMismatch -f 'Disabled', $Disabled, $user.Enabled) + return $false + } + + $existingUserPasswordNeverExpires = ($null -eq $user.PasswordExpires) + if ($PSBoundParameters.ContainsKey('PasswordNeverExpires') -and $PasswordNeverExpires -ne $existingUserPasswordNeverExpires) + { + # The PasswordNeverExpires property does not match + Write-Verbose -Message ($script:localizedData.PropertyMismatch -f 'PasswordNeverExpires', $PasswordNeverExpires, $existingUserPasswordNeverExpires) + return $false + } + + if ($PSBoundParameters.ContainsKey('PasswordChangeNotAllowed') -and $PasswordChangeNotAllowed -ne (-not $user.UserMayChangePassword)) + { + # The PasswordChangeNotAllowed property does not match + Write-Verbose -Message ($script:localizedData.PropertyMismatch -f 'PasswordChangeNotAllowed', $PasswordChangeNotAllowed, (-not $user.UserMayChangePassword)) + return $false + } + + # All properties match. Return $true. + Write-Verbose -Message ($script:localizedData.AllUserPropertisMatch -f 'User', $UserName) + return $true +} + +<# + .SYNOPSIS + Checks that the username does not contain invalid characters. + + .PARAMETER UserName + The username to validate. +#> +function Assert-UserNameValid +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $UserName + ) + + # Check if the name consists of only periods and/or white spaces + $wrongName = $true + + for ($i = 0; $i -lt $UserName.Length; $i++) + { + if (-not [System.Char]::IsWhiteSpace($UserName, $i) -and $UserName[$i] -ne '.') + { + $wrongName = $false + break + } + } + + $invalidChars = @('\', '/', '"', '[', ']', ':', '|', '<', '>', '+', '=', ';', ',', '?', '*', '@') + + if ($wrongName) + { + New-InvalidArgumentException ` + -Message ($script:localizedData.InvalidUserName -f $UserName, [System.String]::Join(' ', $invalidChars)) ` + -ArgumentName 'UserName' + } + + if ($UserName.IndexOfAny($invalidChars) -ne -1) + { + New-InvalidArgumentException ` + -Message ($script:localizedData.InvalidUserName -f $UserName, [System.String]::Join(' ', $invalidChars)) ` + -ArgumentName 'UserName' + } +} + +<# + .SYNOPSIS + Creates a new Connection error record and throws it. + + .PARAMETER ErrorId + The ID for the error record to be thrown. + + .PARAMETER ErrorMessage + Message to be included in the error record to be thrown. +#> +function New-ConnectionException +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $ErrorId, + + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $ErrorMessage + ) + + $errorCategory = [System.Management.Automation.ErrorCategory]::ConnectionError + $exception = New-Object ` + -TypeName System.ArgumentException ` + -ArgumentList $ErrorMessage + $errorRecord = New-Object ` + -TypeName System.Management.Automation.ErrorRecord ` + -ArgumentList @($exception, $ErrorId, $errorCategory, $null) + throw $errorRecord +} + +<# + .SYNOPSIS + Tests the local user's credentials on the local machine. + + .PARAMETER UserName + The username to validate the credentials of. + + .PARAMETER Password + The password of the given user. +#> +function Test-CredentialsValidOnNanoServer +{ + [OutputType([System.Boolean])] + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $UserName, + + [Parameter()] + [ValidateNotNullOrEmpty()] + [System.Security.SecureString] + $Password + ) + + $source = @' + [Flags] + private enum LogonType + { + Logon32LogonInteractive = 2, + Logon32LogonNetwork, + Logon32LogonBatch, + Logon32LogonService, + Logon32LogonUnlock, + Logon32LogonNetworkCleartext, + Logon32LogonNewCredentials + } + + [Flags] + private enum LogonProvider + { + Logon32ProviderDefault = 0, + Logon32ProviderWinnt35, + Logon32ProviderWinnt40, + Logon32ProviderWinnt50 + } + + [DllImport("api-ms-win-security-logon-l1-1-1.dll", CharSet = CharSet.Unicode, SetLastError = true)] + private static extern Boolean LogonUser( + String lpszUserName, + String lpszDomain, + IntPtr lpszPassword, + LogonType dwLogonType, + LogonProvider dwLogonProvider, + out IntPtr phToken + ); + + + [DllImport("api-ms-win-core-handle-l1-1-0.dll", + EntryPoint = "CloseHandle", SetLastError = true, + CharSet = CharSet.Unicode, CallingConvention = CallingConvention.StdCall)] + internal static extern bool CloseHandle(IntPtr handle); + + public static bool ValidateCredentials(string username, SecureString password) + { + IntPtr tokenHandle = IntPtr.Zero; + IntPtr unmanagedPassword = IntPtr.Zero; + + unmanagedPassword = SecureStringMarshal.SecureStringToCoTaskMemUnicode(password); + + try + { + return LogonUser( + username, + null, + unmanagedPassword, + LogonType.Logon32LogonInteractive, + LogonProvider.Logon32ProviderDefault, + out tokenHandle); + } + catch + { + return false; + } + finally + { + if (tokenHandle != IntPtr.Zero) + { + CloseHandle(tokenHandle); + } + if (unmanagedPassword != IntPtr.Zero) { + Marshal.ZeroFreeCoTaskMemUnicode(unmanagedPassword); + } + unmanagedPassword = IntPtr.Zero; + } + } +'@ + + Add-Type -PassThru -Namespace Microsoft.Windows.DesiredStateConfiguration.NanoServer.UserResource ` + -Name CredentialsValidationTool -MemberDefinition $source -Using System.Security -ReferencedAssemblies System.Security.SecureString.dll | Out-Null + return [Microsoft.Windows.DesiredStateConfiguration.NanoServer.UserResource.CredentialsValidationTool]::ValidateCredentials($UserName, $Password) +} + +Export-ModuleMember -Function *-TargetResource diff --git a/deployment/dsc/azshcihost/xPSDesiredStateConfiguration/9.1.0/DSCResources/DSC_xUserResource/DSC_xUserResource.schema.mof b/deployment/dsc/azshcihost/xPSDesiredStateConfiguration/9.1.0/DSCResources/DSC_xUserResource/DSC_xUserResource.schema.mof new file mode 100644 index 0000000..5bb0191 --- /dev/null +++ b/deployment/dsc/azshcihost/xPSDesiredStateConfiguration/9.1.0/DSCResources/DSC_xUserResource/DSC_xUserResource.schema.mof @@ -0,0 +1,13 @@ +[ClassVersion("1.0.0"), FriendlyName("xUser")] +class DSC_xUserResource : OMI_BaseResource +{ + [Key,Description("The name of the User to Create/Modify/Delete")] String UserName; + [Write,Description("An enumerated value that describes if the user is expected to exist on the machine"),ValueMap{"Present", "Absent"},Values{"Present", "Absent"}] String Ensure; + [Write,Description("The display name of the user")] String FullName; + [Write,Description("A description for the user")] String Description; + [Write,Description("The password for the user"),EmbeddedInstance("MSFT_Credential")] String Password; + [Write,Description("Value used to disable/enable a user account")] Boolean Disabled; + [Write,Description("Value used to set whether a user's password expires or not")] Boolean PasswordNeverExpires; + [Write,Description("Value used to require a user to change their password")] Boolean PasswordChangeRequired; + [Write,Description("Value used to set whether a user can/cannot change their password")] Boolean PasswordChangeNotAllowed; +}; diff --git a/deployment/dsc/azshcihost/xPSDesiredStateConfiguration/9.1.0/DSCResources/DSC_xUserResource/en-US/DSC_xUserResource.schema.mfl b/deployment/dsc/azshcihost/xPSDesiredStateConfiguration/9.1.0/DSCResources/DSC_xUserResource/en-US/DSC_xUserResource.schema.mfl new file mode 100644 index 0000000..1723e04 --- /dev/null +++ b/deployment/dsc/azshcihost/xPSDesiredStateConfiguration/9.1.0/DSCResources/DSC_xUserResource/en-US/DSC_xUserResource.schema.mfl @@ -0,0 +1,13 @@ +[Description("This resource is used to manage local user accounts.") : Amended,AMENDMENT, LOCALE("MS_409")] +class DSC_xUserResource : OMI_BaseResource +{ + [Key, Description("The name of the User to Create/Modify/Delete") : Amended] String UserName; + [Write, Description("An enumerated value that describes if the user is expected to exist on the machine, ValueMap{"Present", "Absent"}, Values{"Present", "Absent"}] String Ensure; + [Write, Description("The full name of the user") : Amended] String FullName; + [Write, Description("A description for the user") : Amended] String Description; + [Write, Description("The password for the user"), (EmbeddedInstance("MSFT_Credential") : Amended] String Password; + [Write, Description("Value used to disable/enable a user account") : Amended] Boolean Disabled; + [Write, Description("Value used to set whether a user's password expires or not") : Amended] Boolean PasswordNeverExpires; + [Write, Description("Value used to require a user to change their password") : Amended] Boolean PasswordChangeRequired; + [Write, Description("Value used to set whether a user can/cannot change their password") : Amended] Boolean PasswordChangeNotAllowed; +}; diff --git a/deployment/dsc/azshcihost/xPSDesiredStateConfiguration/9.1.0/DSCResources/DSC_xUserResource/en-US/DSC_xUserResource.strings.psd1 b/deployment/dsc/azshcihost/xPSDesiredStateConfiguration/9.1.0/DSCResources/DSC_xUserResource/en-US/DSC_xUserResource.strings.psd1 new file mode 100644 index 0000000..ee860bb --- /dev/null +++ b/deployment/dsc/azshcihost/xPSDesiredStateConfiguration/9.1.0/DSCResources/DSC_xUserResource/en-US/DSC_xUserResource.strings.psd1 @@ -0,0 +1,23 @@ +# Localized resources for DSC_xUserResource + +ConvertFrom-StringData @' + UserWithName = User: {0} + RemoveOperation = Remove + AddOperation = Add + SetOperation = Set + ConfigurationStarted = Configuration of user {0} started. + ConfigurationCompleted = Configuration of user {0} completed successfully. + UserCreated = User {0} created successfully. + UserUpdated = User {0} properties updated successfully. + UserRemoved = User {0} removed successfully. + NoConfigurationRequired = User {0} exists on this node with the desired properties. No action required. + NoConfigurationRequiredUserDoesNotExist = User {0} does not exist on this node. No action required. + InvalidUserName = The name {0} cannot be used. Names may not consist entirely of periods and/or spaces, or contain these characters: {1} + UserExists = A user with the name {0} exists. + UserDoesNotExist = A user with the name {0} does not exist. + PropertyMismatch = The value of the {0} property is expected to be {1} but it is {2}. + PasswordPropertyMismatch = The value of the {0} property does not match. + AllUserPropertisMatch = All {0} {1} properties match. + ConnectionError = There could be a possible connection error while trying to use the System.DirectoryServices API's. + MultipleMatches = There could be a possible multiple matches exception while trying to use the System.DirectoryServices API's. +'@ diff --git a/deployment/dsc/azshcihost/xPSDesiredStateConfiguration/9.1.0/DSCResources/DSC_xWindowsFeature/DSC_xWindowsFeature.psm1 b/deployment/dsc/azshcihost/xPSDesiredStateConfiguration/9.1.0/DSCResources/DSC_xWindowsFeature/DSC_xWindowsFeature.psm1 new file mode 100644 index 0000000..41fd156 --- /dev/null +++ b/deployment/dsc/azshcihost/xPSDesiredStateConfiguration/9.1.0/DSCResources/DSC_xWindowsFeature/DSC_xWindowsFeature.psm1 @@ -0,0 +1,568 @@ +$errorActionPreference = 'Stop' +Set-StrictMode -Version 'Latest' + +$modulePath = Join-Path -Path (Split-Path -Path (Split-Path -Path $PSScriptRoot -Parent) -Parent) -ChildPath 'Modules' + +# Import the shared modules +Import-Module -Name (Join-Path -Path $modulePath ` + -ChildPath (Join-Path -Path 'xPSDesiredStateConfiguration.Common' ` + -ChildPath 'xPSDesiredStateConfiguration.Common.psm1')) + +# Import Localization Strings +$script:localizedData = Get-LocalizedData -ResourceName 'DSC_xWindowsFeature' + +<# + .SYNOPSIS + Retrieves the status of the role or feature with the given name on the target machine. + + .PARAMETER Name + The name of the role or feature to retrieve + + .PARAMETER Credential + The credential (if required) to retrieve the role or feature. + Optional. + + .NOTES + If the specified role or feature does not contain any subfeatures then + IncludeAllSubFeature will be set to $false. If the specified feature contains one + or more subfeatures then IncludeAllSubFeature will be set to $true only if all the + subfeatures are installed. Otherwise, IncludeAllSubFeature will be set to $false. +#> +function Get-TargetResource +{ + [CmdletBinding()] + [OutputType([System.Collections.Hashtable])] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $Name, + + [Parameter()] + [ValidateNotNullOrEmpty()] + [System.Management.Automation.PSCredential] + [System.Management.Automation.Credential()] + $Credential + ) + + Write-Verbose -Message ($script:localizedData.GetTargetResourceStartMessage -f $Name) + + Import-ServerManager + + Write-Verbose -Message ($script:localizedData.QueryFeature -f $Name) + + $isWinServer2008R2SP1 = Test-IsWinServer2008R2SP1 + if ($isWinServer2008R2SP1 -and $PSBoundParameters.ContainsKey('Credential')) + { + $feature = Invoke-Command -ScriptBlock { Get-WindowsFeature -Name $Name } ` + -ComputerName . ` + -Credential $Credential ` + } + else + { + $feature = Get-WindowsFeature @PSBoundParameters + } + + Assert-SingleInstanceOfFeature -Feature $feature -Name $Name + + $includeAllSubFeature = $true + + if ($feature.SubFeatures.Count -eq 0) + { + $includeAllSubFeature = $false + } + else + { + foreach ($currentSubFeatureName in $feature.SubFeatures) + { + $getWindowsFeatureParameters = @{ + Name = $currentSubFeatureName + } + + if ($PSBoundParameters.ContainsKey('Credential')) + { + $getWindowsFeatureParameters['Credential'] = $Credential + } + + if ($isWinServer2008R2SP1 -and $PSBoundParameters.ContainsKey('Credential')) + { + <# + Calling Get-WindowsFeature through Invoke-Command to start a new process with + the given credential since Get-WindowsFeature doesn't support the Credential + attribute on this server. + #> + $subFeature = Invoke-Command -ScriptBlock { Get-WindowsFeature -Name $currentSubFeatureName } ` + -ComputerName . ` + -Credential $Credential ` + } + else + { + $subFeature = Get-WindowsFeature @getWindowsFeatureParameters + } + + Assert-SingleInstanceOfFeature -Feature $subFeature -Name $currentSubFeatureName + + if (-not $subFeature.Installed) + { + $includeAllSubFeature = $false + break + } + } + } + + if ($feature.Installed) + { + $ensureResult = 'Present' + } + else + { + $ensureResult = 'Absent' + } + + Write-Verbose -Message ($script:localizedData.GetTargetResourceEndMessage -f $Name) + + # Add all feature properties to the hash table + return @{ + Name = $Name + DisplayName = $feature.DisplayName + Ensure = $ensureResult + IncludeAllSubFeature = $includeAllSubFeature + } +} + +<# + .SYNOPSIS + Installs or uninstalls the role or feature with the given name on the target machine + with the option of installing or uninstalling all subfeatures as well. + + .PARAMETER Name + The name of the role or feature to install or uninstall. + + .PARAMETER Ensure + Specifies whether the role or feature should be installed ('Present') + or uninstalled ('Absent'). + By default this is set to Present. + + .PARAMETER IncludeAllSubFeature + Specifies whether or not all subfeatures should be installed or uninstalled with + the specified role or feature. Default is false. + If this property is true and Ensure is set to Present, all subfeatures will be installed. + If this property is false and Ensure is set to Present, subfeatures will not be installed or uninstalled. + If Ensure is set to Absent, all subfeatures will be uninstalled. + + .PARAMETER Credential + The credential (if required) to install or uninstall the role or feature. + Optional. + + .PARAMETER LogPath + The custom path to the log file to log this operation. + If not passed in, the default log path will be used (%windir%\logs\ServerManager.log). +#> +function Set-TargetResource +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $Name, + + [Parameter()] + [ValidateSet('Present', 'Absent')] + [System.String] + $Ensure = 'Present', + + [Parameter()] + [System.Boolean] + $IncludeAllSubFeature = $false, + + [Parameter()] + [ValidateNotNullOrEmpty()] + [System.Management.Automation.PSCredential] + [System.Management.Automation.Credential()] + $Credential, + + [Parameter()] + [ValidateNotNullOrEmpty()] + [System.String] + $LogPath + ) + + Write-Verbose -Message ($script:localizedData.SetTargetResourceStartMessage -f $Name) + + Import-ServerManager + + $isWinServer2008R2SP1 = Test-IsWinServer2008R2SP1 + + if ($Ensure -eq 'Present') + { + $addWindowsFeatureParameters = @{ + Name = $Name + IncludeAllSubFeature = $IncludeAllSubFeature + } + + if ($PSBoundParameters.ContainsKey('LogPath')) + { + $addWindowsFeatureParameters['LogPath'] = $LogPath + } + + Write-Verbose -Message ($script:localizedData.InstallFeature -f $Name) + + if ($isWinServer2008R2SP1 -and $PSBoundParameters.ContainsKey('Credential')) + { + <# + Calling Add-WindowsFeature through Invoke-Command to start a new process with + the given credential since Add-WindowsFeature doesn't support the Credential + attribute on this server. + #> + $feature = Invoke-Command -ScriptBlock { Add-WindowsFeature @addWindowsFeatureParameters } ` + -ComputerName . ` + -Credential $Credential + } + else + { + if ($PSBoundParameters.ContainsKey('Credential')) + { + $addWindowsFeatureParameters['Credential'] = $Credential + } + + $feature = Add-WindowsFeature @addWindowsFeatureParameters + } + + if ($null -ne $feature -and $feature.Success) + { + Write-Verbose -Message ($script:localizedData.InstallSuccess -f $Name) + + # Check if reboot is required, if so notify the Local Configuration Manager. + if ($feature.RestartNeeded -eq 'Yes') + { + Write-Verbose -Message $script:localizedData.RestartNeeded + Set-DSCMachineRebootRequired + } + } + else + { + New-InvalidOperationException -Message ($script:localizedData.FeatureInstallationFailureError -f $Name) + } + } + # Ensure = 'Absent' + else + { + $removeWindowsFeatureParameters = @{ + Name = $Name + } + + if ($PSBoundParameters.ContainsKey('LogPath')) + { + $removeWindowsFeatureParameters['LogPath'] = $LogPath + } + + Write-Verbose -Message ($script:localizedData.UninstallFeature -f $Name) + + if ($isWinServer2008R2SP1 -and $PSBoundParameters.ContainsKey('Credential')) + { + <# + Calling Remove-WindowsFeature through Invoke-Command to start a new process with + the given credential since Remove-WindowsFeature doesn't support the Credential + attribute on this server. + #> + $feature = Invoke-Command -ScriptBlock { Remove-WindowsFeature @removeWindowsFeatureParameters } ` + -ComputerName . ` + -Credential $Credential + } + else + { + if ($PSBoundParameters.ContainsKey('Credential')) + { + $addWindowsFeatureParameters['Credential'] = $Credential + } + + $feature = Remove-WindowsFeature @removeWindowsFeatureParameters + } + + if ($null -ne $feature -and $feature.Success) + { + Write-Verbose ($script:localizedData.UninstallSuccess -f $Name) + + # Check if reboot is required, if so notify the Local Configuration Manager. + if ($feature.RestartNeeded -eq 'Yes') + { + Write-Verbose -Message $script:localizedData.RestartNeeded + Set-DSCMachineRebootRequired + } + } + else + { + New-InvalidOperationException -Message ($script:localizedData.FeatureUninstallationFailureError -f $Name) + } + } + + Write-Verbose -Message ($script:localizedData.SetTargetResourceEndMessage -f $Name) +} + +<# + .SYNOPSIS + Tests if the role or feature with the given name is in the desired state. + + .PARAMETER Name + The name of the role or feature to test the state of. + + .PARAMETER Ensure + Specifies whether the role or feature should be installed ('Present') + or uninstalled ('Absent'). + By default this is set to Present. + + .PARAMETER IncludeAllSubFeature + Specifies whether the subfeatures of the indicated role or feature should also be checked + to ensure they are in the desired state. If Ensure is set to 'Present' and this is set to + $true then each subfeature is checked to ensure it is installed as well. If Ensure is set to + Absent and this is set to $true, then each subfeature is checked to ensure it is uninstalled. + As of now, this test can't be used to check if a feature is Installed but all of its + subfeatures are uninstalled. + By default this is set to $false. + + .PARAMETER Credential + The Credential (if required) to test the status of the role or feature. + Optional. + + .PARAMETER LogPath + The path to the log file to log this operation. + Not used in Test-TargetResource. + +#> +function Test-TargetResource +{ + [CmdletBinding()] + [OutputType([System.Boolean])] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $Name, + + [Parameter()] + [ValidateSet('Present', 'Absent')] + [System.String] + $Ensure = 'Present', + + [Parameter()] + [System.Boolean] + $IncludeAllSubFeature = $false, + + [Parameter()] + [ValidateNotNullOrEmpty()] + [System.Management.Automation.PSCredential] + [System.Management.Automation.Credential()] + $Credential, + + [Parameter()] + [ValidateNotNullOrEmpty()] + [System.String] + $LogPath + + ) + + Write-Verbose -Message ($script:localizedData.TestTargetResourceStartMessage -f $Name) + + Import-ServerManager + + $testTargetResourceResult = $false + + $getWindowsFeatureParameters = @{ + Name = $Name + } + + if ($PSBoundParameters.ContainsKey('Credential')) + { + $getWindowsFeatureParameters['Credential'] = $Credential + } + + Write-Verbose -Message ($script:localizedData.QueryFeature -f $Name) + + $isWinServer2008R2SP1 = Test-IsWinServer2008R2SP1 + if ($isWinServer2008R2SP1 -and $PSBoundParameters.ContainsKey('Credential')) + { + <# + Calling Get-WindowsFeature through Invoke-Command to start a new process with + the given credential since Get-WindowsFeature doesn't support the Credential + attribute on this server. + #> + $feature = Invoke-Command -ScriptBlock { Get-WindowsFeature -Name $Name } ` + -ComputerName . ` + -Credential $Credential + } + else + { + $feature = Get-WindowsFeature @getWindowsFeatureParameters + } + + Assert-SingleInstanceOfFeature -Feature $feature -Name $Name + + # Check if the feature is in the requested Ensure state. + if (($Ensure -eq 'Present' -and $feature.Installed -eq $true) -or ` + ($Ensure -eq 'Absent' -and $feature.Installed -eq $false)) + { + $testTargetResourceResult = $true + + if ($IncludeAllSubFeature) + { + # Check if each subfeature is in the requested state. + foreach ($currentSubFeatureName in $feature.SubFeatures) + { + $getWindowsFeatureParameters['Name'] = $currentSubFeatureName + + if ($isWinServer2008R2SP1 -and $PSBoundParameters.ContainsKey('Credential')) + { + <# + Calling Get-WindowsFeature through Invoke-Command to start a new process with + the given credential since Get-WindowsFeature doesn't support the Credential + attribute on this server. + #> + $subFeature = Invoke-Command -ScriptBlock { Get-WindowsFeature -Name $currentSubFeatureName } ` + -ComputerName . ` + -Credential $Credential + } + else + { + $subFeature = Get-WindowsFeature @getWindowsFeatureParameters + } + + Assert-SingleInstanceOfFeature -Feature $subFeature -Name $currentSubFeatureName + + if (-not $subFeature.Installed -and $Ensure -eq 'Present') + { + $testTargetResourceResult = $false + break + } + + if ($subFeature.Installed -and $Ensure -eq 'Absent') + { + $testTargetResourceResult = $false + break + } + } + } + } + else + { + # Ensure is not in the correct state + $testTargetResourceResult = $false + } + + Write-Verbose -Message ($script:localizedData.TestTargetResourceEndMessage -f $Name) + + return $testTargetResourceResult +} + + +<# + .SYNOPSIS + Asserts that a single instance of the given role or feature exists. + + .PARAMETER Feature + The role or feature object to check. + + .PARAMETER Name + The name of the role or feature to include in any error messages that are thrown. + (Not used to assert validity of the feature). +#> +function Assert-SingleInstanceOfFeature +{ + [CmdletBinding()] + param + ( + [Parameter()] + [System.Management.Automation.PSObject[]] + $Feature, + + [Parameter()] + [System.String] + $Name + ) + + if ($null -eq $Feature) + { + New-InvalidOperationException -Message ($script:localizedData.FeatureNotFoundError -f $Name) + } + + if ($Feature.Count -gt 1) + { + New-InvalidOperationException -Message ($script:localizedData.MultipleFeatureInstancesError -f $Name) + } +} + +<# + .SYNOPSIS + Sets up the ServerManager module on the target node. + Throws an error if not on a machine running Windows Server. +#> +function Import-ServerManager +{ + param + () + + <# + Enable ServerManager-PSH-Cmdlets feature if OS is WS2008R2 Core. + Datacenter = 12, Standard = 13, Enterprise = 14 + #> + $serverCoreOSCodes = @( 12, 13, 14 ) + + $operatingSystem = Get-CimInstance -Class 'Win32_OperatingSystem' + + # Check if this operating system needs an update to the ServerManager cmdlets + if ($operatingSystem.Version.StartsWith('6.1.') -and ` + $serverCoreOSCodes -contains $operatingSystem.OperatingSystemSKU) + { + Write-Verbose -Message $script:localizedData.EnableServerManagerPSHCmdletsFeature + + <# + ServerManager-PSH-Cmdlets has a depndency on Powershell 2 update: MicrosoftWindowsPowerShell, + so enabling the MicrosoftWindowsPowerShell update. + #> + $null = Dism\online\enable-feature\FeatureName:MicrosoftWindowsPowerShell + $null = Dism\online\enable-feature\FeatureName:ServerManager-PSH-Cmdlets + } + + try + { + Import-Module -Name 'ServerManager' -ErrorAction Stop + } + catch [System.Management.Automation.RuntimeException] + { + if ($_.Exception.Message -like "*Some or all identity references could not be translated*") + { + Write-Verbose $_.Exception.Message + } + else + { + Write-Verbose -Message $script:localizedData.ServerManagerModuleNotFoundMessage + New-InvalidOperationException -Message $script:localizedData.SkuNotSupported + } + } + catch + { + Write-Verbose -Message $script:localizedData.ServerManagerModuleNotFoundMessage + New-InvalidOperationException -Message $script:localizedData.SkuNotSupported + } +} + +<# + .SYNOPSIS + Tests if the machine is a Windows Server 2008 R2 SP1 machine. + + .NOTES + Since Assert-PrequisitesValid ensures that ServerManager is available on the machine, + this function only checks the OS version. +#> +function Test-IsWinServer2008R2SP1 +{ + param + () + + return ([Environment]::OSVersion.Version.ToString().Contains('6.1.')) +} + +Export-ModuleMember -Function *-TargetResource diff --git a/deployment/dsc/azshcihost/xPSDesiredStateConfiguration/9.1.0/DSCResources/DSC_xWindowsFeature/DSC_xWindowsFeature.schema.mof b/deployment/dsc/azshcihost/xPSDesiredStateConfiguration/9.1.0/DSCResources/DSC_xWindowsFeature/DSC_xWindowsFeature.schema.mof new file mode 100644 index 0000000..94e1e83 --- /dev/null +++ b/deployment/dsc/azshcihost/xPSDesiredStateConfiguration/9.1.0/DSCResources/DSC_xWindowsFeature/DSC_xWindowsFeature.schema.mof @@ -0,0 +1,11 @@ + +[ClassVersion("1.0.0.0"), FriendlyName("xWindowsFeature")] +class DSC_xWindowsFeature : OMI_BaseResource +{ + [Key, Description("The name of the role or feature to install or uninstall.")] String Name; + [Write, Description("Specifies whether the role or feature should be installed or uninstalled. To install the feature, set this property to Present. To uninstall the feature, set the property to Absent."), ValueMap{"Present", "Absent"}, Values{"Present", "Absent"}] String Ensure; + [Write, Description("Specifies whether the subfeatures of the main feature should also be installed.")] Boolean IncludeAllSubFeature; + [Write, Description("The path to the log file to log this operation.")] String LogPath; + [Write, Description("A credential, if needed, to install or uninstall the role or feature."), EmbeddedInstance("MSFT_Credential")] String Credential; + [Read, Description("The display name of the retrieved role or feature.")] String DisplayName; +}; diff --git a/deployment/dsc/azshcihost/xPSDesiredStateConfiguration/9.1.0/DSCResources/DSC_xWindowsFeature/en-US/DSC_xWindowsFeature.schema.mfl b/deployment/dsc/azshcihost/xPSDesiredStateConfiguration/9.1.0/DSCResources/DSC_xWindowsFeature/en-US/DSC_xWindowsFeature.schema.mfl new file mode 100644 index 0000000..e87be67 --- /dev/null +++ b/deployment/dsc/azshcihost/xPSDesiredStateConfiguration/9.1.0/DSCResources/DSC_xWindowsFeature/en-US/DSC_xWindowsFeature.schema.mfl @@ -0,0 +1,11 @@ +[Description("This resource is used to install, uninstall and query roles or features on the DSC managed node.") : Amended,AMENDMENT, LOCALE("MS_409")] +class DSC_xWindowsFeature : OMI_BaseResource +{ + [Key, Description("The name of the role or feature to install or uninstall.") : Amended] String Name; + [Write, Description("Specifies whether the role or feature should be installed or uninstalled. To install the feature, set this property to Present. To uninstall the feature, set the property to Absent.") : Amended] String Ensure; + [Write, Description("Specifies whether the subfeatures of the main feature should also be installed.") : Amended] Boolean IncludeAllSubFeature; + [Write, Description("The path to the log file to log this operation.") : Amended] String LogPath; + [Write, Description("A credential, if needed, to install or uninstall the role or feature."), EmbeddedInstance("MSFT_Credential") : Amended] String Credential; + [Read, Description("The display name of the retrieved role or feature.") : Amended] String DisplayName; +}; + diff --git a/deployment/dsc/azshcihost/xPSDesiredStateConfiguration/9.1.0/DSCResources/DSC_xWindowsFeature/en-US/DSC_xWindowsFeature.strings.psd1 b/deployment/dsc/azshcihost/xPSDesiredStateConfiguration/9.1.0/DSCResources/DSC_xWindowsFeature/en-US/DSC_xWindowsFeature.strings.psd1 new file mode 100644 index 0000000..e6c2388 --- /dev/null +++ b/deployment/dsc/azshcihost/xPSDesiredStateConfiguration/9.1.0/DSCResources/DSC_xWindowsFeature/en-US/DSC_xWindowsFeature.strings.psd1 @@ -0,0 +1,23 @@ +# Localized strings for DSC_xWindowsFeature.psd1 + +ConvertFrom-StringData @' + FeatureNotFoundError = The requested feature {0} could not be found on the target machine. + MultipleFeatureInstancesError = Failure to get the requested feature {0} information from the target machine. Wildcard pattern is not supported in the feature name. + FeatureInstallationFailureError = Failure to successfully install the feature {0} . + FeatureUninstallationFailureError = Failure to successfully uninstall the feature {0} . + QueryFeature = Querying for feature {0} using Server Manager cmdlet Get-WindowsFeature. + InstallFeature = Trying to install feature {0} using Server Manager cmdlet Add-WindowsFeature. + UninstallFeature = Trying to uninstall feature {0} using Server Manager cmdlet Remove-WindowsFeature. + RestartNeeded = The Target machine needs to be restarted. + GetTargetResourceStartMessage = Begin executing Get functionality on the {0} feature. + GetTargetResourceEndMessage = End executing Get functionality on the {0} feature. + SetTargetResourceStartMessage = Begin executing Set functionality on the {0} feature. + SetTargetResourceEndMessage = End executing Set functionality on the {0} feature. + TestTargetResourceStartMessage = Begin executing Test functionality on the {0} feature. + TestTargetResourceEndMessage = End executing Test functionality on the {0} feature. + ServerManagerModuleNotFoundMessage = ServerManager module is not installed on the machine. + SkuNotSupported = Installing roles and features using PowerShell Desired State Configuration is supported only on Server SKU's. It is not supported on Client SKU. + EnableServerManagerPSHCmdletsFeature = Windows Server 2008R2 Core operating system detected: ServerManager-PSH-Cmdlets feature has been enabled. + UninstallSuccess = Successfully uninstalled the feature {0}. + InstallSuccess = Successfully installed the feature {0}. +'@ diff --git a/deployment/dsc/azshcihost/xPSDesiredStateConfiguration/9.1.0/DSCResources/DSC_xWindowsOptionalFeature/DSC_xWindowsOptionalFeature.psm1 b/deployment/dsc/azshcihost/xPSDesiredStateConfiguration/9.1.0/DSCResources/DSC_xWindowsOptionalFeature/DSC_xWindowsOptionalFeature.psm1 new file mode 100644 index 0000000..a3daeda --- /dev/null +++ b/deployment/dsc/azshcihost/xPSDesiredStateConfiguration/9.1.0/DSCResources/DSC_xWindowsOptionalFeature/DSC_xWindowsOptionalFeature.psm1 @@ -0,0 +1,417 @@ +$errorActionPreference = 'Stop' +Set-StrictMode -Version 'Latest' + +$modulePath = Join-Path -Path (Split-Path -Path (Split-Path -Path $PSScriptRoot -Parent) -Parent) -ChildPath 'Modules' + +# Import the shared modules +Import-Module -Name (Join-Path -Path $modulePath ` + -ChildPath (Join-Path -Path 'xPSDesiredStateConfiguration.Common' ` + -ChildPath 'xPSDesiredStateConfiguration.Common.psm1')) + +# Import Localization Strings +$script:localizedData = Get-LocalizedData -ResourceName 'DSC_xWindowsOptionalFeature' + +<# + .SYNOPSIS + Retrieves the state of a Windows optional feature resource. + + .PARAMETER Name + The name of the Windows optional feature resource to retrieve. +#> +function Get-TargetResource +{ + [CmdletBinding()] + [OutputType([System.Collections.Hashtable])] + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $Name + ) + + Write-Verbose -Message ($script:localizedData.GetTargetResourceStartMessage -f $Name) + + Assert-ResourcePrerequisitesValid + + $windowsOptionalFeature = Dism\Get-WindowsOptionalFeature -FeatureName $Name -Online + + <# + $windowsOptionalFeatureProperties and this section of code are needed because an error will be thrown if a property + is not found in WMF 4 instead of returning null. + #> + $windowsOptionalFeatureProperties = @{} + $propertiesNeeded = @( 'LogPath', 'State', 'CustomProperties', 'FeatureName', 'LogLevel', 'Description', 'DisplayName' ) + + foreach ($property in $propertiesNeeded) + { + try + { + $windowsOptionalFeatureProperties[$property] = $windowsOptionalFeature.$property + } + catch + { + $windowsOptionalFeatureProperties[$property] = $null + } + } + + $windowsOptionalFeatureResource = @{ + LogPath = $windowsOptionalFeatureProperties.LogPath + Ensure = Convert-FeatureStateToEnsure -State $windowsOptionalFeatureProperties.State + CustomProperties = Convert-CustomPropertyArrayToStringArray ` + -CustomProperties $windowsOptionalFeatureProperties.CustomProperties + Name = $windowsOptionalFeatureProperties.FeatureName + LogLevel = $windowsOptionalFeatureProperties.LogLevel + Description = $windowsOptionalFeatureProperties.Description + DisplayName = $windowsOptionalFeatureProperties.DisplayName + } + + Write-Verbose -Message ($script:localizedData.GetTargetResourceEndMessage -f $Name) + + return $windowsOptionalFeatureResource +} + +<# + .SYNOPSIS + Enables or disables a Windows optional feature + + .PARAMETER Name + The name of the feature to enable or disable. + + .PARAMETER Ensure + Specifies whether the feature should be enabled or disabled. + To enable the feature, set this property to Present. + To disable the feature, set the property to Absent. + + .PARAMETER RemoveFilesOnDisable + Specifies that all files associated with the feature should be removed if the feature is + being disabled. + + .PARAMETER NoWindowsUpdateCheck + Specifies whether or not DISM contacts Windows Update (WU) when searching for the source + files to enable the feature. + If $true, DISM will not contact WU. + + .PARAMETER LogPath + The path to the log file to log this operation. + There is no default value, but if not set, the log will appear at + %WINDIR%\Logs\Dism\dism.log. + + .PARAMETER LogLevel + The maximum output level to show in the log. + Accepted values are: "ErrorsOnly" (only errors are logged), "ErrorsAndWarning" (errors and + warnings are logged), and "ErrorsAndWarningAndInformation" (errors, warnings, and debug + information are logged). +#> +function Set-TargetResource +{ + [CmdletBinding(SupportsShouldProcess = $true)] + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $Name, + + [Parameter()] + [ValidateSet('Present', 'Absent')] + [System.String] + $Ensure = 'Present', + + [Parameter()] + [System.Boolean] + $RemoveFilesOnDisable, + + [Parameter()] + [System.Boolean] + $NoWindowsUpdateCheck, + + [Parameter()] + [System.String] + $LogPath, + + [Parameter()] + [ValidateSet('ErrorsOnly', 'ErrorsAndWarning', 'ErrorsAndWarningAndInformation')] + [System.String] + $LogLevel = 'ErrorsAndWarningAndInformation' + ) + + Write-Verbose -Message ($script:localizedData.SetTargetResourceStartMessage -f $Name) + + Assert-ResourcePrerequisitesValid + + $dismLogLevel = switch ($LogLevel) + { + 'ErrorsOnly' + { + 'Errors' + break + } + + 'ErrorsAndWarning' + { + 'Warnings' + break + } + + 'ErrorsAndWarningAndInformation' + { + 'WarningsInfo' + break + } + } + + # Construct splatting hashtable for DISM cmdlets + $dismCmdletParameters = @{ + FeatureName = $Name + Online = $true + LogLevel = $dismLogLevel + NoRestart = $true + } + + if ($PSBoundParameters.ContainsKey('LogPath')) + { + $dismCmdletParameters['LogPath'] = $LogPath + } + + if ($Ensure -eq 'Present') + { + if ($PSCmdlet.ShouldProcess($Name, $script:localizedData.ShouldProcessEnableFeature)) + { + if ($NoWindowsUpdateCheck) + { + $dismCmdletParameters['LimitAccess'] = $true + } + + $windowsOptionalFeature = Dism\Enable-WindowsOptionalFeature @dismCmdletParameters + } + + Write-Verbose -Message ($script:localizedData.FeatureInstalled -f $Name) + } + else + { + if ($PSCmdlet.ShouldProcess($Name, $script:localizedData.ShouldProcessDisableFeature)) + { + if ($RemoveFilesOnDisable) + { + $dismCmdletParameters['Remove'] = $true + } + + $windowsOptionalFeature = Dism\Disable-WindowsOptionalFeature @dismCmdletParameters + } + + Write-Verbose -Message ($script:localizedData.FeatureUninstalled -f $Name) + } + + <# + $restartNeeded and this section of code are needed because an error will be thrown if the + RestartNeeded property is not found in WMF 4. + #> + try + { + $restartNeeded = $windowsOptionalFeature.RestartNeeded + } + catch + { + $restartNeeded = $false + } + + # Indicate we need a restart if needed + if ($restartNeeded) + { + Write-Verbose -Message $script:localizedData.RestartNeeded + Set-DSCMachineRebootRequired + } + + Write-Verbose -Message ($script:localizedData.SetTargetResourceEndMessage -f $Name) +} + +<# + .SYNOPSIS + Tests if a Windows optional feature is in the specified state. + + .PARAMETER Name + The name of the feature to test the state of. + + .PARAMETER Ensure + Specifies whether the feature should be enabled or disabled. + To test if the feature is enabled, set this property to Present. + To test if the feature is disabled, set this property to Absent. + + .PARAMETER RemoveFilesOnDisable + Not used in Test-TargetResource. + + .PARAMETER NoWindowsUpdateCheck + Not used in Test-TargetResource. + + .PARAMETER LogPath + Not used in Test-TargetResource. + + .PARAMETER LogLevel + Not used in Test-TargetResource. +#> +function Test-TargetResource +{ + [CmdletBinding()] + [OutputType([System.Boolean])] + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $Name, + + [Parameter()] + [ValidateSet('Present', 'Absent')] + [System.String] + $Ensure = 'Present', + + [Parameter()] + [System.Boolean] + $RemoveFilesOnDisable, + + [Parameter()] + [System.Boolean] + $NoWindowsUpdateCheck, + + [Parameter()] + [System.String] + $LogPath, + + [Parameter()] + [ValidateSet('ErrorsOnly', 'ErrorsAndWarning', 'ErrorsAndWarningAndInformation')] + [System.String] + $LogLevel = 'ErrorsAndWarningAndInformation' + ) + + Write-Verbose -Message ($script:localizedData.TestTargetResourceStartMessage -f $Name) + + Assert-ResourcePrerequisitesValid + + $windowsOptionalFeature = Dism\Get-WindowsOptionalFeature -FeatureName $Name -Online + + $featureIsInDesiredState = $false + + if ($null -eq $windowsOptionalFeature -or $windowsOptionalFeature.State -eq 'Disabled') + { + $featureIsInDesiredState = $Ensure -eq 'Absent' + } + elseif ($windowsOptionalFeature.State -eq 'Enabled') + { + $featureIsInDesiredState = $Ensure -eq 'Present' + } + + Write-Verbose -Message ($script:localizedData.TestTargetResourceEndMessage -f $Name) + + return $featureIsInDesiredState +} + +<# + .SYNOPSIS + Converts a list of CustomProperty objects into an array of Strings. + + .PARAMETER CustomProperties + The list of CustomProperty objects to be converted. + Each CustomProperty object should have Name, Value, and Path properties. +#> +function Convert-CustomPropertyArrayToStringArray +{ + [CmdletBinding()] + [OutputType([System.String[]])] + param + ( + [Parameter()] + [System.Management.Automation.PSObject[]] + $CustomProperties + ) + + $propertiesAsStrings = [System.String[]] @() + + foreach ($customProperty in $CustomProperties) + { + if ($null -ne $customProperty) + { + $propertiesAsStrings += "Name = $($customProperty.Name), Value = $($customProperty.Value), Path = $($customProperty.Path)" + } + } + + return $propertiesAsStrings +} + +<# + .SYNOPSIS + Converts the string state returned by the DISM Get-WindowsOptionalFeature cmdlet to Present or Absent. + + .PARAMETER State + The state to be converted to either Present or Absent. + Should be either Enabled or Disabled. +#> +function Convert-FeatureStateToEnsure +{ + [CmdletBinding()] + [OutputType([System.String])] + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $State + ) + + if ($State -eq 'Disabled') + { + return 'Absent' + } + elseif ($State -eq 'Enabled') + { + return 'Present' + } + else + { + Write-Warning ($script:localizedData.CouldNotConvertFeatureState -f $State) + return $State + } +} + +<# + .SYNOPSIS + Throws errors if the prerequisites for using WindowsOptionalFeature are not met on the + target machine. + + Current prerequisites are: + - Must be running either a Windows client, at least Windows Server 2012, or Nano Server + - Must be running as an administrator + - The DISM PowerShell module must be available for import +#> +function Assert-ResourcePrerequisitesValid +{ + [CmdletBinding()] + param () + + Write-Verbose -Message $script:localizedData.ValidatingPrerequisites + + # Check that we're running on Server 2012 (or later) or on a client SKU + $operatingSystem = Get-CimInstance -ClassName 'Win32_OperatingSystem' + + if (($operatingSystem.ProductType -eq 2) -and ([System.Int32] $operatingSystem.BuildNumber -lt 9600)) + { + New-InvalidOperationException -Message $script:localizedData.NotSupportedSku + } + + # Check that we are running as an administrator + $windowsIdentity = [System.Security.Principal.WindowsIdentity]::GetCurrent() + $windowsPrincipal = New-Object -TypeName 'System.Security.Principal.WindowsPrincipal' -ArgumentList @( $windowsIdentity ) + + $adminRole = [System.Security.Principal.WindowsBuiltInRole]::Administrator + + if (-not $windowsPrincipal.IsInRole($adminRole)) + { + New-InvalidOperationException -Message $script:localizedData.ElevationRequired + } + + # Check that Dism PowerShell module is available + Import-Module -Name 'Dism' -ErrorVariable 'errorsFromDismImport' -ErrorAction 'SilentlyContinue' -Force -Verbose:$false + + if ($errorsFromDismImport.Count -gt 0) + { + New-InvalidOperationException -Message $script:localizedData.DismNotAvailable + } +} + +Export-ModuleMember -Function *-TargetResource diff --git a/deployment/dsc/azshcihost/xPSDesiredStateConfiguration/9.1.0/DSCResources/DSC_xWindowsOptionalFeature/DSC_xWindowsOptionalFeature.schema.mof b/deployment/dsc/azshcihost/xPSDesiredStateConfiguration/9.1.0/DSCResources/DSC_xWindowsOptionalFeature/DSC_xWindowsOptionalFeature.schema.mof new file mode 100644 index 0000000..982293a --- /dev/null +++ b/deployment/dsc/azshcihost/xPSDesiredStateConfiguration/9.1.0/DSCResources/DSC_xWindowsOptionalFeature/DSC_xWindowsOptionalFeature.schema.mof @@ -0,0 +1,14 @@ + +[ClassVersion("1.0.0.0"), FriendlyName("xWindowsOptionalFeature")] +class DSC_xWindowsOptionalFeature : OMI_BaseResource +{ + [Key, Description("The name of the feature to enable or disable.")] String Name; + [Write, Description("Specifies whether the feature should be enabled or disabled. To enable the feature, set this property to Present. To disable the feature, set the property to Absent."), ValueMap{"Present", "Absent"}, Values{"Present", "Absent"}] String Ensure; + [Write, Description("Specifies that all files associated with the feature should be removed if the feature is being disabled.")] Boolean RemoveFilesOnDisable; + [Write, Description("Specifies whether or not DISM contacts Windows Update (WU) when searching for the source files to enable the feature. If $true, DISM will not contact WU.")] Boolean NoWindowsUpdateCheck; + [Write, Description("The maximum output level to show in the log. Accepted values are: ErrorsOnly (only errors are logged), ErrorsAndWarning (errors and warnings are logged), and ErrorsAndWarningAndInformation (errors, warnings, and debug information are logged)."), ValueMap{"ErrorsOnly", "ErrorsAndWarning", "ErrorsAndWarningAndInformation"}, Values{"ErrorsOnly", "ErrorsAndWarning", "ErrorsAndWarningAndInformation"}] String LogLevel; + [Write, Description("The path to the log file to log this operation.")] String LogPath; + [Read, Description("The custom properties retrieved from the Windows optional feature as an array of strings.")] String CustomProperties[]; + [Read, Description("The description retrieved from the Windows optional feature.")] String Description; + [Read, Description("The display name retrieved from the Windows optional feature.")] String DisplayName; +}; diff --git a/deployment/dsc/azshcihost/xPSDesiredStateConfiguration/9.1.0/DSCResources/DSC_xWindowsOptionalFeature/en-US/DSC_xWindowsOptionalFeature.schema.mfl b/deployment/dsc/azshcihost/xPSDesiredStateConfiguration/9.1.0/DSCResources/DSC_xWindowsOptionalFeature/en-US/DSC_xWindowsOptionalFeature.schema.mfl new file mode 100644 index 0000000..d3e51ad --- /dev/null +++ b/deployment/dsc/azshcihost/xPSDesiredStateConfiguration/9.1.0/DSCResources/DSC_xWindowsOptionalFeature/en-US/DSC_xWindowsOptionalFeature.schema.mfl @@ -0,0 +1,13 @@ +[Description("This resource is used to enable and disable Windows optional features.") : Amended,AMENDMENT, LOCALE("MS_409")] +class DSC_xWindowsOptionalFeature : OMI_BaseResource +{ + [Key, Description("The name of the feature to enable or disable.") : Amended] String Name; + [Description("Specifies whether the feature should be enabled or disabled. To enable the feature, set this property to Present. To disable the feature, set the property to Absent.") : Amended] String Ensure; + [Description("Specifies that all files associated with the feature should be removed if the feature is being disabled.") : Amended] Boolean RemoveFilesOnDisable; + [Description("Specifies whether or not DISM contacts Windows Update (WU) when searching for the source files to enable the feature. If $true, DISM will not contact WU.") : Amended] Boolean NoWindowsUpdateCheck; + [Description("The maximum output level to show in the log. Accepted values are: ErrorsOnly (only errors are logged), ErrorsAndWarning (errors and warnings are logged), and ErrorsAndWarningAndInformation (errors, warnings, and debug information are logged).") : Amended] String LogLevel; + [Description("The path to the log file to log this operation.") : Amended] String LogPath; + [Description("The custom properties retrieved from the Windows optional feature as an array of strings.") : Amended] String CustomProperties[]; + [Description("The description retrieved from the Windows optional feature.") : Amended] String Description; + [Description("The display name retrieved from the Windows optional feature.") : Amended] String DisplayName; +}; diff --git a/deployment/dsc/azshcihost/xPSDesiredStateConfiguration/9.1.0/DSCResources/DSC_xWindowsOptionalFeature/en-US/DSC_xWindowsOptionalFeature.strings.psd1 b/deployment/dsc/azshcihost/xPSDesiredStateConfiguration/9.1.0/DSCResources/DSC_xWindowsOptionalFeature/en-US/DSC_xWindowsOptionalFeature.strings.psd1 new file mode 100644 index 0000000..b29de25 --- /dev/null +++ b/deployment/dsc/azshcihost/xPSDesiredStateConfiguration/9.1.0/DSCResources/DSC_xWindowsOptionalFeature/en-US/DSC_xWindowsOptionalFeature.strings.psd1 @@ -0,0 +1,20 @@ +# Localized resources for DSC_xWindowsOptionalFeature + +ConvertFrom-StringData @' + DismNotAvailable = PowerShell module DISM could not be imported. + NotSupportedSku = This resource is available only on Windows client operating systems and Windows Server 2012 or later. + ElevationRequired = This resource must run as an Administrator. + ValidatingPrerequisites = Validating resource prerequisites. + CouldNotConvertFeatureState = Could not convert feature state '{0}' into Absent or Present. + RestartNeeded = Target machine needs to restart. + GetTargetResourceStartMessage = Started Get-TargetResource on the '{0}' feature. + GetTargetResourceEndMessage = Finished Get-TargetResource on the '{0}' feature. + SetTargetResourceStartMessage = Started Set-TargetResource on the '{0}' feature. + SetTargetResourceEndMessage = Finished Set-TargetResource on the '{0}' feature. + TestTargetResourceStartMessage = Started Test-TargetResource on the '{0}' feature. + TestTargetResourceEndMessage = Finished Test-TargetResource on the '{0}' feature. + FeatureInstalled = Installed feature '{0}'. + FeatureUninstalled = Uninstalled feature '{0}'. + ShouldProcessEnableFeature = Enable Windows optional feature. + ShouldProcessDisableFeature = Disable Windows optional feature. +'@ diff --git a/deployment/dsc/azshcihost/xPSDesiredStateConfiguration/9.1.0/DSCResources/DSC_xWindowsPackageCab/DSC_xWindowsPackageCab.psm1 b/deployment/dsc/azshcihost/xPSDesiredStateConfiguration/9.1.0/DSCResources/DSC_xWindowsPackageCab/DSC_xWindowsPackageCab.psm1 new file mode 100644 index 0000000..a301b67 --- /dev/null +++ b/deployment/dsc/azshcihost/xPSDesiredStateConfiguration/9.1.0/DSCResources/DSC_xWindowsPackageCab/DSC_xWindowsPackageCab.psm1 @@ -0,0 +1,238 @@ +$errorActionPreference = 'Stop' +Set-StrictMode -Version 'Latest' + +$modulePath = Join-Path -Path (Split-Path -Path (Split-Path -Path $PSScriptRoot -Parent) -Parent) -ChildPath 'Modules' + +# Import the shared modules +Import-Module -Name (Join-Path -Path $modulePath ` + -ChildPath (Join-Path -Path 'xPSDesiredStateConfiguration.Common' ` + -ChildPath 'xPSDesiredStateConfiguration.Common.psm1')) + +# Import Localization Strings +$script:localizedData = Get-LocalizedData -ResourceName 'DSC_xWindowsPackageCab' + +Import-Module -Name 'Dism' + +<# + .SYNOPSIS + Retrieves the current state of a package from a windows cabinet (cab) file. + + .PARAMETER Name + The name of the package to retrieve the state of. + + .PARAMETER Ensure + Not used in Get-TargetResource. + Provided here to follow DSC design convention of including all mandatory parameters + in Get, Set, and Test. + + .PARAMETER SourcePath + The path to the cab file the package should be installed or uninstalled from. + Returned from Get-TargetResource as it is passed in. + + .PARAMETER LogPath + The path to a file to log this operation to. + There is no default value, but if not set, the log will appear at %WINDIR%\Logs\Dism\dism.log. +#> +function Get-TargetResource +{ + [CmdletBinding()] + [OutputType([System.Collections.Hashtable])] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $Name, + + [Parameter(Mandatory = $true)] + [ValidateSet('Present', 'Absent')] + [System.String] + $Ensure, + + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $SourcePath, + + [Parameter()] + [ValidateNotNullOrEmpty()] + [System.String] + $LogPath + ) + + $windowsPackageCab = @{ + Name = $Name + Ensure = 'Present' + SourcePath = $SourcePath + LogPath = $LogPath + } + + $getWindowsPackageParams = @{ + PackageName = $Name + Online = $true + } + + if ($PSBoundParameters.ContainsKey('LogPath')) + { + $getWindowsPackageParams['LogPath'] = $LogPath + } + + Write-Verbose -Message ($script:localizedData.RetrievingPackage -f $Name) + + try + { + $windowsPackageInfo = Dism\Get-WindowsPackage @getWindowsPackageParams + } + catch + { + $windowsPackageInfo = $null + } + + if ($null -eq $windowsPackageInfo -or -not ($windowsPackageInfo.PackageState -in @( 'Installed', 'InstallPending' ))) + { + $windowsPackageCab.Ensure = 'Absent' + } + + Write-Verbose -Message ($script:localizedData.PackageEnsureState -f $Name, $windowsPackageCab.Ensure) + + return $windowsPackageCab +} + +<# + .SYNOPSIS + Installs or uninstalls a package from a windows cabinet (cab) file. + + .PARAMETER Name + The name of the package to install or uninstall. + + .PARAMETER Ensure + Specifies whether the package should be installed or uninstalled. + To install the package, set this property to Present. + To uninstall the package, set the property to Absent. + + .PARAMETER SourcePath + The path to the cab file to install or uninstall the package from. + + .PARAMETER LogPath + The path to a file to log this operation to. + There is no default value, but if not set, the log will appear at %WINDIR%\Logs\Dism\dism.log. +#> +function Set-TargetResource +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $Name, + + [Parameter(Mandatory = $true)] + [ValidateSet('Present', 'Absent')] + [System.String] + $Ensure, + + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $SourcePath, + + [Parameter()] + [ValidateNotNullOrEmpty()] + [System.String] + $LogPath + ) + + Write-Verbose -Message ($script:localizedData.SetTargetResourceStarting -f $Name) + + if (-not (Test-Path -Path $SourcePath)) + { + New-InvalidArgumentException -ArgumentName 'SourcePath' -Message ($script:localizedData.SourcePathDoesNotExist -f $SourcePath) + } + + if ($Ensure -ieq 'Present') + { + Write-Verbose -Message ($script:localizedData.AddingPackage -f $SourcePath) + Dism\Add-WindowsPackage -PackagePath $SourcePath -LogPath $LogPath -Online + } + else + { + Write-Verbose -Message ($script:localizedData.RemovingPackage -f $SourcePath) + Dism\Remove-WindowsPackage -PackagePath $SourcePath -LogPath $LogPath -Online + } + + Write-Verbose -Message ($script:localizedData.SetTargetResourceFinished -f $Name) +} + +<# + .SYNOPSIS + Tests whether a package in a windows cabinet (cab) file is installed or uninstalled. + + .PARAMETER Name + The name of the cab package to test for installation. + + .PARAMETER Ensure + Specifies whether to test if the package is installed or uninstalled. + To test if the package is installed, set this property to Present. + To test if the package is uninstalled, set the property to Absent. + + .PARAMETER SourcePath + Not used in Test-TargetResource. + + .PARAMETER LogPath + The path to a file to log this operation to. + There is no default value, but if not set, the log will appear at %WINDIR%\Logs\Dism\dism.log. +#> +function Test-TargetResource +{ + [CmdletBinding()] + [OutputType([System.Boolean])] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $Name, + + [Parameter(Mandatory = $true)] + [ValidateSet('Present', 'Absent')] + [System.String] + $Ensure, + + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $SourcePath, + + [Parameter()] + [ValidateNotNullOrEmpty()] + [System.String] + $LogPath + ) + + $getTargetResourceParams = @{ + Name = $Name + Ensure = $Ensure + SourcePath = $SourcePath + } + + if ($PSBoundParameters.ContainsKey('LogPath')) + { + $getTargetResourceParams['LogPath'] = $LogPath + } + + $windowsPackageCab = Get-TargetResource @getTargetResourceParams + + if ($windowsPackageCab.Ensure -ieq $Ensure) + { + Write-Verbose -Message ($script:localizedData.EnsureStatesMatch -f $Name) + return $true + } + else + { + Write-Verbose -Message ($script:localizedData.EnsureStatesDoNotMatch -f $Name) + return $false + } +} + +Export-ModuleMember -Function '*-TargetResource' diff --git a/deployment/dsc/azshcihost/xPSDesiredStateConfiguration/9.1.0/DSCResources/DSC_xWindowsPackageCab/DSC_xWindowsPackageCab.schema.mof b/deployment/dsc/azshcihost/xPSDesiredStateConfiguration/9.1.0/DSCResources/DSC_xWindowsPackageCab/DSC_xWindowsPackageCab.schema.mof new file mode 100644 index 0000000..ee5951f --- /dev/null +++ b/deployment/dsc/azshcihost/xPSDesiredStateConfiguration/9.1.0/DSCResources/DSC_xWindowsPackageCab/DSC_xWindowsPackageCab.schema.mof @@ -0,0 +1,9 @@ + +[ClassVersion("1.0.0.0"), FriendlyName("xWindowsPackageCab")] +class DSC_xWindowsPackageCab : OMI_BaseResource +{ + [Key, Description("The name of the package to install or uninstall.")] String Name; + [Required, Description("Specifies whether the package should be installed or uninstalled. To install the package, set this property to Present. To uninstall the package, set the property to Absent."), ValueMap{"Present", "Absent"}, Values{"Present", "Absent"}] String Ensure; + [Required, Description("The path to the cab file to install or uninstall the package from.")] String SourcePath; + [Write, Description("The path to a file to log the operation to.")] String LogPath; +}; diff --git a/deployment/dsc/azshcihost/xPSDesiredStateConfiguration/9.1.0/DSCResources/DSC_xWindowsPackageCab/en-US/DSC_xWindowsPackageCab.schema.mfl b/deployment/dsc/azshcihost/xPSDesiredStateConfiguration/9.1.0/DSCResources/DSC_xWindowsPackageCab/en-US/DSC_xWindowsPackageCab.schema.mfl new file mode 100644 index 0000000..db43ded --- /dev/null +++ b/deployment/dsc/azshcihost/xPSDesiredStateConfiguration/9.1.0/DSCResources/DSC_xWindowsPackageCab/en-US/DSC_xWindowsPackageCab.schema.mfl @@ -0,0 +1,8 @@ +[Description("This resource is used to install or uninstall a package from a windows cabinet (cab) file.") : Amended,AMENDMENT, LOCALE("MS_409")] +class DSC_xWindowsPackageCab : OMI_BaseResource +{ + [Key, Description("The name of the package to install or uninstall.") : Amended] String Name; + [Description("Specifies whether the package should be installed or uninstalled. To install the package, set this property to Present. To uninstall the package, set the property to Absent.") : Amended] String Ensure; + [Description("The path to the cab file to install or uninstall the package from.") : Amended] String SourcePath; + [Description("The path to a file to log the operation to.") : Amended] String LogPath; +}; diff --git a/deployment/dsc/azshcihost/xPSDesiredStateConfiguration/9.1.0/DSCResources/DSC_xWindowsPackageCab/en-US/DSC_xWindowsPackageCab.strings.psd1 b/deployment/dsc/azshcihost/xPSDesiredStateConfiguration/9.1.0/DSCResources/DSC_xWindowsPackageCab/en-US/DSC_xWindowsPackageCab.strings.psd1 new file mode 100644 index 0000000..998c1c6 --- /dev/null +++ b/deployment/dsc/azshcihost/xPSDesiredStateConfiguration/9.1.0/DSCResources/DSC_xWindowsPackageCab/en-US/DSC_xWindowsPackageCab.strings.psd1 @@ -0,0 +1,13 @@ +# Localized resources for DSC_xWindowsPackageCab + +ConvertFrom-StringData @' + RetrievingPackage = Retrieving information for the package {0} + PackageEnsureState = The package {0} is currently {1} + SourcePathDoesNotExist = Could not find the source file at path {0} + SetTargetResourceStarting = Starting configuration of the WindowsPackageCab resource {0} + SetTargetResourceFinished = Finished configuration of WindowsPackageCab resource {0} + AddingPackage = Adding a package from the source at path {0} + RemovingPackage = Removing package from the source at path {0} + EnsureStatesMatch = Ensure states match for package {0} + EnsureStatesDoNotMatch = Ensure states do not match for package {0} +'@ diff --git a/deployment/dsc/azshcihost/xPSDesiredStateConfiguration/9.1.0/DSCResources/DSC_xWindowsProcess/DSC_xWindowsProcess.psm1 b/deployment/dsc/azshcihost/xPSDesiredStateConfiguration/9.1.0/DSCResources/DSC_xWindowsProcess/DSC_xWindowsProcess.psm1 new file mode 100644 index 0000000..b9d2459 --- /dev/null +++ b/deployment/dsc/azshcihost/xPSDesiredStateConfiguration/9.1.0/DSCResources/DSC_xWindowsProcess/DSC_xWindowsProcess.psm1 @@ -0,0 +1,1565 @@ +$errorActionPreference = 'Stop' +Set-StrictMode -Version 'Latest' + +$modulePath = Join-Path -Path (Split-Path -Path (Split-Path -Path $PSScriptRoot -Parent) -Parent) -ChildPath 'Modules' + +# Import the shared modules +Import-Module -Name (Join-Path -Path $modulePath ` + -ChildPath (Join-Path -Path 'xPSDesiredStateConfiguration.Common' ` + -ChildPath 'xPSDesiredStateConfiguration.Common.psm1')) + +# Import Localization Strings +$script:localizedData = Get-LocalizedData -ResourceName 'DSC_xWindowsProcess' + +<# + .SYNOPSIS + Retrieves the current state of the Windows process(es) with the specified + executable and arguments. + + If more than one process is found, only the information of the first process is retrieved. + ProcessCount will contain the actual number of processes that were found. + + .PARAMETER Path + The path to the process executable. If this is the file name of the executable + (not the fully qualified path), the DSC resource will search the environment Path variable + ($env:Path) to find the executable file. If the value of this property is a fully qualified + path, DSC will use the given Path variable to find the file. If the path is not found it + will throw an error. Relative paths are not allowed. + + .PARAMETER Arguments + The arguments to the process as a single string. + + .PARAMETER Credential + The credential of the user account to start the process under. +#> +function Get-TargetResource +{ + [OutputType([System.Collections.Hashtable])] + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $Path, + + [Parameter(Mandatory = $true)] + [AllowEmptyString()] + [System.String] + $Arguments, + + [Parameter()] + [ValidateNotNullOrEmpty()] + [System.Management.Automation.PSCredential] + [System.Management.Automation.Credential()] + $Credential + ) + + Write-Verbose -Message ($script:localizedData.GetTargetResourceStartMessage -f $Path) + + $Path = Expand-Path -Path $Path + + $getProcessCimInstanceArguments = @{ + Path = $Path + Arguments = $Arguments + } + + if ($PSBoundParameters.ContainsKey('Credential')) + { + $getProcessCimInstanceArguments['Credential'] = $Credential + } + + $processCimInstance = @( Get-ProcessCimInstance @getProcessCimInstanceArguments ) + + $processToReturn = @{} + + if ($processCimInstance.Count -eq 0) + { + $processToReturn = @{ + Path = $Path + Arguments = $Arguments + Ensure ='Absent' + } + } + else + { + $processId = $processCimInstance[0].ProcessId + $getProcessResult = Get-Process -ID $processId + + $processToReturn = @{ + Path = $Path + Arguments = $Arguments + PagedMemorySize = $getProcessResult.PagedMemorySize64 + NonPagedMemorySize = $getProcessResult.NonpagedSystemMemorySize64 + VirtualMemorySize = $getProcessResult.VirtualMemorySize64 + HandleCount = $getProcessResult.HandleCount + Ensure = 'Present' + ProcessId = $processId + ProcessCount = $processCimInstance.Count + } + } + + Write-Verbose -Message ($script:localizedData.GetTargetResourceEndMessage -f $Path) + + return $processToReturn +} + +<# + .SYNOPSIS + Sets the Windows process with the specified executable path and arguments + to the specified state. + + If multiple process are found, the specified state will be set for all of them. + + .PARAMETER Path + The path to the process executable. If this is the file name of the executable + (not the fully qualified path), the DSC resource will search the environment Path variable + ($env:Path) to find the executable file. If the value of this property is a fully qualified + path, DSC will use the given Path variable to find the file. If the path is not found it + will throw an error. Relative paths are not allowed. + + .PARAMETER Arguments + The arguments to pass to the process as a single string. + + .PARAMETER Credential + The credential of the user account to start the process under. + + .PARAMETER Ensure + Specifies whether or not the process should exist. + To start or modify a process, set this property to Present. + To stop a process, set this property to Absent. + The default value is Present. + + .PARAMETER StandardOutputPath + The file path to write the standard output to. Any existing file at this path + will be overwritten.This property cannot be specified at the same time as Credential + when running the process as a local user. + + .PARAMETER StandardErrorPath + The file path to write the standard error output to. Any existing file at this path + will be overwritten. + + .PARAMETER StandardInputPath + The file path to get standard input from. This property cannot be specified at the + same time as Credential when running the process as a local user. + + .PARAMETER WorkingDirectory + The file path to use as the working directory for the process. Any existing file + at this path will be overwritten. This property cannot be specified at the same time + as Credential when running the process as a local user. +#> +function Set-TargetResource +{ + [CmdletBinding(SupportsShouldProcess = $true)] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $Path, + + [Parameter(Mandatory = $true)] + [AllowEmptyString()] + [System.String] + $Arguments, + + [Parameter()] + [ValidateNotNullOrEmpty()] + [System.Management.Automation.PSCredential] + [System.Management.Automation.Credential()] + $Credential, + + [Parameter()] + [ValidateSet('Present', 'Absent')] + [System.String] + $Ensure = 'Present', + + [Parameter()] + [System.String] + $StandardOutputPath, + + [Parameter()] + [System.String] + $StandardErrorPath, + + [Parameter()] + [System.String] + $StandardInputPath, + + [Parameter()] + [System.String] + $WorkingDirectory + ) + + Write-Verbose -Message ($script:localizedData.SetTargetResourceStartMessage -f $Path) + + Assert-PsDscContextNotRunAsUser + + $Path = Expand-Path -Path $Path + + $getProcessCimInstanceArguments = @{ + Path = $Path + Arguments = $Arguments + } + + if ($PSBoundParameters.ContainsKey('Credential')) + { + $getProcessCimInstanceArguments['Credential'] = $Credential + } + + $processCimInstance = @( Get-ProcessCimInstance @getProcessCimInstanceArguments ) + + if ($Ensure -eq 'Absent') + { + $assertHashtableParams = @{ + Hashtable = $PSBoundParameters + Key = @( 'StandardOutputPath', + 'StandardErrorPath', + 'StandardInputPath', + 'WorkingDirectory' ) + } + Assert-HashtableDoesNotContainKey @assertHashtableParams + + $whatIfShouldProcess = $PSCmdlet.ShouldProcess($Path, $script:localizedData.StoppingProcessWhatif) + + if ($processCimInstance.Count -gt 0 -and $whatIfShouldProcess) + { + # If there are multiple process Ids, all will be included to be stopped + $processIds = $processCimInstance.ProcessId + + # Redirecting error output to standard output while we try to stop the processes + $stopProcessError = Stop-Process -Id $processIds -Force 2>&1 + + if ($null -eq $stopProcessError) + { + Write-Verbose -Message ($script:localizedData.ProcessesStopped -f $Path, ($processIds -join ',')) + } + else + { + $errorMessage = ($script:localizedData.ErrorStopping -f $Path, + ($processIds -join ','), + ($stopProcessError | Out-String)) + + New-InvalidOperationException -Message $errorMessage + } + + <# + Before returning from Set-TargetResource we have to ensure a subsequent + Test-TargetResource is going to work + #> + if (-not (Wait-ProcessCount -ProcessSettings $getProcessCimInstanceArguments -ProcessCount 0)) + { + $message = $script:localizedData.ErrorStopping -f $Path, ($processIds -join ','), + $script:localizedData.FailureWaitingForProcessesToStop + + New-InvalidOperationException -Message $message + } + } + else + { + Write-Verbose -Message ($script:localizedData.ProcessAlreadyStopped -f $Path) + } + } + # Ensure = 'Present' + else + { + $shouldBeRootedPathArguments = @( 'StandardInputPath', + 'WorkingDirectory', + 'StandardOutputPath', + 'StandardErrorPath' ) + + foreach ($shouldBeRootedPathArgument in $shouldBeRootedPathArguments) + { + if (-not [System.String]::IsNullOrEmpty($PSBoundParameters[$shouldBeRootedPathArgument])) + { + $assertPathArgumentRootedParams = @{ + PathArgumentName = $shouldBeRootedPathArgument + PathArgument = $PSBoundParameters[$shouldBeRootedPathArgument] + } + Assert-PathArgumentRooted @assertPathArgumentRootedParams + } + } + + $shouldExistPathArguments = @( 'StandardInputPath', 'WorkingDirectory' ) + + foreach ($shouldExistPathArgument in $shouldExistPathArguments) + { + if (-not [System.String]::IsNullOrEmpty($PSBoundParameters[$shouldExistPathArgument])) + { + $assertPathArgumentValidParams = @{ + PathArgumentName = $shouldExistPathArgument + PathArgument = $PSBoundParameters[$shouldExistPathArgument] + } + Assert-PathArgumentValid @assertPathArgumentValidParams + } + } + + if ($processCimInstance.Count -eq 0) + { + $startProcessArguments = @{ + FilePath = $Path + } + + $startProcessOptionalArgumentMap = @{ + Credential = 'Credential' + RedirectStandardOutput = 'StandardOutputPath' + RedirectStandardError = 'StandardErrorPath' + RedirectStandardInput = 'StandardInputPath' + WorkingDirectory = 'WorkingDirectory' + } + + foreach ($startProcessOptionalArgumentName in $startProcessOptionalArgumentMap.Keys) + { + $parameterKey = $startProcessOptionalArgumentMap[$startProcessOptionalArgumentName] + $parameterValue = $PSBoundParameters[$parameterKey] + + if (-not [System.String]::IsNullOrEmpty($parameterValue)) + { + $startProcessArguments[$startProcessOptionalArgumentName] = $parameterValue + } + } + + if (-not [System.String]::IsNullOrEmpty($Arguments) -and ` + -not ($Arguments.Trim().Length -eq 0)) + { + $startProcessArguments['ArgumentList'] = Add-SurroundingDoubleQuotesToString -StringIn $Arguments + } + + if ($PSCmdlet.ShouldProcess($Path, $script:localizedData.StartingProcessWhatif)) + { + <# + Start-Process calls .net Process.Start() + If -Credential is present Process.Start() uses win32 api CreateProcessWithLogonW + http://msdn.microsoft.com/en-us/library/0w4h05yb(v=vs.110).aspx + CreateProcessWithLogonW cannot be called as LocalSystem user. + Details http://msdn.microsoft.com/en-us/library/windows/desktop/ms682431(v=vs.85).aspx + (section Remarks/Windows XP with SP2 and Windows Server 2003) + + In this case we call another api. + #> + if (($PSBoundParameters.ContainsKey('Credential')) -and (Test-IsRunFromLocalSystemUser)) + { + # Throw an exception if any of the below parameters are included with Credential passed + foreach ($key in @('StandardOutputPath', 'StandardInputPath', 'WorkingDirectory')) + { + if ($PSBoundParameters.Keys -contains $key) + { + $newInvalidArgumentExceptionParams = @{ + ArgumentName = $key + Message = $script:localizedData.ErrorParametersNotSupportedWithCredential + } + New-InvalidArgumentException @newInvalidArgumentExceptionParams + } + } + + try + { + Start-ProcessAsLocalSystemUser -Path $Path -Arguments $Arguments -Credential $Credential + } + catch + { + throw (New-Object -TypeName 'System.Management.Automation.ErrorRecord' ` + -ArgumentList @( $_.Exception, 'Win32Exception', 'OperationStopped', $null )) + } + } + # Credential not passed in or running from a LocalSystem + else + { + try + { + Start-Process @startProcessArguments + } + catch [System.Exception] + { + $errorMessage = ($script:localizedData.ErrorStarting -f $Path, $_.Exception.Message) + + New-InvalidOperationException -Message $errorMessage + } + } + + Write-Verbose -Message ($script:localizedData.ProcessesStarted -f $Path) + + # Before returning from Set-TargetResource we have to ensure a subsequent Test-TargetResource is going to work + if (-not (Wait-ProcessCount -ProcessSettings $getProcessCimInstanceArguments -ProcessCount 1)) + { + $message = $script:localizedData.ErrorStarting -f $Path, + $script:localizedData.FailureWaitingForProcessesToStart + + New-InvalidOperationException -Message $message + } + } + } + else + { + Write-Verbose -Message ($script:localizedData.ProcessAlreadyStarted -f $Path) + } + } + + Write-Verbose -Message ($script:localizedData.SetTargetResourceEndMessage -f $Path) +} + +<# + .SYNOPSIS + Tests if the Windows process with the specified executable path and arguments is in + the specified state. + + .PARAMETER Path + The path to the process executable. If this is the file name of the executable + (not the fully qualified path), the DSC resource will search the environment Path variable + ($env:Path) to find the executable file. If the value of this property is a fully qualified + path, DSC will use the given Path variable to find the file. If the path is not found it + will throw an error. Relative paths are not allowed. + + .PARAMETER Arguments + The arguments to pass to the process as a single string. + + .PARAMETER Credential + The credential of the user account the process should be running under. + + .PARAMETER Ensure + Specifies whether or not the process should exist. + If the process should exist, set this property to Present. + If the process should not exist, set this property to Absent. + The default value is Present. + + .PARAMETER StandardOutputPath + Not used in Test-TargetResource. + + .PARAMETER StandardErrorPath + Not used in Test-TargetResource. + + .PARAMETER StandardInputPath + Not used in Test-TargetResource. + + .PARAMETER WorkingDirectory + Not used in Test-TargetResource. +#> +function Test-TargetResource +{ + [OutputType([System.Boolean])] + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $Path, + + [Parameter(Mandatory = $true)] + [AllowEmptyString()] + [System.String] + $Arguments, + + [Parameter()] + [ValidateNotNullOrEmpty()] + [System.Management.Automation.PSCredential] + [System.Management.Automation.Credential()] + $Credential, + + [Parameter()] + [ValidateSet('Present', 'Absent')] + [System.String] + $Ensure = 'Present', + + [Parameter()] + [System.String] + $StandardOutputPath, + + [Parameter()] + [System.String] + $StandardErrorPath, + + [Parameter()] + [System.String] + $StandardInputPath, + + [Parameter()] + [System.String] + $WorkingDirectory + ) + + Write-Verbose -Message ($script:localizedData.TestTargetResourceStartMessage -f $Path) + + Assert-PsDscContextNotRunAsUser + + $Path = Expand-Path -Path $Path + + $getProcessCimInstanceArguments = @{ + Path = $Path + Arguments = $Arguments + } + + if ($PSBoundParameters.ContainsKey('Credential')) + { + $getProcessCimInstanceArguments['Credential'] = $Credential + } + + $processCimInstances = @( Get-ProcessCimInstance @getProcessCimInstanceArguments ) + + Write-Verbose -Message ($script:localizedData.TestTargetResourceEndMessage -f $Path) + + if ($Ensure -eq 'Absent') + { + return ($processCimInstances.Count -eq 0) + } + else + { + return ($processCimInstances.Count -gt 0) + } +} + +<# + .SYNOPSIS + Expands a relative leaf path into a full, rooted path. Throws an invalid argument exception + if the path is not valid. + + .PARAMETER Path + The relative leaf path to expand. +#> +function Expand-Path +{ + [OutputType([System.String])] + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $Path + ) + + $Path = [Environment]::ExpandEnvironmentVariables($Path) + + # Check to see if the path is rooted. If so, return it as is. + if ([IO.Path]::IsPathRooted($Path)) + { + if (-not (Test-Path -Path $Path -PathType 'Leaf')) + { + New-InvalidArgumentException -ArgumentName 'Path' -Message ($script:localizedData.FileNotFound -f $Path) + } + + return $Path + } + + # Check to see if the path to the file exists in the current location. If so, return the full rooted path. + $rootedPath = [System.IO.Path]::GetFullPath($Path) + + if ([System.IO.File]::Exists($rootedPath)) + { + return $rootedPath + } + + # If the path is not found, throw an exception + New-InvalidArgumentException -ArgumentName 'Path' -Message ($script:localizedData.FileNotFound -f $Path) +} + +<# + .SYNOPSIS + Retrieves any process CIM instance objects that match the given path, arguments, and credential. + + .PARAMETER Path + The executable path of the process to retrieve. + + .PARAMETER Arguments + The arguments of the process to retrieve as a single string. + + .PARAMETER Credential + The credential of the user account of the process to retrieve + + .PARAMETER UseGetCimInstanceThreshold + If the number of processes returned by the Get-Process method is greater than or equal to + this value, this function will retrieve all processes at the executable path. This will + help the function execute faster. Otherwise, this function will retrieve each process + CIM instance with the process IDs retrieved from Get-Process. +#> +function Get-ProcessCimInstance +{ + [OutputType([Microsoft.Management.Infrastructure.CimInstance[]])] + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $Path, + + [Parameter()] + [System.String] + $Arguments, + + [Parameter()] + [ValidateNotNullOrEmpty()] + [System.Management.Automation.PSCredential] + [System.Management.Automation.Credential()] + $Credential, + + [Parameter()] + [ValidateRange(0, [System.Int32]::MaxValue)] + [System.Int32] + $UseGetCimInstanceThreshold = 8 + ) + + $processName = [IO.Path]::GetFileNameWithoutExtension($Path) + + $getProcessResult = @( Get-Process -Name $processName -ErrorAction 'SilentlyContinue' ) + + $processCimInstances = @() + + if ($getProcessResult.Count -ge $UseGetCimInstanceThreshold) + { + $escapedPathForWqlFilter = ConvertTo-EscapedStringForWqlFilter -FilterString $Path + $wqlFilter = "ExecutablePath = '$escapedPathForWqlFilter'" + + $processCimInstances = Get-CimInstance -ClassName 'Win32_Process' -Filter $wqlFilter + } + else + { + foreach ($process in $getProcessResult) + { + if ($process.Path -ieq $Path) + { + Write-Verbose -Message ($script:localizedData.VerboseInProcessHandle -f $process.Id) + $getCimInstanceParams = @{ + ClassName = 'Win32_Process' + Filter = "ProcessId = $($process.Id)" + ErrorAction = 'SilentlyContinue' + } + $processCimInstances += Get-CimInstance @getCimInstanceParams + } + } + } + + if ($PSBoundParameters.ContainsKey('Credential')) + { + $splitCredentialResult = Split-Credential -Credential $Credential + $domain = $splitCredentialResult.Domain + $userName = $splitCredentialResult.UserName + $processesWithCredential = @() + + foreach ($process in $processCimInstances) + { + if ((Get-ProcessOwner -Process $process) -eq "$domain\$userName") + { + $processesWithCredential += $process + } + } + $processCimInstances = $processesWithCredential + } + + if ($null -eq $Arguments) + { + $Arguments = [System.String]::Empty + } + + $processesWithMatchingArguments = @() + + foreach ($process in $processCimInstances) + { + $commandLineArgs = Get-ArgumentsFromCommandLineInput -CommandLineInput $process.CommandLine + + if ([String]::IsNullOrEmpty($Arguments) -or ` + $commandLineArgs -eq $Arguments -or ` + $commandLineArgs -eq (Add-SurroundingDoubleQuotesToString -StringIn $Arguments)) + { + $processesWithMatchingArguments += $process + } + } + + return $processesWithMatchingArguments +} + +<# + .SYNOPSIS + Converts a string to an escaped string to be used in a WQL filter such as the one passed in + the Filter parameter of Get-WmiObject. + + .PARAMETER FilterString + The string to convert. +#> +function ConvertTo-EscapedStringForWqlFilter +{ + [OutputType([System.String])] + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $FilterString + ) + + return $FilterString.Replace("\", "\\").Replace('"', '\"').Replace("'", "\'") +} + +<# + .SYNOPSIS + Retrieves the owner of a Process. + + .PARAMETER Process + The Process to retrieve the owner of. + + .NOTES + If the process was killed by the time this function is called, this function will throw a + WMIMethodException with the message "Not found". +#> +function Get-ProcessOwner +{ + [OutputType([System.String])] + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNull()] + [System.Object] + $Process + ) + + $owner = Get-ProcessOwnerCimInstance -Process $Process -ErrorAction 'SilentlyContinue' + + if ($null -ne $owner) + { + if ($null -ne $owner.Domain) + { + return ($owner.Domain + '\' + $owner.User) + } + else + { + # return the default domain + return ($env:computerName + '\' + $owner.User) + } + } + + return [System.String]::Empty +} + +<# + .SYNOPSIS + Wrapper function to retrieve the CIM instance of the owner of a process + + .PARAMETER Process + The process to retrieve the CIM instance of the owner of. + + .NOTES + If the process was killed by the time this function is called, this function will throw a + WMIMethodException with the message "Not found". +#> +function Get-ProcessOwnerCimInstance +{ + [OutputType([Microsoft.Management.Infrastructure.CimInstance])] + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNull()] + [System.Object] + $Process + ) + + return Invoke-CimMethod -InputObject $Process -MethodName 'GetOwner' -ErrorAction 'SilentlyContinue' +} + +<# + .SYNOPSIS + Retrieves the 'arguments' part of command line input. + + .PARAMETER CommandLineInput + The command line input to retrieve the arguments from. + + .EXAMPLE + Get-ArgumentsFromCommandLineInput -CommandLineInput 'C:\temp\a.exe X Y Z' + Returns 'X Y Z'. +#> +function Get-ArgumentsFromCommandLineInput +{ + [OutputType([System.String])] + [CmdletBinding()] + param + ( + [Parameter()] + [System.String] + $CommandLineInput + ) + + if ([System.String]::IsNullOrWhitespace($CommandLineInput)) + { + return [System.String]::Empty + } + + $CommandLineInput = $CommandLineInput.Trim() + + if ($CommandLineInput.StartsWith('"')) + { + $endOfCommandChar = [System.Char]'"' + } + else + { + $endOfCommandChar = [System.Char]' ' + } + + $endofCommandIndex = $CommandLineInput.IndexOf($endOfCommandChar, 1) + + if ($endofCommandIndex -eq -1) + { + return [System.String]::Empty + } + + return $CommandLineInput.Substring($endofCommandIndex + 1).Trim() +} + +<# + .SYNOPSIS + Throws an invalid argument exception if the given hashtable contains the given key(s). + + .PARAMETER Hashtable + The hashtable to check the keys of. + + .PARAMETER Key + The key(s) that should not be in the hashtable. +#> +function Assert-HashtableDoesNotContainKey +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [System.Collections.Hashtable] + $Hashtable, + + [Parameter(Mandatory = $true)] + [System.String[]] + $Key + ) + + foreach ($keyName in $Key) + { + if ($Hashtable.ContainsKey($keyName)) + { + New-InvalidArgumentException -ArgumentName $keyName ` + -Message ($script:localizedData.ParameterShouldNotBeSpecified -f $keyName) + } + } +} + +<# + .SYNOPSIS + Waits for the given amount of time for the given number of processes with the given settings + to be running. If not all processes are running by 'WaitTime', the function returns + false, otherwise it returns true. + + .PARAMETER ProcessSettings + The settings for the running process(es) that we're getting the count of. + + .PARAMETER ProcessCount + The number of processes running to wait for. + + .PARAMETER WaitTime + The amount of milliseconds to wait for all processes to be running. + Default is 2000. +#> +function Wait-ProcessCount +{ + [OutputType([System.Boolean])] + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.Collections.Hashtable] + $ProcessSettings, + + [Parameter(Mandatory = $true)] + [ValidateRange(0, [System.Int32]::MaxValue)] + [System.Int32] + $ProcessCount, + + [Parameter()] + [System.Int32] + $WaitTime = 200000 + ) + + $startTime = [System.DateTime]::Now + + do + { + $actualProcessCount = @( Get-ProcessCimInstance @ProcessSettings ).Count + } while ($actualProcessCount -ne $ProcessCount -and ([System.DateTime]::Now - $startTime).TotalMilliseconds -lt $WaitTime) + + return $actualProcessCount -eq $ProcessCount +} + +<# + .SYNOPSIS + Throws an error if the given path argument is not rooted. + + .PARAMETER PathArgumentName + The name of the path argument that should be rooted. + + .PARAMETER PathArgument + The path arguments that should be rooted. +#> +function Assert-PathArgumentRooted +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $PathArgumentName, + + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $PathArgument + ) + + if (-not ([IO.Path]::IsPathRooted($PathArgument))) + { + $message = $script:localizedData.PathShouldBeAbsolute -f $PathArgumentName, $PathArgument + + New-InvalidArgumentException -ArgumentName 'Path' ` + -Message $message + } +} + +<# + .SYNOPSIS + Throws an error if the given path argument does not exist. + + .PARAMETER PathArgumentName + The name of the path argument that should exist. + + .PARAMETER PathArgument + The path argument that should exist. +#> +function Assert-PathArgumentValid +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $PathArgumentName, + + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $PathArgument + ) + + if (-not (Test-Path -Path $PathArgument)) + { + $message = $script:localizedData.PathShouldExist -f $PathArgument, $PathArgumentName + + New-InvalidArgumentException -ArgumentName 'Path' ` + -Message $message + } +} + +<# + .SYNOPSIS + Tests if the current user is from the local system. +#> +function Test-IsRunFromLocalSystemUser +{ + [OutputType([System.Boolean])] + [CmdletBinding()] + param + ( + ) + + $identity = [Security.Principal.WindowsIdentity]::GetCurrent() + $principal = New-Object -TypeName Security.Principal.WindowsPrincipal -ArgumentList $identity + + return $principal.Identity.IsSystem +} + +<# + .SYNOPSIS + Starts the process with the given credential when the user is a local system user. + + .PARAMETER Path + The path to the process executable. + + .PARAMETER Arguments + Indicates a string of arguments to pass to the process as-is. + + .PARAMETER Credential + Indicates the credential for starting the process. +#> +function Start-ProcessAsLocalSystemUser +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $Path, + + [Parameter(Mandatory = $true)] + [AllowEmptyString()] + [System.String] + $Arguments, + + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.Management.Automation.PSCredential] + [System.Management.Automation.Credential()] + $Credential + ) + + $splitCredentialResult = Split-Credential -Credential $Credential + + <# + Internally we use win32 api LogonUser() with + dwLogonType == LOGON32_LOGON_NETWORK_CLEARTEXT. + + It grants the process ability for second-hop. + #> + Import-DscNativeMethods + + $Path = Add-SurroundingDoubleQuotesToString -StringIn $Path + $Arguments = Add-SurroundingDoubleQuotesToString -StringIn $Arguments + + [PSDesiredStateConfiguration.NativeMethods]::CreateProcessAsUser( "$Path $Arguments", $splitCredentialResult.Domain, + $splitCredentialResult.UserName, $Credential.Password, + $false, [ref] $null ) +} + +<# + .SYNOPSIS + Splits a credential into a username and domain without calling GetNetworkCredential. + Calls to GetNetworkCredential expose the password as plain text in memory. + + .PARAMETER Credential + The credential to pull the username and domain out of. + + .NOTES + Supported formats: DOMAIN\username, username@domain +#> +function Split-Credential +{ + [OutputType([System.Collections.Hashtable])] + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.Management.Automation.PSCredential] + [System.Management.Automation.Credential()] + $Credential + ) + + $wrongFormat = $false + + if ($Credential.UserName.Contains('\')) + { + $credentialSegments = $Credential.UserName.Split('\') + + if ($credentialSegments.Length -gt 2) + { + # i.e. domain\user\foo + $wrongFormat = $true + } + else + { + $domain = $credentialSegments[0] + $userName = $credentialSegments[1] + } + } + elseif ($Credential.UserName.Contains('@')) + { + $credentialSegments = $Credential.UserName.Split('@') + + if ($credentialSegments.Length -gt 2) + { + # i.e. user@domain@foo + $wrongFormat = $true + } + else + { + $UserName = $credentialSegments[0] + $Domain = $credentialSegments[1] + } + } + else + { + # Support for default domain (localhost) + $domain = $env:computerName + $userName = $Credential.UserName + } + + if ($wrongFormat) + { + $message = $script:localizedData.ErrorInvalidUserName -f $Credential.UserName + + New-InvalidArgumentException -ArgumentName 'Credential' -Message $message + } + + return @{ + Domain = $domain + UserName = $userName + } +} + +<# + .SYNOPSIS + Asserts that the PsDscContext is not run as user. + Throws an invalid argument exception if DSC is running as a specific user + (the PsDscRunAsCredential parameter was provided to DSC). + + .NOTES + Strict mode is turned off for this function since it does not recognize $PsDscContext +#> +function Assert-PsDscContextNotRunAsUser +{ + [CmdletBinding()] + param + ( + ) + + Set-StrictMode -Off + + if ($null -ne $PsDscContext.RunAsUser) + { + $newInvalidArgumentExceptionParams = @{ + ArgumentName = 'PsDscRunAsCredential' + Message = ($script:localizedData.ErrorRunAsCredentialParameterNotSupported -f $PsDscContext.RunAsUser) + } + + New-InvalidArgumentException @newInvalidArgumentExceptionParams + } +} + +<# + .SYNOPSIS + Imports the DSC native methods so that a process can be started with a credential + for a user from the local system. + Currently Start-Process, which is the command used otherwise, cannot do this. +#> +function Import-DscNativeMethods +{ + $dscNativeMethodsSource = @" + +using System; +using System.Collections.Generic; +using System.Text; +using System.Security; +using System.Runtime.InteropServices; +using System.Diagnostics; +using System.Security.Principal; +#if !CORECLR +using System.ComponentModel; +#endif +using System.IO; + +namespace PSDesiredStateConfiguration +{ +#if !CORECLR + [SuppressUnmanagedCodeSecurity] +#endif + public static class NativeMethods + { + //The following structs and enums are used by the various Win32 API's that are used in the code below + + [StructLayout(LayoutKind.Sequential)] + public struct STARTUPINFO + { + public Int32 cb; + public string lpReserved; + public string lpDesktop; + public string lpTitle; + public Int32 dwX; + public Int32 dwY; + public Int32 dwXSize; + public Int32 dwXCountChars; + public Int32 dwYCountChars; + public Int32 dwFillAttribute; + public Int32 dwFlags; + public Int16 wShowWindow; + public Int16 cbReserved2; + public IntPtr lpReserved2; + public IntPtr hStdInput; + public IntPtr hStdOutput; + public IntPtr hStdError; + } + + [StructLayout(LayoutKind.Sequential)] + public struct PROCESS_INFORMATION + { + public IntPtr hProcess; + public IntPtr hThread; + public Int32 dwProcessID; + public Int32 dwThreadID; + } + + [Flags] + public enum LogonType + { + LOGON32_LOGON_INTERACTIVE = 2, + LOGON32_LOGON_NETWORK = 3, + LOGON32_LOGON_BATCH = 4, + LOGON32_LOGON_SERVICE = 5, + LOGON32_LOGON_UNLOCK = 7, + LOGON32_LOGON_NETWORK_CLEARTEXT = 8, + LOGON32_LOGON_NEW_CREDENTIALS = 9 + } + + [Flags] + public enum LogonProvider + { + LOGON32_PROVIDER_DEFAULT = 0, + LOGON32_PROVIDER_WINNT35, + LOGON32_PROVIDER_WINNT40, + LOGON32_PROVIDER_WINNT50 + } + [StructLayout(LayoutKind.Sequential)] + public struct SECURITY_ATTRIBUTES + { + public Int32 Length; + public IntPtr lpSecurityDescriptor; + public bool bInheritHandle; + } + + public enum SECURITY_IMPERSONATION_LEVEL + { + SecurityAnonymous, + SecurityIdentification, + SecurityImpersonation, + SecurityDelegation + } + + public enum TOKEN_TYPE + { + TokenPrimary = 1, + TokenImpersonation + } + + [StructLayout(LayoutKind.Sequential, Pack = 1)] + internal struct TokPriv1Luid + { + public int Count; + public long Luid; + public int Attr; + } + + public const int GENERIC_ALL_ACCESS = 0x10000000; + public const int CREATE_NO_WINDOW = 0x08000000; + internal const int SE_PRIVILEGE_ENABLED = 0x00000002; + internal const int TOKEN_QUERY = 0x00000008; + internal const int TOKEN_ADJUST_PRIVILEGES = 0x00000020; + internal const string SE_INCRASE_QUOTA = "SeIncreaseQuotaPrivilege"; + +#if CORECLR + [DllImport("api-ms-win-core-handle-l1-1-0.dll", +#else + [DllImport("kernel32.dll", +#endif + EntryPoint = "CloseHandle", SetLastError = true, + CharSet = CharSet.Auto, CallingConvention = CallingConvention.StdCall)] + public static extern bool CloseHandle(IntPtr handle); + +#if CORECLR + [DllImport("api-ms-win-core-processthreads-l1-1-2.dll", +#else + [DllImport("advapi32.dll", +#endif + EntryPoint = "CreateProcessAsUser", SetLastError = true, + CharSet = CharSet.Ansi, CallingConvention = CallingConvention.StdCall)] + public static extern bool CreateProcessAsUser( + IntPtr hToken, + string lpApplicationName, + string lpCommandLine, + ref SECURITY_ATTRIBUTES lpProcessAttributes, + ref SECURITY_ATTRIBUTES lpThreadAttributes, + bool bInheritHandle, + Int32 dwCreationFlags, + IntPtr lpEnvrionment, + string lpCurrentDirectory, + ref STARTUPINFO lpStartupInfo, + ref PROCESS_INFORMATION lpProcessInformation + ); + +#if CORECLR + [DllImport("api-ms-win-security-base-l1-1-0.dll", EntryPoint = "DuplicateTokenEx")] +#else + [DllImport("advapi32.dll", EntryPoint = "DuplicateTokenEx")] +#endif + public static extern bool DuplicateTokenEx( + IntPtr hExistingToken, + Int32 dwDesiredAccess, + ref SECURITY_ATTRIBUTES lpThreadAttributes, + Int32 ImpersonationLevel, + Int32 dwTokenType, + ref IntPtr phNewToken + ); + +#if CORECLR + [DllImport("api-ms-win-security-logon-l1-1-1.dll", CharSet = CharSet.Unicode, SetLastError = true)] +#else + [DllImport("advapi32.dll", CharSet = CharSet.Unicode, SetLastError = true)] +#endif + public static extern Boolean LogonUser( + String lpszUserName, + String lpszDomain, + IntPtr lpszPassword, + LogonType dwLogonType, + LogonProvider dwLogonProvider, + out IntPtr phToken + ); + +#if CORECLR + [DllImport("api-ms-win-security-base-l1-1-0.dll", ExactSpelling = true, SetLastError = true)] +#else + [DllImport("advapi32.dll", ExactSpelling = true, SetLastError = true)] +#endif + internal static extern bool AdjustTokenPrivileges( + IntPtr htok, + bool disall, + ref TokPriv1Luid newst, + int len, + IntPtr prev, + IntPtr relen + ); + +#if CORECLR + [DllImport("api-ms-win-downlevel-kernel32-l1-1-0.dll", ExactSpelling = true)] +#else + [DllImport("kernel32.dll", ExactSpelling = true)] +#endif + internal static extern IntPtr GetCurrentProcess(); + +#if CORECLR + [DllImport("api-ms-win-downlevel-advapi32-l1-1-1.dll", ExactSpelling = true, SetLastError = true)] +#else + [DllImport("advapi32.dll", ExactSpelling = true, SetLastError = true)] +#endif + internal static extern bool OpenProcessToken( + IntPtr h, + int acc, + ref IntPtr phtok + ); + +#if CORECLR + [DllImport("api-ms-win-downlevel-kernel32-l1-1-0.dll", ExactSpelling = true)] +#else + [DllImport("kernel32.dll", ExactSpelling = true)] +#endif + internal static extern int WaitForSingleObject( + IntPtr h, + int milliseconds + ); + +#if CORECLR + [DllImport("api-ms-win-downlevel-kernel32-l1-1-0.dll", ExactSpelling = true)] +#else + [DllImport("kernel32.dll", ExactSpelling = true)] +#endif + internal static extern bool GetExitCodeProcess( + IntPtr h, + out int exitcode + ); + +#if CORECLR + [DllImport("api-ms-win-downlevel-advapi32-l4-1-0.dll", SetLastError = true)] +#else + [DllImport("advapi32.dll", SetLastError = true)] +#endif + internal static extern bool LookupPrivilegeValue( + string host, + string name, + ref long pluid + ); + + internal static void ThrowException( + string message + ) + { +#if CORECLR + throw new Exception(message); +#else + throw new Win32Exception(message); +#endif + } + + public static void CreateProcessAsUser(string strCommand, string strDomain, string strName, SecureString secureStringPassword, bool waitForExit, ref int ExitCode) + { + var hToken = IntPtr.Zero; + var hDupedToken = IntPtr.Zero; + TokPriv1Luid tp; + var pi = new PROCESS_INFORMATION(); + var sa = new SECURITY_ATTRIBUTES(); + sa.Length = Marshal.SizeOf(sa); + Boolean bResult = false; + try + { + IntPtr unmanagedPassword = IntPtr.Zero; + try + { +#if CORECLR + unmanagedPassword = SecureStringMarshal.SecureStringToCoTaskMemUnicode(secureStringPassword); +#else + unmanagedPassword = Marshal.SecureStringToGlobalAllocUnicode(secureStringPassword); +#endif + bResult = LogonUser( + strName, + strDomain, + unmanagedPassword, + LogonType.LOGON32_LOGON_NETWORK_CLEARTEXT, + LogonProvider.LOGON32_PROVIDER_DEFAULT, + out hToken + ); + } + finally + { + Marshal.ZeroFreeGlobalAllocUnicode(unmanagedPassword); + } + if (!bResult) + { + ThrowException("$($script:localizedData.UserCouldNotBeLoggedError)" + Marshal.GetLastWin32Error().ToString()); + } + IntPtr hproc = GetCurrentProcess(); + IntPtr htok = IntPtr.Zero; + bResult = OpenProcessToken( + hproc, + TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY, + ref htok + ); + if (!bResult) + { + ThrowException("$($script:localizedData.OpenProcessTokenError)" + Marshal.GetLastWin32Error().ToString()); + } + tp.Count = 1; + tp.Luid = 0; + tp.Attr = SE_PRIVILEGE_ENABLED; + bResult = LookupPrivilegeValue( + null, + SE_INCRASE_QUOTA, + ref tp.Luid + ); + if (!bResult) + { + ThrowException("$($script:localizedData.PrivilegeLookingUpError)" + Marshal.GetLastWin32Error().ToString()); + } + bResult = AdjustTokenPrivileges( + htok, + false, + ref tp, + 0, + IntPtr.Zero, + IntPtr.Zero + ); + if (!bResult) + { + ThrowException("$($script:localizedData.TokenElevationError)" + Marshal.GetLastWin32Error().ToString()); + } + + bResult = DuplicateTokenEx( + hToken, + GENERIC_ALL_ACCESS, + ref sa, + (int)SECURITY_IMPERSONATION_LEVEL.SecurityIdentification, + (int)TOKEN_TYPE.TokenPrimary, + ref hDupedToken + ); + if (!bResult) + { + ThrowException("$($script:localizedData.DuplicateTokenError)" + Marshal.GetLastWin32Error().ToString()); + } + var si = new STARTUPINFO(); + si.cb = Marshal.SizeOf(si); + si.lpDesktop = ""; + bResult = CreateProcessAsUser( + hDupedToken, + null, + strCommand, + ref sa, + ref sa, + false, + 0, + IntPtr.Zero, + null, + ref si, + ref pi + ); + if (!bResult) + { + ThrowException("$($script:localizedData.CouldNotCreateProcessError)" + Marshal.GetLastWin32Error().ToString()); + } + if (waitForExit) { + int status = WaitForSingleObject(pi.hProcess, -1); + if(status == -1) + { + ThrowException("$($script:localizedData.WaitFailedError)" + Marshal.GetLastWin32Error().ToString()); + } + + bResult = GetExitCodeProcess(pi.hProcess, out ExitCode); + if(!bResult) + { + ThrowException("$($script:localizedData.RetriveStatusError)" + Marshal.GetLastWin32Error().ToString()); + } + } + } + finally + { + if (pi.hThread != IntPtr.Zero) + { + CloseHandle(pi.hThread); + } + if (pi.hProcess != IntPtr.Zero) + { + CloseHandle(pi.hProcess); + } + if (hDupedToken != IntPtr.Zero) + { + CloseHandle(hDupedToken); + } + } + } + } +} + +"@ + # if not on Nano: + Add-Type -TypeDefinition $dscNativeMethodsSource -ReferencedAssemblies 'System.ServiceProcess' +} + +<# + .SYNOPSIS + Takes the given string, adds surrounding double quotes to the + string if it does not already have them, and returns the new string. + + .PARAMETER StringIn + The string to add quotes to, if missing. + + .PARAMETER QuoteEmptyStrings + Whether quotes should be added to empty strings. Defaults to False. +#> +function Add-SurroundingDoubleQuotesToString +{ + [OutputType([System.String])] + [CmdletBinding()] + param + ( + [Parameter()] + [System.String] + $StringIn, + + [Parameter()] + [System.Boolean] + $QuoteEmptyStrings = $false + ) + + if ($QuoteEmptyStrings -and [String]::IsNullOrEmpty($StringIn) -or $StringIn -notlike "`"*`"") + { + return "`"$StringIn`"" + } + else + { + return $StringIn + } +} + +Export-ModuleMember -Function *-TargetResource diff --git a/deployment/dsc/azshcihost/xPSDesiredStateConfiguration/9.1.0/DSCResources/DSC_xWindowsProcess/DSC_xWindowsProcess.schema.mof b/deployment/dsc/azshcihost/xPSDesiredStateConfiguration/9.1.0/DSCResources/DSC_xWindowsProcess/DSC_xWindowsProcess.schema.mof new file mode 100644 index 0000000..252f557 --- /dev/null +++ b/deployment/dsc/azshcihost/xPSDesiredStateConfiguration/9.1.0/DSCResources/DSC_xWindowsProcess/DSC_xWindowsProcess.schema.mof @@ -0,0 +1,18 @@ +[ClassVersion("1.0.0"), FriendlyName("xWindowsProcess")] +class DSC_xWindowsProcess : OMI_BaseResource +{ + [Key, Description("The full path or file name to the process executable to start or stop.")] String Path; + [Key, Description("A string of arguments to pass to the process executable. Pass in an empty string if no arguments are needed.")] String Arguments; + [Write, EmbeddedInstance("MSFT_Credential"), Description("The credential to run the process under.")] String Credential; + [Write, ValueMap{"Present", "Absent"}, Values{"Present", "Absent"}, Description("Indicates whether the process is present (running) or absent (not running).")] String Ensure; + [Write, Description("The path to write the standard output stream to.")] String StandardOutputPath; + [Write, Description("The path to write the standard error stream to.")] String StandardErrorPath; + [Write, Description("The path to receive standard input from.")] String StandardInputPath; + [Write, Description("The directory to run the processes under.")] String WorkingDirectory; + [Read, Description("The amount of paged memory, in bytes, allocated for the process.")] UInt64 PagedMemorySize; + [Read, Description("The amount of nonpaged memory, in bytes, allocated for the process.")] UInt64 NonPagedMemorySize; + [Read, Description("The amount of virtual memory, in bytes, allocated for the process.")] UInt64 VirtualMemorySize; + [Read, Description("The number of handles opened by the process.")] SInt32 HandleCount; + [Read, Description("The unique identifier of the process.")] SInt32 ProcessId; + [Read, Description("The number of instances of the given process that are currently running.")] SInt32 ProcessCount; +}; diff --git a/deployment/dsc/azshcihost/xPSDesiredStateConfiguration/9.1.0/DSCResources/DSC_xWindowsProcess/en-US/DSC_xWindowsProcess.schema.mfl b/deployment/dsc/azshcihost/xPSDesiredStateConfiguration/9.1.0/DSCResources/DSC_xWindowsProcess/en-US/DSC_xWindowsProcess.schema.mfl new file mode 100644 index 0000000..34246af --- /dev/null +++ b/deployment/dsc/azshcihost/xPSDesiredStateConfiguration/9.1.0/DSCResources/DSC_xWindowsProcess/en-US/DSC_xWindowsProcess.schema.mfl @@ -0,0 +1,19 @@ +[Description("The xWindowsProcess resource provides a mechanism to manage processes.") : Amended,AMENDMENT, LOCALE("MS_409")] +class DSC_xWindowsProcess : OMI_BaseResource +{ + [Key, Description("The full path or file name to the process executable to start or stop.") : Amended] String Path; + [Key, Description("A string of arguments to pass to the process executable. Pass in an empty string if no arguments are needed.") : Amended] String Arguments; + [Description("The credential to run the process under.") : Amended] String Credential; + [Description("Indicates whether the process is present (running) or absent (not running).\nPresent {default} \nAbsent \n") : Amended] String Ensure; + [Description("The path to write the standard output stream to.") : Amended] String StandardOutputPath; + [Description("The path to write the standard error stream to.") : Amended] String StandardErrorPath; + [Description("The path to receive standard input from.") : Amended] String StandardInputPath; + [Description("The directory to run the processes under.") : Amended] String WorkingDirectory; + [Description("The amount of paged memory, in bytes, allocated for the process.") : Amended] UInt64 PagedMemorySize; + [Description("The amount of nonpaged memory, in bytes, allocated for the process.") : Amended] UInt64 NonPagedMemorySize; + [Description("The amount of virtual memory, in bytes, allocated for the process.") : Amended] UInt64 VirtualMemorySize; + [Description("The number of handles opened by the process.") : Amended] SInt32 HandleCount; + [Description("The unique identifier of the process.") : Amended] SInt32 ProcessId; + [Description("The number of instances of the given process that are currently running.") : Amended] SInt32 ProcessCount; +}; + diff --git a/deployment/dsc/azshcihost/xPSDesiredStateConfiguration/9.1.0/DSCResources/DSC_xWindowsProcess/en-US/DSC_xWindowsProcess.strings.psd1 b/deployment/dsc/azshcihost/xPSDesiredStateConfiguration/9.1.0/DSCResources/DSC_xWindowsProcess/en-US/DSC_xWindowsProcess.strings.psd1 new file mode 100644 index 0000000..057aab1 --- /dev/null +++ b/deployment/dsc/azshcihost/xPSDesiredStateConfiguration/9.1.0/DSCResources/DSC_xWindowsProcess/en-US/DSC_xWindowsProcess.strings.psd1 @@ -0,0 +1,36 @@ +# Localized resources for DSC_xWindowsProcess + +ConvertFrom-StringData @' + CouldNotCreateProcessError = Could not create process. Error code: + DuplicateTokenError = Duplicate token. Error code: + FileNotFound = File '{0}' not found in the environment path. + ErrorInvalidUserName = Invalid username: {0}. Username cannot contain multiple '@' or multiple '\' + ErrorParametersNotSupportedWithCredential = Can't specify StandardOutputPath, StandardInputPath or WorkingDirectory when trying to run a process under a local user. + ErrorRunAsCredentialParameterNotSupported = The PsDscRunAsCredential parameter is not supported by the Process resource. To start the process with user '{0}', add the Credential parameter. + ErrorStarting = Failure starting process matching path '{0}'. Message: {1}. + ErrorStopping = Failure stopping processes matching path '{0}' with IDs '({1})'. Message: {2}. + FailureWaitingForProcessesToStart = Failed to wait for processes to start. + FailureWaitingForProcessesToStop = Failed to wait for processes to stop. + GetTargetResourceStartMessage = Begin executing Get functionality for the process {0}. + GetTargetResourceEndMessage = End executing Get functionality for the process {0}. + OpenProcessTokenError = Error while opening process token. Error code: + ParameterShouldNotBeSpecified = Parameter {0} should not be specified. + PathShouldBeAbsolute = The path '{0}' should be absolute for argument '{1}'. + PathShouldExist = The path '{0}' should exist for argument '{1}'. + PrivilegeLookingUpError = Error while looking up privilege. Error code: + ProcessAlreadyStarted = Process matching path '{0}' found running. No action required. + ProcessAlreadyStopped = Process matching path '{0}' not found running. No action required. + ProcessesStarted = Processes matching path '{0}' started. + ProcessesStopped = Processes matching path '{0}' with IDs '({1})' stopped. + RetriveStatusError = Failed to retrieve status. Error code: + SetTargetResourceStartMessage = Begin executing Set functionality for the process {0}. + SetTargetResourceEndMessage = End executing Set functionality for the process {0}. + StartingProcessWhatif = Start-Process. + StoppingProcessWhatIf = Stop-Process. + TestTargetResourceStartMessage = Begin executing Test functionality for the process {0}. + TestTargetResourceEndMessage = End executing Test functionality for the process {0}. + TokenElevationError = Error while getting token elevation. Error code: + UserCouldNotBeLoggedError = User could not be logged. Error code: + WaitFailedError = Failed while waiting for process. Error code: + VerboseInProcessHandle = In process handle {0}. +'@ diff --git a/deployment/dsc/azshcihost/xPSDesiredStateConfiguration/9.1.0/DSCResources/xFileUpload/xFileUpload.psd1 b/deployment/dsc/azshcihost/xPSDesiredStateConfiguration/9.1.0/DSCResources/xFileUpload/xFileUpload.psd1 new file mode 100644 index 0000000..50b43e3 --- /dev/null +++ b/deployment/dsc/azshcihost/xPSDesiredStateConfiguration/9.1.0/DSCResources/xFileUpload/xFileUpload.psd1 @@ -0,0 +1,25 @@ +@{ + # Script module or binary module file associated with this manifest. + RootModule = 'xFileUpload.schema.psm1' + + # Version number of this module. + ModuleVersion = '1.0.0.0' + + # ID used to uniquely identify this module + GUID = '1fbfd112-4272-4fb8-b31c-fb5b417484bc' + + # Author of this module + Author = 'DSC Community' + + # Company or vendor of this module + CompanyName = 'DSC Community' + + # Copyright statement for this module + Copyright = 'Copyright the DSC Community contributors. All rights reserved.' + + # Description of the functionality provided by this module + Description = '' + + # Minimum version of the Windows PowerShell engine required by this module + PowerShellVersion = '4.0' +} diff --git a/deployment/dsc/azshcihost/xPSDesiredStateConfiguration/9.1.0/DSCResources/xFileUpload/xFileUpload.schema.psm1 b/deployment/dsc/azshcihost/xPSDesiredStateConfiguration/9.1.0/DSCResources/xFileUpload/xFileUpload.schema.psm1 new file mode 100644 index 0000000..1353248 --- /dev/null +++ b/deployment/dsc/azshcihost/xPSDesiredStateConfiguration/9.1.0/DSCResources/xFileUpload/xFileUpload.schema.psm1 @@ -0,0 +1,670 @@ +$errorActionPreference = 'Stop' +Set-StrictMode -Version 'Latest' + +$modulePath = Join-Path -Path (Split-Path -Path (Split-Path -Path $PSScriptRoot -Parent) -Parent) -ChildPath 'Modules' + +# Import the shared modules +Import-Module -Name (Join-Path -Path $modulePath ` + -ChildPath (Join-Path -Path 'xPSDesiredStateConfiguration.Common' ` + -ChildPath 'xPSDesiredStateConfiguration.Common.psm1')) + +<# + .SYNOPSIS + DSC Composite Resource uploads file or folder to an SMB share. + + .DESCRIPTION + This is a DSC Composite resource that can be used to upload + a file or folder into an SMB file share. The SMB file share + does not have to be currently mounted. It will be mounted + during the upload process using the optional Credential + and then dismounted after completion of the upload. + + .PARAMETER DestinationPath + The destination SMB share path to upload the file or folder to. + + .PARAMETER SourcePath + The source path of the file or folder to upload. + + .PARAMETER Credential + Credentials to access the destination SMB share path where file + or folder should be uploaded. + + .PARAMETER certificateThumbprint + Thumbprint of the certificate which should be used for encryption/decryption. + + .EXAMPLE + $securePassword = ConvertTo-SecureString -String 'password' -AsPlainText -Force + $credential = New-Object -TypeName System.Management.Automation.PSCredential -ArgumentList 'domain\user', $securePassword + xFileUpload ` + -DestinationPath '\\machine\share\destinationfolder' ` + -SourcePath 'C:\folder\file.txt' ` + -Credential $credential +#> +configuration xFileUpload +{ + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingConvertToSecureStringWithPlainText', '')] + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('DscResource.AnalyzerRules\Measure-Keyword', '', Justification = 'Script resource name is seen as a keyword if this is not used.')] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $DestinationPath, + + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $SourcePath, + + [Parameter()] + [System.Management.Automation.Credential()] + [System.Management.Automation.PSCredential] + $Credential, + + [Parameter()] + [System.String] + $CertificateThumbprint + ) + + $cacheLocation = "$env:ProgramData\Microsoft\Windows\PowerShell\configuration\BuiltinProvCache\DSC_xFileUpload" + + if ($Credential) + { + $username = $Credential.UserName + + # Encrypt password + $password = Invoke-Command ` + -ScriptBlock $getEncryptedPassword ` + -ArgumentList $Credential, $CertificateThumbprint + } + + Script FileUpload + { + # Get script is not implemented cause reusing Script resource's schema does not make sense + GetScript = { + return @{} + }; + + SetScript = { + # Generating credential object if password and username are specified + $Credential = $null + + if (($using:password) -and ($using:username)) + { + # Validate that certificate thumbprint is specified + if (-not $using:CertificateThumbprint) + { + $errorMessage = 'Certificate thumbprint has to be specified if credentials are present.' + Invoke-Command ` + -ScriptBlock $using:throwTerminatingError ` + -ArgumentList 'CertificateThumbprintIsRequired', $errorMessage, 'InvalidData' + } + + Write-Debug -Message 'Username and password specified.' + + # Decrypt password + $decryptedPassword = Invoke-Command ` + -ScriptBlock $using:getDecryptedPassword ` + -ArgumentList $using:password, $using:CertificateThumbprint + + # Generate credential + $securePassword = ConvertTo-SecureString -String $decryptedPassword -AsPlainText -Force + $Credential = New-Object ` + -TypeName System.Management.Automation.PSCredential ` + -ArgumentList ($using:username, $securePassword) + } + + # Validate DestinationPath is UNC path + if (-not ($using:DestinationPath -as [System.Uri]).isUnc) + { + $errorMessage = "Destination path $using:DestinationPath is not a valid UNC path." + Invoke-Command ` + -ScriptBlock $using:throwTerminatingError ` + -ArgumentList 'DestinationPathIsNotUNCFailure', $errorMessage, 'InvalidData' + } + + # Verify source is localpath + if (-not (($using:SourcePath -as [System.Uri]).Scheme -match 'file')) + { + $errorMessage = "Source path $using:SourcePath has to be local path." + Invoke-Command ` + -ScriptBlock $using:throwTerminatingError ` + -ArgumentList 'SourcePathIsNotLocalFailure', $errorMessage, 'InvalidData' + } + + # Check whether source path is existing file or directory + $sourcePathType = $null + + if (-not (Test-Path -Path $using:SourcePath)) + { + $errorMessage = "Source path $using:SourcePath does not exist." + Invoke-Command ` + -ScriptBlock $using:throwTerminatingError ` + -ArgumentList 'SourcePathDoesNotExistFailure', $errorMessage, 'InvalidData' + } + else + { + $item = Get-Item -Path $using:SourcePath + + switch ($item.GetType().Name) + { + 'FileInfo' + { + $sourcePathType = 'File' + } + + 'DirectoryInfo' + { + $sourcePathType = 'Directory' + } + } + } + + Write-Debug -Message "SourcePath $using:SourcePath is of type: $sourcePathType" + + $psDrive = $null + + # Mount the drive only if Credentials are specified and it's currently not accessible + if ($Credential) + { + if (Test-Path -Path $using:DestinationPath -ErrorAction Ignore) + { + Write-Debug -Message "Destination path $using:DestinationPath is already accessible. No mount needed." + } + else + { + $psDriveArgs = @{ + Name = ([System.Guid]::NewGuid()) + PSProvider = 'FileSystem' + Root = $using:DestinationPath + Scope = 'Private' + Credential = $Credential + } + + try + { + Write-Debug -Message "Create psdrive with destination path $using:DestinationPath..." + $psDrive = New-PSDrive @psDriveArgs -ErrorAction Stop + } + catch + { + $errorMessage = "Cannot access destination path $using:DestinationPath with given Credential" + Invoke-Command ` + -ScriptBlock $using:throwTerminatingError ` + -ArgumentList 'DestinationPathNotAccessibleFailure', $errorMessage, 'InvalidData' + } + } + } + + try + { + # Get expected destination path + $expectedDestinationPath = $null + + if (-not (Test-Path -Path $using:DestinationPath)) + { + # DestinationPath has to exist + $errorMessage = 'Invalid parameter values: DestinationPath does not exist, but has to be existing directory.' + Throw-TerminatingError -ErrorMessage $errorMessage -ErrorCategory 'InvalidData' -ErrorId 'DestinationPathDoesNotExistFailure' + } + else + { + $item = Get-Item -Path $using:DestinationPath + + switch ($item.GetType().Name) + { + 'FileInfo' + { + # DestinationPath cannot be file + $errorMessage = 'Invalid parameter values: DestinationPath is file, but has to be existing directory.' + Invoke-Command ` + -ScriptBlock $using:throwTerminatingError ` + -ArgumentList 'DestinationPathCannotBeFileFailure', $errorMessage, 'InvalidData' + } + + 'DirectoryInfo' + { + $expectedDestinationPath = Join-Path ` + -Path $using:DestinationPath ` + -ChildPath (Split-Path -Path $using:SourcePath -Leaf) + } + } + + Write-Debug -Message "ExpectedDestinationPath is $expectedDestinationPath" + } + + # Copy destination path + try + { + Write-Debug -Message "Copying $using:SourcePath to $using:DestinationPath" + Copy-Item -Path $using:SourcePath -Destination $using:DestinationPath -Recurse -Force -ErrorAction Stop + } + catch + { + $errorMessage = "Could not copy source path $using:SourcePath to $using:DestinationPath : $($_.Exception)" + Invoke-Command ` + -ScriptBlock $using:throwTerminatingError ` + -ArgumentList 'CopyDirectoryOverFileFailure', $errorMessage, 'InvalidData' + } + + # Verify whether expectedDestinationPath was created + if (-not (Test-Path -Path $expectedDestinationPath)) + { + $errorMessage = "Destination path $using:DestinationPath could not be created" + Invoke-Command ` + -ScriptBlock $using:throwTerminatingError ` + -ArgumentList 'DestinationPathNotCreatedFailure', $errorMessage, 'InvalidData' + } + # If expectedDestinationPath exists + else + { + Write-Verbose -Message "$sourcePathType $expectedDestinationPath has been successfully created" + + # Update cache + $uploadedItem = Get-Item -Path $expectedDestinationPath + $lastWriteTime = $uploadedItem.LastWriteTimeUtc + $inputObject = @{} + $inputObject['LastWriteTimeUtc'] = $lastWriteTime + $key = [System.String]::Join('', @($using:DestinationPath, $using:SourcePath, $expectedDestinationPath)).GetHashCode().ToString() + $path = Join-Path $using:cacheLocation $key + + if (-not (Test-Path -Path $using:cacheLocation)) + { + New-Item -Path $using:cacheLocation -ItemType Directory | Out-Null + } + + Write-Debug -Message "Updating cache for DestinationPath = $using:DestinationPath and SourcePath = $using:SourcePath. CacheKey = $key" + Export-CliXml -Path $path -InputObject $inputObject -Force + } + } + finally + { + # Remove PSDrive + if ($psDrive) + { + Write-Debug -Message "Removing PSDrive on root $($psDrive.Root)" + Remove-PSDrive -Name $psDrive -Force + } + } + }; + + TestScript = { + # Generating credential object if password and username are specified + $Credential = $null + + if (($using:password) -and ($using:username)) + { + # Validate that certificate thumbprint is specified + if (-not $using:CertificateThumbprint) + { + $errorMessage = 'Certificate thumbprint has to be specified if credentials are present.' + Invoke-Command ` + -ScriptBlock $using:throwTerminatingError ` + -ArgumentList 'CertificateThumbprintIsRequired', $errorMessage, 'InvalidData' + } + + Write-Debug -Message 'Username and password specified. Generating credential' + + # Decrypt password + $decryptedPassword = Invoke-Command ` + -ScriptBlock $using:getDecryptedPassword ` + -ArgumentList $using:password, $using:CertificateThumbprint + + # Generate credential + $securePassword = ConvertTo-SecureString -String $decryptedPassword -AsPlainText -Force + $Credential = New-Object ` + -TypeName System.Management.Automation.PSCredential ` + -ArgumentList ($using:username, $securePassword) + } + else + { + Write-Debug -Message 'No credentials specified.' + } + + # Validate DestinationPath is UNC path + if (-not ($using:DestinationPath -as [System.Uri]).isUnc) + { + $errorMessage = "Destination path $using:DestinationPath is not a valid UNC path." + Invoke-Command ` + -ScriptBlock $using:throwTerminatingError ` + -ArgumentList 'DestinationPathIsNotUNCFailure', $errorMessage, 'InvalidData' + + } + + # Check whether source path is existing file or directory (needed for expectedDestinationPath) + $sourcePathType = $null + if (-not (Test-Path -Path $using:SourcePath)) + { + $errorMessage = "Source path $using:SourcePath does not exist." + Invoke-Command ` + -ScriptBlock $using:throwTerminatingError ` + -ArgumentList 'SourcePathDoesNotExistFailure', $errorMessage, 'InvalidData' + } + else + { + $item = Get-Item -Path $using:SourcePath + + switch ($item.GetType().Name) + { + 'FileInfo' + { + $sourcePathType = 'File' + } + + 'DirectoryInfo' + { + $sourcePathType = 'Directory' + } + } + } + + Write-Debug -Message "SourcePath $using:SourcePath is of type: $sourcePathType" + + $psDrive = $null + + # Mount the drive only if credentials are specified and it's currently not accessible + if ($Credential) + { + if (Test-Path -Path $using:DestinationPath -ErrorAction Ignore) + { + Write-Debug -Message "Destination path $using:DestinationPath is already accessible. No mount needed." + } + else + { + $psDriveArgs = @{ + Name = ([System.Guid]::NewGuid()) + PSProvider = 'FileSystem' + Root = $using:DestinationPath + Scope = 'Private' + Credential = $Credential + + } + try + { + Write-Debug -Message "Create psdrive with destination path $using:DestinationPath..." + $psDrive = New-PSDrive @psDriveArgs -ErrorAction Stop + } + catch + { + $errorMessage = "Cannot access destination path $using:DestinationPath with given Credential" + Invoke-Command ` + -ScriptBlock $using:throwTerminatingError ` + -ArgumentList 'DestinationPathNotAccessibleFailure', $errorMessage, 'InvalidData' + } + } + } + + try + { + # Get expected destination path + $expectedDestinationPath = $null + + if (-not (Test-Path -Path $using:DestinationPath)) + { + # DestinationPath has to exist + $errorMessage = 'Invalid parameter values: DestinationPath does not exist or is not accessible. DestinationPath has to be existing directory.' + Invoke-Command ` + -ScriptBlock $using:throwTerminatingError ` + -ArgumentList 'DestinationPathDoesNotExistFailure', $errorMessage, 'InvalidData' + } + else + { + $item = Get-Item -Path $using:DestinationPath + + switch ($item.GetType().Name) + { + 'FileInfo' + { + # DestinationPath cannot be file + $errorMessage = 'Invalid parameter values: DestinationPath is file, but has to be existing directory.' + Invoke-Command ` + -ScriptBlock $using:throwTerminatingError ` + -ArgumentList 'DestinationPathCannotBeFileFailure', $errorMessage, 'InvalidData' + } + + 'DirectoryInfo' + { + $expectedDestinationPath = Join-Path ` + -Path $using:DestinationPath ` + -ChildPath (Split-Path -Path $using:SourcePath -Leaf) + } + } + + Write-Debug -Message "ExpectedDestinationPath is $expectedDestinationPath" + } + + # Check whether ExpectedDestinationPath exists and has expected type + $itemExists = $false + + if (-not (Test-Path $expectedDestinationPath)) + { + Write-Debug -Message 'Expected destination path does not exist or is not accessible.' + } + # If expectedDestinationPath exists + else + { + $expectedItem = Get-Item -Path $expectedDestinationPath + $expectedItemType = $expectedItem.GetType().Name + + # If expectedDestinationPath has same type as sourcePathType, we need to verify cache to determine whether no upload is needed + if ((($expectedItemType -eq 'FileInfo') -and ($sourcePathType -eq 'File')) -or ` + (($expectedItemType -eq 'DirectoryInfo') -and ($sourcePathType -eq 'Directory'))) + { + # Get cache + Write-Debug -Message "Getting cache for $expectedDestinationPath" + $cacheContent = $null + $key = [System.String]::Join('', @($using:DestinationPath, $using:SourcePath, $expectedDestinationPath)).GetHashCode().ToString() + $path = Join-Path -Path $using:cacheLocation -ChildPath $key + Write-Debug -Message "Looking for cache under $path" + + if (-not (Test-Path -Path $path)) + { + Write-Debug -Message "No cache found for DestinationPath = $using:DestinationPath and SourcePath = $using:SourcePath. CacheKey = $key" + } + else + { + $cacheContent = Import-CliXml -Path $path + Write-Debug -Message "Found cache for DestinationPath = $using:DestinationPath and SourcePath = $using:SourcePath. CacheKey = $key" + } + + # Verify whether cache reflects current state or upload is needed + if ($cacheContent -ne $null -and ($cacheContent.LastWriteTimeUtc -eq $expectedItem.LastWriteTimeUtc)) + { + # No upload needed + Write-Debug -Message 'Cache reflects current state. No need for upload.' + $itemExists = $true + } + else + { + Write-Debug -Message 'Cache is empty or it does not reflect current state. Upload will be performed.' + } + } + else + { + Write-Debug -Message "Expected destination path: $expectedDestinationPath is of type $expectedItemType, although source path is $sourcePathType" + } + } + } + finally + { + # Remove PSDrive + if ($psDrive) + { + Write-Debug -Message "Removing PSDrive on root $($psDrive.Root)" + Remove-PSDrive -Name $psDrive -Force + } + } + + return $itemExists + }; + } +} + +# Encrypts password using the defined public key +[System.Management.Automation.ScriptBlock] $getEncryptedPassword = { + param + ( + [Parameter(Mandatory = $true)] + [PSCredential] + $Credential, + + [Parameter(Mandatory = $true)] + [System.String] + $CertificateThumbprint + ) + + $value = $Credential.GetNetworkCredential().Password + + $cert = Invoke-Command ` + -ScriptBlock $getCertificate ` + -ArgumentList $CertificateThumbprint + + $encryptedPassword = $null + + if ($cert) + { + # Cast the public key correctly + $rsaProvider = [System.Security.Cryptography.RSACryptoServiceProvider] $cert.PublicKey.Key + + if ($rsaProvider -eq $null) + { + $errorMessage = "Could not get public key from certificate with thumbprint: $CertificateThumbprint . Please verify certificate is valid for encryption." + Invoke-Command ` + -ScriptBlock $throwTerminatingError ` + -ArgumentList "DecryptionCertificateNotFound", $errorMessage, "InvalidOperation" + } + + # Convert to a byte array + $keybytes = [System.Text.Encoding]::UNICODE.GetBytes($value) + + # Add a null terminator to the byte array + $keybytes += 0 + $keybytes += 0 + + # Encrypt using the public key + $encbytes = $rsaProvider.Encrypt($keybytes, $false) + + # Return a string + $encryptedPassword = [Convert]::ToBase64String($encbytes) + } + else + { + $errorMessage = "Could not find certificate which matches thumbprint: $CertificateThumbprint . Could not encrypt password" + Invoke-Command ` + -ScriptBlock $throwTerminatingError ` + -ArgumentList "EncryptionCertificateNot", $errorMessage, "InvalidOperation" + } + + return $encryptedPassword +} + +# Retrieves certificate by thumbprint +[System.Management.Automation.ScriptBlock] $getCertificate = { + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $CertificateThumbprint + ) + + $cert = $null + + foreach ($certIndex in (Get-Childitem -Path Cert:\LocalMachine\My)) + { + if ($certIndex.Thumbprint -match $CertificateThumbprint) + { + $cert = $certIndex + break + } + } + + if (-not $cert) + { + $errorMessage = "Error Reading certificate store for {0}. Please verify thumbprint is correct and certificate belongs to cert:\LocalMachine\My store." -f ${CertificateThumbprint}; + Invoke-Command ` + -ScriptBlock $throwTerminatingError ` + -ArgumentList "InvalidPathSpecified", $errorMessage, "InvalidOperation" + } + else + { + $cert + } +} + +# Throws terminating error specified errorCategory, errorId and errorMessage +[System.Management.Automation.ScriptBlock] $throwTerminatingError = { + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $ErrorId, + + [Parameter(Mandatory = $true)] + [System.String] + $ErrorMessage, + + [Parameter(Mandatory = $true)] + $ErrorCategory + ) + + $exception = New-Object -TypeName System.InvalidOperationException -ArgumentList $ErrorMessage + $errorRecord = New-Object -TypeName System.Management.Automation.ErrorRecord -ArgumentList ($exception, $ErrorId, $ErrorCategory, $null) + throw $errorRecord +} + +# Decrypts password using the defined private key +[System.Management.Automation.ScriptBlock] $getDecryptedPassword = { + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $Value, + + [Parameter(Mandatory = $true)] + [System.String] + $CertificateThumbprint + ) + + $cert = $null + + foreach ($certIndex in (Get-Childitem -Path Cert:\LocalMachine\My)) + { + if ($certIndex.Thumbprint -match $CertificateThumbprint) + { + $cert = $certIndex + break + } + } + + if (-not $cert) + { + $errorMessage = "Error Reading certificate store for {0}. Please verify thumbprint is correct and certificate belongs to cert:\LocalMachine\My store." -f ${CertificateThumbprint}; + $exception = New-Object -TypeName System.InvalidOperationException -ArgumentList $errorMessage + $errorRecord = New-Object -TypeName System.Management.Automation.ErrorRecord -ArgumentList ($exception, "InvalidPathSpecified", "InvalidOperation", $null) + throw $errorRecord + } + + $decryptedPassword = $null + + # Get RSA provider + $rsaProvider = [System.Security.Cryptography.RSACryptoServiceProvider] $cert.PrivateKey + + if ($rsaProvider -eq $null) + { + $errorMessage = "Could not get private key from certificate with thumbprint: $CertificateThumbprint . Please verify certificate is valid for decryption." + $exception = New-Object -TypeName System.InvalidOperationException -ArgumentList $errorMessage + $errorRecord = New-Object -TypeName System.Management.Automation.ErrorRecord -ArgumentList ($exception, "DecryptionCertificateNotFound", "InvalidOperation", $null) + throw $errorRecord + } + + # Convert to bytes array + $encBytes = [Convert]::FromBase64String($value) + + # Decrypt bytes + $decryptedBytes = $rsaProvider.Decrypt($encBytes, $false) + + # Convert to string + $decryptedPassword = [System.Text.Encoding]::Unicode.GetString($decryptedBytes) + + return $decryptedPassword +} diff --git a/deployment/dsc/azshcihost/xPSDesiredStateConfiguration/9.1.0/DSCResources/xGroupSet/xGroupSet.psd1 b/deployment/dsc/azshcihost/xPSDesiredStateConfiguration/9.1.0/DSCResources/xGroupSet/xGroupSet.psd1 new file mode 100644 index 0000000..4cbe3bc --- /dev/null +++ b/deployment/dsc/azshcihost/xPSDesiredStateConfiguration/9.1.0/DSCResources/xGroupSet/xGroupSet.psd1 @@ -0,0 +1,25 @@ +@{ + # Script module or binary module file associated with this manifest. + RootModule = 'xGroupSet.schema.psm1' + + # Version number of this module. + ModuleVersion = '1.0.0.0' + + # ID used to uniquely identify this module + GUID = 'c5e227b5-52dc-4653-b08f-6d94e06bb90b' + + # Author of this module + Author = 'DSC Community' + + # Company or vendor of this module + CompanyName = 'DSC Community' + + # Copyright statement for this module + Copyright = 'Copyright the DSC Community contributors. All rights reserved.' + + # Description of the functionality provided by this module + Description = 'Configures multiple xGroup resources with common settings but different names.' + + # Minimum version of the Windows PowerShell engine required by this module + PowerShellVersion = '4.0' +} diff --git a/deployment/dsc/azshcihost/xPSDesiredStateConfiguration/9.1.0/DSCResources/xGroupSet/xGroupSet.schema.psm1 b/deployment/dsc/azshcihost/xPSDesiredStateConfiguration/9.1.0/DSCResources/xGroupSet/xGroupSet.schema.psm1 new file mode 100644 index 0000000..61f23e7 --- /dev/null +++ b/deployment/dsc/azshcihost/xPSDesiredStateConfiguration/9.1.0/DSCResources/xGroupSet/xGroupSet.schema.psm1 @@ -0,0 +1,74 @@ +$errorActionPreference = 'Stop' +Set-StrictMode -Version 'Latest' + +$modulePath = Join-Path -Path (Split-Path -Path (Split-Path -Path $PSScriptRoot -Parent) -Parent) -ChildPath 'Modules' + +# Import the shared modules +Import-Module -Name (Join-Path -Path $modulePath ` + -ChildPath (Join-Path -Path 'xPSDesiredStateConfiguration.Common' ` + -ChildPath 'xPSDesiredStateConfiguration.Common.psm1')) + +<# + .SYNOPSIS + A composite DSC resource to configure a set of similar xGroup resources. + + .PARAMETER GroupName + An array of the names of the groups to configure. + + .PARAMETER Ensure + Specifies whether or not the set of groups should exist. + + Set this property to Present to create or modify a set of groups. + Set this property to Absent to remove a set of groups. + + .PARAMETER MembersToInclude + The members that should be included in each group in the set. + + .PARAMETER MembersToExclude + The members that should be excluded from each group in the set. + + .PARAMETER Credential + The credential to resolve all groups and user accounts. +#> +configuration xGroupSet +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String[]] + $GroupName, + + [Parameter()] + [ValidateSet('Present', 'Absent')] + [System.String] + $Ensure, + + [Parameter()] + [System.String[]] + $MembersToInclude, + + [Parameter()] + [System.String[]] + $MembersToExclude, + + [Parameter()] + [ValidateNotNullOrEmpty()] + [System.Management.Automation.PSCredential] + [System.Management.Automation.Credential()] + $Credential + ) + + $newResourceSetConfigurationParams = @{ + ResourceName = 'xGroup' + ModuleName = 'xPSDesiredStateConfiguration' + KeyParameterName = 'GroupName' + Parameters = $PSBoundParameters + } + + $configurationScriptBlock = New-ResourceSetConfigurationScriptBlock @newResourceSetConfigurationParams + + # This script block must be run directly in this configuration in order to resolve variables + . $configurationScriptBlock +} diff --git a/deployment/dsc/azshcihost/xPSDesiredStateConfiguration/9.1.0/DSCResources/xProcessSet/xProcessSet.psd1 b/deployment/dsc/azshcihost/xPSDesiredStateConfiguration/9.1.0/DSCResources/xProcessSet/xProcessSet.psd1 new file mode 100644 index 0000000..2d83e58 --- /dev/null +++ b/deployment/dsc/azshcihost/xPSDesiredStateConfiguration/9.1.0/DSCResources/xProcessSet/xProcessSet.psd1 @@ -0,0 +1,25 @@ +@{ + # Script module or binary module file associated with this manifest. + RootModule = 'xProcessSet.schema.psm1' + + # Version number of this module. + ModuleVersion = '1.0.0.0' + + # ID used to uniquely identify this module + GUID = '0cb71def-366f-4f3b-88a9-b9b37d266dd6' + + # Author of this module + Author = 'DSC Community' + + # Company or vendor of this module + CompanyName = 'DSC Community' + + # Copyright statement for this module + Copyright = 'Copyright the DSC Community contributors. All rights reserved.' + + # Description of the functionality provided by this module + Description = 'Provides a mechanism to configure and manage multiple xWindowsProcess resources on a target node.' + + # Minimum version of the Windows PowerShell engine required by this module + PowerShellVersion = '4.0' +} diff --git a/deployment/dsc/azshcihost/xPSDesiredStateConfiguration/9.1.0/DSCResources/xProcessSet/xProcessSet.schema.psm1 b/deployment/dsc/azshcihost/xPSDesiredStateConfiguration/9.1.0/DSCResources/xProcessSet/xProcessSet.schema.psm1 new file mode 100644 index 0000000..1a3516d --- /dev/null +++ b/deployment/dsc/azshcihost/xPSDesiredStateConfiguration/9.1.0/DSCResources/xProcessSet/xProcessSet.schema.psm1 @@ -0,0 +1,104 @@ +$errorActionPreference = 'Stop' +Set-StrictMode -Version 'Latest' + +$modulePath = Join-Path -Path (Split-Path -Path (Split-Path -Path $PSScriptRoot -Parent) -Parent) -ChildPath 'Modules' + +# Import the shared modules +Import-Module -Name (Join-Path -Path $modulePath ` + -ChildPath (Join-Path -Path 'xPSDesiredStateConfiguration.Common' ` + -ChildPath 'xPSDesiredStateConfiguration.Common.psm1')) + +<# + .SYNOPSIS + A composite DSC resource to configure a set of similar xWindowsProcess resources. + No arguments can be passed into these xWindowsProcess resources. + + .PARAMETER Path + The file paths to the executables of the processes to start or stop. Only the names of the + files may be specified if they are all accessible through the environment path. Relative + paths are not supported. + + .PARAMETER Ensure + Specifies whether or not the processes should exist. + + To start processes, set this property to Present. + To stop processes, set this property to Absent. + + .PARAMETER Credential + The credential of the user account to start the processes under. + + .PARAMETER StandardOutputPath + The file path to write the standard output to. Any existing file at this path + will be overwritten.This property cannot be specified at the same time as Credential + when running the processes as a local user. + + .PARAMETER StandardErrorPath + The file path to write the standard error output to. Any existing file at this path + will be overwritten. + + .PARAMETER StandardInputPath + The file path to get standard input from. This property cannot be specified at the + same time as Credential when running the processes as a local user. + + .PARAMETER WorkingDirectory + The file path to use as the working directory for the processes. Any existing file + at this path will be overwritten. This property cannot be specified at the same time + as Credential when running the processes as a local user. +#> +configuration xProcessSet +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String[]] + $Path, + + [Parameter()] + [ValidateSet('Present', 'Absent')] + [System.String] + $Ensure, + + [Parameter()] + [ValidateNotNullOrEmpty()] + [System.Management.Automation.PSCredential] + [System.Management.Automation.Credential()] + $Credential, + + [Parameter()] + [ValidateNotNullOrEmpty()] + [System.String] + $StandardOutputPath, + + [Parameter()] + [ValidateNotNullOrEmpty()] + [System.String] + $StandardErrorPath, + + [Parameter()] + [ValidateNotNullOrEmpty()] + [System.String] + $StandardInputPath, + + [Parameter()] + [ValidateNotNullOrEmpty()] + [System.String] + $WorkingDirectory + ) + + $newResourceSetConfigurationParams = @{ + ResourceName = 'xWindowsProcess' + ModuleName = 'xPSDesiredStateConfiguration' + KeyParameterName = 'Path' + Parameters = $PSBoundParameters + } + + # Arguments is a key parameter in xWindowsProcess resource. Adding it as a common parameter with an empty value string + $newResourceSetConfigurationParams['Parameters']['Arguments'] = '' + + $configurationScriptBlock = New-ResourceSetConfigurationScriptBlock @newResourceSetConfigurationParams + + # This script block must be run directly in this configuration in order to resolve variables + . $configurationScriptBlock +} diff --git a/deployment/dsc/azshcihost/xPSDesiredStateConfiguration/9.1.0/DSCResources/xServiceSet/xServiceSet.psd1 b/deployment/dsc/azshcihost/xPSDesiredStateConfiguration/9.1.0/DSCResources/xServiceSet/xServiceSet.psd1 new file mode 100644 index 0000000..03da2aa --- /dev/null +++ b/deployment/dsc/azshcihost/xPSDesiredStateConfiguration/9.1.0/DSCResources/xServiceSet/xServiceSet.psd1 @@ -0,0 +1,25 @@ +@{ + # Script module or binary module file associated with this manifest. + RootModule = 'xServiceSet.schema.psm1' + + # Version number of this module. + ModuleVersion = '1.0.0.0' + + # ID used to uniquely identify this module + GUID = 'c3ac5e1f-c1fd-4ed0-be24-b271c7062484' + + # Author of this module + Author = 'DSC Community' + + # Company or vendor of this module + CompanyName = 'DSC Community' + + # Copyright statement for this module + Copyright = 'Copyright the DSC Community contributors. All rights reserved.' + + # Description of the functionality provided by this module + Description = 'Allows starting, stopping and change in state or account type for a group of services.' + + # Minimum version of the Windows PowerShell engine required by this module + PowerShellVersion = '4.0' +} diff --git a/deployment/dsc/azshcihost/xPSDesiredStateConfiguration/9.1.0/DSCResources/xServiceSet/xServiceSet.schema.psm1 b/deployment/dsc/azshcihost/xPSDesiredStateConfiguration/9.1.0/DSCResources/xServiceSet/xServiceSet.schema.psm1 new file mode 100644 index 0000000..433a457 --- /dev/null +++ b/deployment/dsc/azshcihost/xPSDesiredStateConfiguration/9.1.0/DSCResources/xServiceSet/xServiceSet.schema.psm1 @@ -0,0 +1,96 @@ +$errorActionPreference = 'Stop' +Set-StrictMode -Version 'Latest' + +$modulePath = Join-Path -Path (Split-Path -Path (Split-Path -Path $PSScriptRoot -Parent) -Parent) -ChildPath 'Modules' + +# Import the shared modules +Import-Module -Name (Join-Path -Path $modulePath ` + -ChildPath (Join-Path -Path 'xPSDesiredStateConfiguration.Common' ` + -ChildPath 'xPSDesiredStateConfiguration.Common.psm1')) + +<# + .SYNOPSIS + A composite DSC resource to configure a set of similar xService resources. + + .PARAMETER Name + An array of the names of the services to configure. + + .PARAMETER Ensure + Specifies whether or not the set of services should exist. + + Set this property to Present to modify a set of services. + Set this property to Absent to remove a set of services. + + .PARAMETER StartupType + The startup type each service in the set should have. + + .PARAMETER BuiltInAccount + The built-in account each service in the set should start under. + + Cannot be specified at the same time as Credential. + + The user account specified by this property must have access to the service + executable paths in order to start the services. + + .PARAMETER State + The state each service in the set should be in. + From the default value defined in xService, the default will be Running. + + .PARAMETER Credential + The credential of the user account each service in the set should start under. + + Cannot be specified at the same time as BuiltInAccount. + + The user specified by this credential will automatically be granted the Log on as a Service + right. The user account specified by this property must have access to the service + executable paths in order to start the services. +#> +configuration xServiceSet +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String[]] + $Name, + + [Parameter()] + [ValidateSet('Present', 'Absent')] + [System.String] + $Ensure, + + [Parameter()] + [ValidateSet('Automatic', 'Manual', 'Disabled')] + [System.String] + $StartupType, + + [Parameter()] + [ValidateSet('LocalSystem', 'LocalService', 'NetworkService')] + [System.String] + $BuiltInAccount, + + [Parameter()] + [ValidateSet('Running', 'Stopped', 'Ignore')] + [System.String] + $State, + + [Parameter()] + [ValidateNotNull()] + [System.Management.Automation.PSCredential] + [System.Management.Automation.Credential()] + $Credential + ) + + $newResourceSetConfigurationParams = @{ + ResourceName = 'xService' + ModuleName = 'xPSDesiredStateConfiguration' + KeyParameterName = 'Name' + Parameters = $PSBoundParameters + } + + $configurationScriptBlock = New-ResourceSetConfigurationScriptBlock @newResourceSetConfigurationParams + + # This script block must be run directly in this configuration in order to resolve variables + . $configurationScriptBlock +} diff --git a/deployment/dsc/azshcihost/xPSDesiredStateConfiguration/9.1.0/DSCResources/xWindowsFeatureSet/xWindowsFeatureSet.psd1 b/deployment/dsc/azshcihost/xPSDesiredStateConfiguration/9.1.0/DSCResources/xWindowsFeatureSet/xWindowsFeatureSet.psd1 new file mode 100644 index 0000000..0c8d3d7 --- /dev/null +++ b/deployment/dsc/azshcihost/xPSDesiredStateConfiguration/9.1.0/DSCResources/xWindowsFeatureSet/xWindowsFeatureSet.psd1 @@ -0,0 +1,25 @@ +@{ + # Script module or binary module file associated with this manifest. + RootModule = 'xWindowsFeatureSet.schema.psm1' + + # Version number of this module. + ModuleVersion = '1.0.0.0' + + # ID used to uniquely identify this module + GUID = 'b18a27e2-f710-4a4a-92b8-6cd076970eb2' + + # Author of this module + Author = 'DSC Community' + + # Company or vendor of this module + CompanyName = 'DSC Community' + + # Copyright statement for this module + Copyright = 'Copyright the DSC Community contributors. All rights reserved.' + + # Description of the functionality provided by this module + Description = 'Provides a mechanism to configure and manage multiple xWindowsFeature resources on a target node.' + + # Minimum version of the Windows PowerShell engine required by this module + PowerShellVersion = '4.0' +} diff --git a/deployment/dsc/azshcihost/xPSDesiredStateConfiguration/9.1.0/DSCResources/xWindowsFeatureSet/xWindowsFeatureSet.schema.psm1 b/deployment/dsc/azshcihost/xPSDesiredStateConfiguration/9.1.0/DSCResources/xWindowsFeatureSet/xWindowsFeatureSet.schema.psm1 new file mode 100644 index 0000000..4a218b9 --- /dev/null +++ b/deployment/dsc/azshcihost/xPSDesiredStateConfiguration/9.1.0/DSCResources/xWindowsFeatureSet/xWindowsFeatureSet.schema.psm1 @@ -0,0 +1,85 @@ +$errorActionPreference = 'Stop' +Set-StrictMode -Version 'Latest' + +$modulePath = Join-Path -Path (Split-Path -Path (Split-Path -Path $PSScriptRoot -Parent) -Parent) -ChildPath 'Modules' + +# Import the shared modules +Import-Module -Name (Join-Path -Path $modulePath ` + -ChildPath (Join-Path -Path 'xPSDesiredStateConfiguration.Common' ` + -ChildPath 'xPSDesiredStateConfiguration.Common.psm1')) + +<# + .SYNOPSIS + A composite DSC resource to configure a set of similar xWindowsFeature resources. + + .PARAMETER Name + The name of the roles or features to install or uninstall. + + .PARAMETER Ensure + Specifies whether the roles or features should be installed or uninstalled. + + To install the features, set this property to Present. + To uninstall the features, set this property to Absent. + + .PARAMETER IncludeAllSubFeature + Specifies whether or not all subfeatures should be installed or uninstalled alongside the specified roles or features. + + If this property is true and Ensure is set to Present, all subfeatures will be installed. + If this property is false and Ensure is set to Present, subfeatures will not be installed or uninstalled. + If Ensure is set to Absent, all subfeatures will be uninstalled. + + .PARAMETER Credential + The credential of the user account under which to install or uninstall the roles or features. + + .PARAMETER LogPath + The custom file path to which to log this operation. + If not passed in, the default log path will be used (%windir%\logs\ServerManager.log). +#> +configuration xWindowsFeatureSet +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String[]] + $Name, + + [Parameter()] + [ValidateSet('Present', 'Absent')] + [System.String] + $Ensure, + + [Parameter()] + [ValidateNotNullOrEmpty()] + [System.String] + $Source, + + [Parameter()] + [System.Boolean] + $IncludeAllSubFeature, + + [Parameter()] + [ValidateNotNull()] + [System.Management.Automation.PSCredential] + [System.Management.Automation.Credential()] + $Credential, + + [Parameter()] + [ValidateNotNullOrEmpty()] + [System.String] + $LogPath + ) + + $newResourceSetConfigurationParams = @{ + ResourceName = 'xWindowsFeature' + ModuleName = 'xPSDesiredStateConfiguration' + KeyParameterName = 'Name' + Parameters = $PSBoundParameters + } + + $configurationScriptBlock = New-ResourceSetConfigurationScriptBlock @newResourceSetConfigurationParams + + # This script block must be run directly in this configuration in order to resolve variables + . $configurationScriptBlock +} diff --git a/deployment/dsc/azshcihost/xPSDesiredStateConfiguration/9.1.0/DSCResources/xWindowsOptionalFeatureSet/xWindowsOptionalFeatureSet.psd1 b/deployment/dsc/azshcihost/xPSDesiredStateConfiguration/9.1.0/DSCResources/xWindowsOptionalFeatureSet/xWindowsOptionalFeatureSet.psd1 new file mode 100644 index 0000000..ab73a68 --- /dev/null +++ b/deployment/dsc/azshcihost/xPSDesiredStateConfiguration/9.1.0/DSCResources/xWindowsOptionalFeatureSet/xWindowsOptionalFeatureSet.psd1 @@ -0,0 +1,25 @@ +@{ + # Script module or binary module file associated with this manifest. + RootModule = 'xWindowsOptionalFeatureSet.schema.psm1' + + # Version number of this module. + ModuleVersion = '1.0.0.0' + + # ID used to uniquely identify this module + GUID = 'a88c1458-db46-402c-947b-7d43ab57e27a' + + # Author of this module + Author = 'DSC Community' + + # Company or vendor of this module + CompanyName = 'DSC Community' + + # Copyright statement for this module + Copyright = 'Copyright the DSC Community contributors. All rights reserved.' + + # Description of the functionality provided by this module + Description = 'Provides a mechanism to configure and manage multiple xWindowsOptionalFeature resources on a target node.' + + # Minimum version of the Windows PowerShell engine required by this module + PowerShellVersion = '4.0' +} diff --git a/deployment/dsc/azshcihost/xPSDesiredStateConfiguration/9.1.0/DSCResources/xWindowsOptionalFeatureSet/xWindowsOptionalFeatureSet.schema.psm1 b/deployment/dsc/azshcihost/xPSDesiredStateConfiguration/9.1.0/DSCResources/xWindowsOptionalFeatureSet/xWindowsOptionalFeatureSet.schema.psm1 new file mode 100644 index 0000000..c6b8d91 --- /dev/null +++ b/deployment/dsc/azshcihost/xPSDesiredStateConfiguration/9.1.0/DSCResources/xWindowsOptionalFeatureSet/xWindowsOptionalFeatureSet.schema.psm1 @@ -0,0 +1,83 @@ +$errorActionPreference = 'Stop' +Set-StrictMode -Version 'Latest' + +$modulePath = Join-Path -Path (Split-Path -Path (Split-Path -Path $PSScriptRoot -Parent) -Parent) -ChildPath 'Modules' + +# Import the shared modules +Import-Module -Name (Join-Path -Path $modulePath ` + -ChildPath (Join-Path -Path 'xPSDesiredStateConfiguration.Common' ` + -ChildPath 'xPSDesiredStateConfiguration.Common.psm1')) + +<# + .SYNOPSIS + A composite DSC resource to configure a set of similar xWindowsOptionalFeature resources. + + .PARAMETER Name + The names of the Windows optional features to enable or disable. + + .PARAMETER Ensure + Specifies whether the features should be enabled or disabled. + + To enable a set of features, set this property to Present. + To disable a set of features, set this property to Absent. + + .PARAMETER RemoveFilesOnDisable + Specifies whether or not to remove all files associated with the features when they are + disabled. + + .PARAMETER NoWindowsUpdateCheck + Specifies whether or not DISM should contact Windows Update (WU) when searching for the + source files to restore Windows optional features on an online image. + + .PARAMETER LogPath + The file path to which to log the opertation. + + .PARAMETER LogLevel + The level of detail to include in the log. +#> +configuration xWindowsOptionalFeatureSet +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String[]] + $Name, + + [Parameter(Mandatory = $true)] + [ValidateSet('Present', 'Absent')] + [System.String] + $Ensure, + + [Parameter()] + [System.Boolean] + $RemoveFilesOnDisable, + + [Parameter()] + [System.Boolean] + $NoWindowsUpdateCheck, + + [Parameter()] + [ValidateNotNullOrEmpty()] + [System.String] + $LogPath, + + [Parameter()] + [ValidateSet('ErrorsOnly', 'ErrorsAndWarning', 'ErrorsAndWarningAndInformation')] + [System.String] + $LogLevel + ) + + $newResourceSetConfigurationParams = @{ + ResourceName = 'xWindowsOptionalFeature' + ModuleName = 'xPSDesiredStateConfiguration' + KeyParameterName = 'Name' + Parameters = $PSBoundParameters + } + + $configurationScriptBlock = New-ResourceSetConfigurationScriptBlock @newResourceSetConfigurationParams + + # This script block must be run directly in this configuration in order to resolve variables + . $configurationScriptBlock +} diff --git a/deployment/dsc/azshcihost/xPSDesiredStateConfiguration/9.1.0/Modules/DscPullServerSetup/DscPullServerSetup.psm1 b/deployment/dsc/azshcihost/xPSDesiredStateConfiguration/9.1.0/Modules/DscPullServerSetup/DscPullServerSetup.psm1 new file mode 100644 index 0000000..7ba5a95 --- /dev/null +++ b/deployment/dsc/azshcihost/xPSDesiredStateConfiguration/9.1.0/Modules/DscPullServerSetup/DscPullServerSetup.psm1 @@ -0,0 +1,492 @@ +$errorActionPreference = 'Stop' +Set-StrictMode -Version 'Latest' + +$modulePath = Split-Path -Path $PSScriptRoot -Parent + +# Import the Networking Resource Helper Module +Import-Module -Name (Join-Path -Path $modulePath ` + -ChildPath (Join-Path -Path 'xPSDesiredStateConfiguration.Common' ` + -ChildPath 'xPSDesiredStateConfiguration.Common.psm1')) + +# Import Localization Strings +$script:localizedData = Get-LocalizedData ` + -ResourceName 'DscPullServerSetup' ` + -ScriptRoot $PSScriptRoot + +<# + .SYNOPSIS + Package DSC modules and mof configuration document and publish them on an enterprise DSC pull server in the required format. + + .DESCRIPTION + Uses Publish-DscModulesAndMof function to package DSC modules into zip files with the version info. + Publishes the zip modules on "$env:ProgramFiles\WindowsPowerShell\DscService\Modules". + Publishes all mof configuration documents that are present in the $Source folder on "$env:ProgramFiles\WindowsPowerShell\DscService\Configuration"- + Use $Force to overwrite the version of the module that exists in the PowerShell module path with the version from the $source folder. + Use $ModuleNameList to specify the names of the modules to be published if the modules do not exist in $Source folder. + + .PARAMETER Source + The folder that contains the configuration mof documents and modules to be published on Pull server. + Everything in this folder will be packaged and published. + + .PARAMETER Force + Switch to overwrite the module in PSModulePath with the version provided in $Sources. + + .PARAMETER ModuleNameList + Package and publish the modules listed in $ModuleNameList based on PowerShell module path content. + + .EXAMPLE + $ModuleList = @("xWebAdministration", "xPhp") + Publish-DscModuleAndMof -Source C:\LocalDepot -ModuleNameList $ModuleList + + .EXAMPLE + Publish-DscModuleAndMof -Source C:\LocalDepot -Force +#> +function Publish-DscModuleAndMof +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [ValidateScript({ + Test-Path -Path $_ -PathType Container + })] + [System.String] + $Source, + + [Parameter()] + [System.Management.Automation.SwitchParameter] + $Force, + + [Parameter()] + [System.String[]] + $ModuleNameList + ) + + # Create working directory + $tempFolder = Join-Path -Path $Source -ChildPath 'temp' + New-Item -Path $tempFolder -ItemType Directory -Force -ErrorAction SilentlyContinue + + # Copy the mof documents from the $Source to working dir + $mofPath = Join-Path -Path $Source -ChildPath '*.mof' + Copy-Item -Path $mofPath -Destination $tempFolder -Force + + # Start Deployment! + Write-LogEntry -Scope $MyInvocation -Message $script:localizedData.StartDeploymentMessage + New-ZipFromPSModulePath -ListModuleNames $ModuleNameList -Destination $tempFolder + New-ZipFromSource -Source $Source -Destination $tempFolder + + # Generate the checkSum file for all the zip and mof files. + New-DSCCheckSum -Path $tempFolder -Force + + # Publish mof and modules to pull server repositories + Publish-ModulesAndChecksum -Source $tempFolder + Publish-MofsInSource -Source $tempFolder + + # Deployment is complete! + Remove-Item -Path $tempFolder -Recurse -Force -ErrorAction SilentlyContinue + Write-LogEntry -Scope $MyInvocation -Message $script:localizedData.EndDeploymentMessage +} + +<# + .SYNOPSIS + Creates a zip archive containing all the modules whose module name was assigned to the parameter ListModuleNames. + The zip archive is created in the path assigned to the parameter Destination. + + .PARAMETER ListModuleNames + List of Modules to package + + .PARAMETER Destination + Destionation path to copy packaged modules to +#> +function New-ZipFromPSModulePath +{ + [CmdletBinding()] + param + ( + [Parameter()] + [System.String[]] + $ListModuleNames, + + [Parameter()] + [ValidateScript({Test-Path -Path $_ -PathType Container})] + [System.String] + $Destination + ) + + # Move all required modules from powershell module path to a temp folder and package them + if ([System.String]::IsNullOrEmpty($ListModuleNames)) + { + Write-LogEntry -Scope $MyInvocation -Message $script:localizedData.NoAdditionalModulesPackagedMessage + } + + foreach ($module in $ListModuleNames) + { + $allVersions = Get-Module -Name $module -ListAvailable + + # Package all versions of the module + foreach ($moduleVersion in $allVersions) + { + $name = $moduleVersion.Name + $source = "$Destination\$name" + + # Create package zip + $path = $moduleVersion.ModuleBase + $version = $moduleVersion.Version.ToString() + Write-LogEntry -Scope $MyInvocation -Message "Zipping $name ($version)" + Compress-Archive -Path "$path\*" -DestinationPath "$source.zip" -Force + $newName = "$Destination\$name" + '_' + "$version" + '.zip' + + # Rename the module folder to contain the version info. + if (Test-Path -Path $newName) + { + $null = Remove-Item -Path $newName -Recurse -Force + } + + $null = Rename-Item -Path "$source.zip" -NewName $newName -Force + } + } +} + +<# + .SYNOPSIS + Deploys all DSC resource modules in the path assigned to the parameter Source. The DSC resource modules are copied + to the path '$env:ProgramFiles\WindowsPowerShell\Modules', and also packaged into a zip archive that is saved to + the path assigned to the parameter Destination. + + .PARAMETER Source + Folder containing DSC Resource Modules to package + + .PARAMETER Destination + Destination path to copy zipped DSC Resources to +#> +function New-ZipFromSource +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [ValidateScript({Test-Path -Path $_ -PathType Container})] + [System.String] + $Source, + + [Parameter(Mandatory = $true)] + [ValidateScript({Test-Path -Path $_ -PathType Container})] + $Destination + ) + + # For each module under $Source folder create a zip package that has the same name as the folder. + $allModulesInSource = Get-ChildItem -Path $Source -Directory + $modules = @() + + foreach ($item in $allModulesInSource) + { + $name = $Item.Name + $alreadyExists = Get-Module -Name $name -ListAvailable + + if (($null -eq $alreadyExists) -or ($Force)) + { + # Install the modules into PowerShell module path and overwrite the content + Copy-Item -Path $item.FullName -Recurse -Force -Destination "$env:ProgramFiles\WindowsPowerShell\Modules" + } + else + { + Write-Warning -Message ($script:localizedData.SkippingModuleOverwriteMessage -f $name, $Source) + } + + $modules += @("$name") + } + + # Package the module in $destination + New-ZipFromPSModulePath -ListModuleNames $modules -Destination $Destination +} + +<# + .SYNOPSIS + Deploy modules to the Pull sever repository. + + .PARAMETER Source + Folder containing zipped DSC Resources to publish +#> +function Publish-ModulesAndChecksum +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [ValidateScript({Test-Path -Path $_ -PathType Container})] + [System.String] + $Source + ) + + # Check if the current machine is a server sku. + $moduleRepository = "$env:ProgramFiles\WindowsPowerShell\DscService\Modules" + + if ((Get-Module -Name ServerManager -ListAvailable) -and (Test-Path -Path $moduleRepository)) + { + Write-LogEntry -Scope $MyInvocation -Message ($script:localizedData.CopyingModulesAndChecksumsMessage -f $moduleRepository) + $zipPath = Join-Path -Path $Source -ChildPath '*.zip*' + Copy-Item -Path $zipPath -Destination $moduleRepository -Force + } + else + { + Write-Warning -Message $script:localizedData.CopyingModulesToPullServerMessage + } +} + +<# + .SYNOPSIS + Deploy configurations and their checksums. + + .PARAMETER Source + Folder containing MOFs to publish +#> +function Publish-MofsInSource +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [ValidateScript({Test-Path -Path $_ -PathType Container})] + [System.String] + $Source + ) + + # Check if the current machine is a server sku. + $mofRepository = "$env:ProgramFiles\WindowsPowerShell\DscService\Configuration" + + if ((Get-Module -Name ServerManager -ListAvailable) -and (Test-Path -Path $mofRepository)) + { + Write-LogEntry -Scope $MyInvocation -Message ($script:localizedData.CopyingMOFsAndChecksumsMessage -f $mofRepository) + $mofPath = Join-Path -Path $Source -ChildPath '*.mof*' + Copy-Item -Path $mofPath -Destination $mofRepository -Force + } + else + { + Write-Warning -Message $script:localizedData.CopyingConfigurationsToPullServerMessage + } +} + +<# + .SYNOPSIS + Writes a version message with the current time, caller, and message. +#> +function Write-LogEntry +{ + [CmdletBinding()] + param + ( + [Parameter()] + [System.DateTime] + $Date = $(Get-Date), + + [Parameter(Mandatory = $true)] + [System.Object] + $Scope, + + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $Message + ) + + Write-Verbose -Message "$Date [$($Scope.MyCommand)] :: $Message" +} + +<# + .SYNOPSIS + Deploy DSC modules to the pullserver. + + .DESCRIPTION + Publish DSC module using Module Info object as an input. + The cmdlet will figure out the location of the module repository using web.config of the pullserver. + + .PARAMETER Name + Name of the module. + + .PARAMETER ModuleBase + This is the location of the base of the module. + + .PARAMETER Version + This is the version of the module + + .PARAMETER PullServerWebConfig + Path to the Pull Server web.config file, i.e. + "$env:SystemDrive\inetpub\wwwroot\PSDSCPullServer\web.config" + + .PARAMETER OutputFolderPath + Path to the Location where the MOF files should be published. + This should be used when the PullServer is a SMB share pull server. + (https://docs.microsoft.com/nl-nl/powershell/dsc/pull-server/pullserversmb) + Defaults to $null + + .EXAMPLE + Get-Module <ModuleName> | Publish-ModuleToPullServer + + .EXAMPLE + Get-Module <ModuleName> | Publish-ModuleToPullServer -OutputFolderPath "\\Server01\DscService\Module" +#> +function Publish-ModuleToPullServer +{ + [CmdletBinding()] + [OutputType([System.Void])] + param + ( + [Parameter(Mandatory = $true, + ValueFromPipelineByPropertyName = $true, + Position = 0)] + [System.String] + $Name, + + [Parameter(Mandatory = $true, + ValueFromPipelineByPropertyName = $true, + Position = 1)] + [System.String] + $ModuleBase, + + [Parameter(Mandatory = $true, + ValueFromPipelineByPropertyName = $true, + Position = 2)] + [Version] + $Version, + + [Parameter(Mandatory = $true)] + [ValidateScript({Test-Path -Path $_ -PathType Leaf})] + [System.String] + $PullServerWebConfig, + + [Parameter()] + [System.String] + $OutputFolderPath = $null + ) + + begin + { + if (-not($OutputFolderPath) -or -not (Test-Path -Path $OutputFolderPath)) + { + if (-not(Test-Path -Path $PullServerWebConfig)) + { + New-InvalidArgumentException ` + -Message ($script:localizedData.InvalidWebConfigPathError -f $PullServerWebConfig) ` + -ArgumentName 'PullServerWebConfig' + } + else + { + <# + Web.Config of Pull Server found so figure out the module path of the pullserver. + Use this value as output folder path. + #> + $webConfigXml = [System.Xml.XmlDocument] (Get-Content -Path $PullServerWebConfig) + $moduleXElement = $webConfigXml.SelectNodes("//appSettings/add[@key = 'ModulePath']") + $OutputFolderPath = $moduleXElement.Value + } + } + } + + process + { + Write-Verbose -Message ($script:localizedData.PublishModuleMessage -f $Name, $Version, $ModuleBase) + $targetPath = Join-Path -Path $OutputFolderPath -ChildPath "$($Name)_$($Version).zip" + + if (Test-Path -Path $targetPath) + { + Compress-Archive -DestinationPath $targetPath -Path "$($ModuleBase)\*" -Update + } + else + { + Compress-Archive -DestinationPath $targetPath -Path "$($ModuleBase)\*" + } + } + + end + { + # Now that all the modules are published generate their checksum. + New-DscChecksum -Path $OutputFolderPath + } +} + +<# + .SYNOPSIS + Deploy DSC Configuration document to the pullserver. + + .DESCRIPTION + Publish MOF file to the pullserver. It takes File Info object as + pipeline input. It also auto detects the location of the configuration + repository using the web.config of the pullserver. + + .PARAMETER FullName + MOF File Name + + .PARAMETER PullServerWebConfig + Path to the Pull Server web.config file, i.e. + "$env:SystemDrive\inetpub\wwwroot\PSDSCPullServer\web.config" + + .PARAMETER OutputFolderPath + Path to the Location where the MOF files should be published. + This should be used when the PullServer is a SMB share pull server. + (https://docs.microsoft.com/nl-nl/powershell/dsc/pull-server/pullserversmb) + Defaults to $null + + .EXAMPLE + Dir <path>\*.mof | Publish-MOFToPullServer + + .EXAMPLE + Dir <path>\*.mof | Publish-MOFToPullServer -OutputFolderPath "\\Server01\DscService\Configuration" +#> +function Publish-MofToPullServer +{ + [CmdletBinding()] + [OutputType([System.Void])] + param + ( + [Parameter(Mandatory = $true, + ValueFromPipelineByPropertyName = $true, + Position = 0)] + [ValidateScript({Test-Path -Path $_ -PathType Leaf})] + [System.String] + $FullName, + + [Parameter(Mandatory = $true)] + [ValidateScript({Test-Path -Path $_ -PathType Leaf})] + [System.String] + $PullServerWebConfig, + + [Parameter()] + [System.String] + $OutputFolderPath = $null + ) + + begin + { + if (-not($OutputFolderPath) -or -not (Test-Path -Path $OutputFolderPath)) + { + <# + Web.Config of Pull Server found so figure out the module path of the pullserver. + Use this value as output folder path. + #> + $webConfigXml = [System.Xml.XmlDocument] (Get-Content -Path $PullServerWebConfig) + $configXElement = $webConfigXml.SelectNodes("//appSettings/add[@key = 'ConfigurationPath']") + $OutputFolderPath = $configXElement.Value + } + } + + process + { + $fileItem = Get-Item -Path $FullName + + if ($fileItem.Extension -eq '.mof') + { + Copy-Item -Path $FullName -Destination $OutputFolderPath -Force + } + else + { + New-InvalidArgumentException ` + -Message ($script:localizedData.InvalidFileTypeError -f $FullName) ` + -ArgumentName 'FullName' + } + } + + end + { + New-DscChecksum -Path $OutputFolderPath -Force + } +} diff --git a/deployment/dsc/azshcihost/xPSDesiredStateConfiguration/9.1.0/Modules/DscPullServerSetup/DscPullServerSetupTest/DscPullServerSetupTest.ps1 b/deployment/dsc/azshcihost/xPSDesiredStateConfiguration/9.1.0/Modules/DscPullServerSetup/DscPullServerSetupTest/DscPullServerSetupTest.ps1 new file mode 100644 index 0000000..2d748b4 --- /dev/null +++ b/deployment/dsc/azshcihost/xPSDesiredStateConfiguration/9.1.0/Modules/DscPullServerSetup/DscPullServerSetupTest/DscPullServerSetupTest.ps1 @@ -0,0 +1,134 @@ +<# + * + * Once you setup your pull server with registration, run the following set of tests on the pull server machine + * to verify if the pullserver is setup properly and ready to go. + #> + +<# + * Prerequisites: + * You need Pester module to run this test. + * With PowerShell 5, use Install-Module Pester to install the module if it is not on pull server node. + * With older PowerShell, install PackageManagement extensions first. + #> + +<# + * Run the test via Invoke-Pester ./PullServerSetupTests.ps1 + * This test assumes default values are used during deployment for the location of web.config and pull server URL. + * If default values are not used during deployment , please update these values in the 'BeforeAll' block accordingly. + #> + +Describe PullServerInstallationTests { + BeforeAll { + # UPDATE THE PULLSERVER URL, If it is different from the default value. + $DscHostFQDN = [System.Net.Dns]::GetHostEntry([System.String] $env:computername).HostName + $script:dscPullServerURL = "https://$($DscHostFQDN):8080/PSDSCPullserver.svc" + + # UPDATE THE LOCATION OF WEB.CONFIG, if it is differnet from the default path. + $DscWebConfigChildPath = '\inetpub\wwwroot\psdscpullserver\web.config' + $DscWebConfigPath = Join-Path -Path $env:SystemDrive -ChildPath $DscWebConfigChildPath + + # Skip all tests if web.config is not found + if (-not (Test-Path -Path $DscWebConfigPath)){ + throw 'No pullserver web.config found.' + } + + # Get web.config content as XML + $DscWebConfigXML = [System.Xml.XmlDocument] (Get-Content -Path $DscWebConfigPath) + + # Registration Keys info. + $DscRegKeyName = 'RegistrationKeys.txt' + $DscRegKeyXMLNode = "//appSettings/add[@key = 'RegistrationKeyPath']" + $DscRegKeyParentPath = ($DscWebConfigXML.SelectSingleNode($DscRegKeyXMLNode)).Value + $DscRegKeyPath = Join-Path -Path $DscRegKeyParentPath -ChildPath $DscRegKeyName + $script:dscRegKey = Get-Content -Path $DscRegKeyPath + + # Configuration repository info. + $DscConfigPathXMLNode = "//appSettings/add[@key = 'ConfigurationPath']" + $DscConfigPath = ($DscWebConfigXML.SelectSingleNode($DscConfigPathXMLNode)).Value + + # Module repository info. + $DscModulePathXMLNode = "//appSettings/add[@key = 'ModulePath']" + $script:dscModulePath = ($DscWebConfigXML.SelectSingleNode($DscModulePathXMLNode)).Value + + # Testing Files/Variables + $DscTestMetaConfigName = 'PullServerSetupTestMetaConfig' + $script:dscTestMetaConfigPath = Join-Path -Path $PSScriptRoot -ChildPath $DscTestMetaConfigName + $DscTestConfigName = 'PullServerSetUpTest' + $script:dscTestMofPath = Join-Path -Path $DscConfigPath -ChildPath "$DscTestConfigName.mof" + } + + Context 'Verify general pull server functionality' { + It "$DscRegKeyPath exists" { + $DscRegKeyPath | Should -Exist + } + + It "Module repository $script:dscModulePath exists" { + $script:dscModulePath | Should -Exist + } + + It "Configuration repository $DscConfigPath exists" { + $DscConfigPath | Should -Exist + } + + It "Verify server $script:dscPullServerURL is up and running" { + $DscPullServerResponse = Invoke-WebRequest -Uri $script:dscPullServerURL -UseBasicParsing + $DscPullServerResponse.StatusCode | Should -Be 200 + } + } + + Context 'Verify pull end to end works' { + It 'Tests local configuration manager' { + [DscLocalConfigurationManager()] + Configuration $DscTestMetaConfigName + { + Settings + { + RefreshMode = 'PULL' + } + ConfigurationRepositoryWeb ConfigurationManager + { + ServerURL = $script:dscPullServerURL + RegistrationKey = $script:dscRegKey + ConfigurationNames = @($DscTestConfigName) + } + } + + PullServerSetupTestMetaConfig -OutputPath $script:dscTestMetaConfigPath + Set-DscLocalConfigurationManager -Path $script:dscTestMetaConfigPath -Verbose:$VerbosePreference -Force + + $DscLocalConfigNames = (Get-DscLocalConfigurationManager).ConfigurationDownloadManagers.ConfigurationNames + $DscLocalConfigNames -contains $DscTestConfigName | Should -BeTrue + } + + It "Creates mof and checksum files in $DscConfigPath" { + # Sample test configuration + Configuration NoOpConfig { + Import-DscResource -ModuleName PSDesiredStateConfiguration + Node ($DscTestConfigName) + { + Script script + { + GetScript = '@{}' + SetScript = '{}' + TestScript = { + if ($false) { return $true } else {return $false} + } + } + } + } + + # Create a mof file copy it to + NoOpConfig -OutputPath $DscConfigPath -Verbose:$VerbosePreference + $script:dscTestMofPath | Should -Exist + + # Create checksum + New-DscChecksum $DscConfigPath -Verbose:$VerbosePreference -Force + "$script:dscTestMofPath.checksum" | Should -Exist + } + + It 'Updates DscConfiguration Successfully' { + Update-DscConfiguration -Wait -Verbose:$VerbosePreference + (Get-DscConfiguration).ConfigurationName | Should -Be 'NoOpConfig' + } + } +} diff --git a/deployment/dsc/azshcihost/xPSDesiredStateConfiguration/9.1.0/Modules/DscPullServerSetup/README.md b/deployment/dsc/azshcihost/xPSDesiredStateConfiguration/9.1.0/Modules/DscPullServerSetup/README.md new file mode 100644 index 0000000..47bb609 --- /dev/null +++ b/deployment/dsc/azshcihost/xPSDesiredStateConfiguration/9.1.0/Modules/DscPullServerSetup/README.md @@ -0,0 +1,61 @@ +# DSCPullServerSetup + +The DSCPullServerSetup module contains utilities to automate DSC module and +configuration document packaging and deployment on an enterprise DSC Pull +Server, with examples. + +## Publish-DscModuleAndMof + +Use `Publish-DscModuleAndMof` cmdlet to package a module containing DSC Resources +that are present in `$Source` or in `$ModuleNameList` into zip files with version +info, then publish them with MOF configuration documents that are present +in `$Source` to the Pull server. + +- This publishes all the DSC Resources in `$env:ProgramFiles\WindowsPowerShell\DscService\Modules`. +- This publishes all the MOF configuration documents in `$env:ProgramFiles\WindowsPowerShell\DscService\Configuration`. +- Use `-Force` to force packaging the version that exists in $Source folder if + a different version of the module exists in the PowerShell module path. +- Use `-ModuleNameList` to specify the names of the modules to be published (all + versions if multiple versions of the module are installed) if no DSC module + present in local folder `$Source` + +```powershell +.EXAMPLE + Publish-DSCModuleAndMof -Source C:\LocalDepot + +.EXAMPLE + $moduleList = @("xWebAdministration", "xPhp") + Publish-DSCModuleAndMof -Source C:\LocalDepot -ModuleNameList $moduleList + +.EXAMPLE + Publish-DSCModuleAndMof -Source C:\LocalDepot -Force +``` + +## How to Configure Pull Server & SQL Server to enable new SQL backend provider feature in DSC + +- Install SQL Server on a clean OS +- On the SQL Server Machine: + - Create a Firewall rule according to this link : https://technet.microsoft.com/en-us/library/ms175043(v=sql.110).aspx + - Enable TCP/IP : + - Open "SQL Server Configuration Manager" + - Now Click on "SQL Server Network Configuration" and Click on "Protocols for Name" + - Right Click on "TCP/IP" (make sure it is Enabled) Click on Properties + - Now Select "IP Addresses" Tab -and- Go to the last entry "IP All" + - Enter "TCP Port" 1433. + - Now Restart "SQL Server .Name." using "services.msc" (winKey + r) + - Enable Remote Connections to the SQL Server + - Go to Server Properties + - Select Connections + - Under the Remote server connections - Click the check box next to "Allow remote connections to this server" + - Create a new User login (This is required as the engine will need this privilege to create the DSC DB and tables) + - Go to the Login Properties + - Select Server Roles - select "Public" and "Sysadmin" + - Select User Mapping - select "db_owner" and "public" + - On the Pull Server + - Update the Web.Config with the SQL server connection string + - Open: C:\inetpub\wwwroot\PSDSCPullServer\Web.config + - &lt;add key="dbprovider" value="System.Data.OleDb"/&gt; + - &lt;add key="dbconnectionstr" value="Provider=SQLNCLI11;Data Source=&lt;ServerName&gt;;Initial Catalog=master;User ID=sa;Password=&lt;sapassword&gt;;Initial Catalog=master;"/&gt; + - If SQL server was installed as a named Instance instead of default one then use + - &lt;add key="dbconnectionstr" value="Provider=SQLNCLI11;Data Source=&lt;ServerName\InstanceName&gt;;Initial Catalog=master;User ID=sa;Password=&lt;sapassword&gt;;Initial Catalog=master;"/&gt; + - Run iireset for the Pull server to pick up the new configuration. diff --git a/deployment/dsc/azshcihost/xPSDesiredStateConfiguration/9.1.0/Modules/DscPullServerSetup/en-US/DscPullServerSetup.strings.psd1 b/deployment/dsc/azshcihost/xPSDesiredStateConfiguration/9.1.0/Modules/DscPullServerSetup/en-US/DscPullServerSetup.strings.psd1 new file mode 100644 index 0000000..33fb44d --- /dev/null +++ b/deployment/dsc/azshcihost/xPSDesiredStateConfiguration/9.1.0/Modules/DscPullServerSetup/en-US/DscPullServerSetup.strings.psd1 @@ -0,0 +1,15 @@ +# Localized resources for DscPullServerSetup + +ConvertFrom-StringData @' + StartDeploymentMessage = Start deployment of Pull Server. + EndDeploymentMessage = End deployment of Pull Server. + NoAdditionalModulesPackagedMessage = No additional modules are specified to be packaged. + SkippingModuleOverwriteMessage = Skipping module overwrite. Module with the name '{0}' already exists.`r`nPlease specify -Force to overwrite the module with the local version of the module located in '{1}' or list names of the modules in ModuleNameList parameter to be packaged from PowerShell module path instead and remove them from '{1}' folder. + CopyingModulesToPullServerMessage = Copying modules to Pull server module repository skipped because the machine is not a server sku or Pull server endpoint is not deployed. + CopyingConfigurationsToPullServerMessage = Copying configuration(s) to Pull server configuration repository skipped because the machine is not a server sku or Pull server endpoint is not deployed. + CopyingModulesAndChecksumsMessage = Copying modules and checksums to '{0}'. + CopyingMOFsAndChecksumsMessage = Copying MOFs and checksums to '{0}'. + PublishModuleMessage = Publishing module '{0}' version '{1}' to '{2}'. + InvalidWebConfigPathError = Web.Config of the pullserver does not exist on the default path '{0}'. Please provide the location of your pullserver web configuration using the parameter -PullServerWebConfig or an alternate path where you want to publish the pullserver modules to. This path should exist. + InvalidFileTypeError = Invalid file '{0}'. Only MOF files can be copied to the Pull Server configuration repository. +'@ diff --git a/deployment/dsc/azshcihost/xPSDesiredStateConfiguration/9.1.0/Modules/xPSDesiredStateConfiguration.Common/en-US/xPSDesiredStateConfiguration.Common.strings.psd1 b/deployment/dsc/azshcihost/xPSDesiredStateConfiguration/9.1.0/Modules/xPSDesiredStateConfiguration.Common/en-US/xPSDesiredStateConfiguration.Common.strings.psd1 new file mode 100644 index 0000000..88f50c0 --- /dev/null +++ b/deployment/dsc/azshcihost/xPSDesiredStateConfiguration/9.1.0/Modules/xPSDesiredStateConfiguration.Common/en-US/xPSDesiredStateConfiguration.Common.strings.psd1 @@ -0,0 +1,11 @@ +# Localized resources for xPSDesiredStateConfiguration.Common + +ConvertFrom-StringData @' + PropertyTypeInvalidForDesiredValues = Property 'DesiredValues' must be either a [System.Collections.Hashtable], [CimInstance] or [PSBoundParametersDictionary]. The type detected was {0}. + PropertyTypeInvalidForValuesToCheck = If 'DesiredValues' is a CimInstance, then property 'ValuesToCheck' must contain a value. + PropertyValidationError = Expected to find an array value for property {0} in the current values, but it was either not present or was null. This has caused the test method to return false. + PropertiesDoesNotMatch = Found an array for property {0} in the current values, but this array does not match the desired state. Details of the changes are below. + PropertyThatDoesNotMatch = {0} - {1} + ValueOfTypeDoesNotMatch = {0} value for property {1} does not match. Current state is '{2}' and desired state is '{3}'. + UnableToCompareProperty = Unable to compare property {0} as the type {1} is not handled by the Test-DscParameterState cmdlet. +'@ diff --git a/deployment/dsc/azshcihost/xPSDesiredStateConfiguration/9.1.0/Modules/xPSDesiredStateConfiguration.Common/xPSDesiredStateConfiguration.Common.psd1 b/deployment/dsc/azshcihost/xPSDesiredStateConfiguration/9.1.0/Modules/xPSDesiredStateConfiguration.Common/xPSDesiredStateConfiguration.Common.psd1 new file mode 100644 index 0000000..0146666 --- /dev/null +++ b/deployment/dsc/azshcihost/xPSDesiredStateConfiguration/9.1.0/Modules/xPSDesiredStateConfiguration.Common/xPSDesiredStateConfiguration.Common.psd1 @@ -0,0 +1,69 @@ +@{ + # Version number of this module. + ModuleVersion = '0.0.1' + + # ID used to uniquely identify this module + GUID = 'b4768e4d-0786-4e9c-9866-6f6e5efc4d63' + + # Author of this module + Author = 'DSC Community' + + # Company or vendor of this module + CompanyName = 'DSC Community' + + # Copyright statement for this module + Copyright = 'Copyright the DSC Community contributors. All rights reserved.' + + # Description of the functionality provided by this module + Description = 'Functions used by the DSC resources in xPSDesiredStateConfiguration.' + + # Functions to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no functions to export. + FunctionsToExport = @( + 'Test-IsNanoServer', + 'Test-DscParameterState', + 'New-InvalidArgumentException', + 'New-InvalidDataException', + 'New-InvalidOperationException', + 'New-ObjectNotFoundException', + 'New-InvalidResultException', + 'New-NotImplementedException', + 'Get-LocalizedData', + 'Set-DscMachineRebootRequired', + 'New-ResourceSetConfigurationScriptBlock' + ) + + # Cmdlets to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no cmdlets to export. + CmdletsToExport = @() + + # Variables to export from this module + VariablesToExport = @( + 'DscWebServiceDefaultAppPoolName' + ) + + # Aliases to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no aliases to export. + AliasesToExport = @() + + # Private data to pass to the module specified in RootModule/ModuleToProcess. This may also contain a PSData hashtable with additional module metadata used by PowerShell. + PrivateData = @{ + + PSData = @{ + # Set to a prerelease string value if the release should be a prerelease. + Prerelease = '' + + # Tags applied to this module. These help with module discovery in online galleries. + Tags = @('DesiredStateConfiguration', 'DSC', 'DSCResourceKit', 'DSCResource') + + # A URL to the license for this module. + LicenseUri = 'https://github.com/dsccommunity/xPSDesiredStateConfiguration/blob/master/LICENSE' + + # A URL to the main website for this project. + ProjectUri = 'https://github.com/dsccommunity/xPSDesiredStateConfiguration' + + # A URL to an icon representing this module. + IconUri = 'https://dsccommunity.org/images/DSC_Logo_300p.png' + + # ReleaseNotes of this module + ReleaseNotes = '' + } # End of PSData hashtable + } # End of PrivateData hashtable +} diff --git a/deployment/dsc/azshcihost/xPSDesiredStateConfiguration/9.1.0/Modules/xPSDesiredStateConfiguration.Common/xPSDesiredStateConfiguration.Common.psm1 b/deployment/dsc/azshcihost/xPSDesiredStateConfiguration/9.1.0/Modules/xPSDesiredStateConfiguration.Common/xPSDesiredStateConfiguration.Common.psm1 new file mode 100644 index 0000000..5dc4407 --- /dev/null +++ b/deployment/dsc/azshcihost/xPSDesiredStateConfiguration/9.1.0/Modules/xPSDesiredStateConfiguration.Common/xPSDesiredStateConfiguration.Common.psm1 @@ -0,0 +1,828 @@ +<# + .SYNOPSIS + Tests if the current machine is a Nano server. +#> +function Test-IsNanoServer +{ + [OutputType([System.Boolean])] + [CmdletBinding()] + param () + + $isNanoServer = $false + + if (Test-CommandExists -Name 'Get-ComputerInfo') + { + $computerInfo = Get-ComputerInfo + + $computerIsServer = 'Server' -ieq $computerInfo.OsProductType + + if ($computerIsServer) + { + $isNanoServer = 'NanoServer' -ieq $computerInfo.OsServerLevel + } + } + + return $isNanoServer +} + +<# + .SYNOPSIS + Tests whether or not the command with the specified name exists. + .PARAMETER Name + The name of the command to test for. +#> +function Test-CommandExists +{ + [OutputType([System.Boolean])] + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $Name + ) + + $command = Get-Command -Name $Name -ErrorAction 'SilentlyContinue' + return ($null -ne $command) +} + +<# + .SYNOPSIS + This method is used to compare current and desired values for any DSC resource. + + .PARAMETER CurrentValues + This is hash table of the current values that are applied to the resource. + + .PARAMETER DesiredValues + This is a PSBoundParametersDictionary of the desired values for the resource. + + .PARAMETER ValuesToCheck + This is a list of which properties in the desired values list should be checked. + If this is empty then all values in DesiredValues are checked. +#> +function Test-DscParameterState +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [System.Collections.Hashtable] + $CurrentValues, + + [Parameter(Mandatory = $true)] + [System.Object] + $DesiredValues, + + [Parameter()] + [System.Array] + $ValuesToCheck + ) + + $returnValue = $true + + if (($DesiredValues.GetType().Name -ne 'HashTable') ` + -and ($DesiredValues.GetType().Name -ne 'CimInstance') ` + -and ($DesiredValues.GetType().Name -ne 'PSBoundParametersDictionary')) + { + $errorMessage = $script:localizedData.PropertyTypeInvalidForDesiredValues -f $($DesiredValues.GetType().Name) + New-InvalidArgumentException -ArgumentName 'DesiredValues' -Message $errorMessage + } + + if (($DesiredValues.GetType().Name -eq 'CimInstance') -and ($null -eq $ValuesToCheck)) + { + $errorMessage = $script:localizedData.PropertyTypeInvalidForValuesToCheck + New-InvalidArgumentException -ArgumentName 'ValuesToCheck' -Message $errorMessage + } + + if (($null -eq $ValuesToCheck) -or ($ValuesToCheck.Count -lt 1)) + { + $keyList = $DesiredValues.Keys + } + else + { + $keyList = $ValuesToCheck + } + + $keyList | ForEach-Object -Process { + if (($_ -ne 'Verbose')) + { + if (($CurrentValues.ContainsKey($_) -eq $false) ` + -or ($CurrentValues.$_ -ne $DesiredValues.$_) ` + -or (($DesiredValues.GetType().Name -ne 'CimInstance' -and $DesiredValues.ContainsKey($_) -eq $true) -and ($null -ne $DesiredValues.$_ -and $DesiredValues.$_.GetType().IsArray))) + { + if ($DesiredValues.GetType().Name -eq 'HashTable' -or ` + $DesiredValues.GetType().Name -eq 'PSBoundParametersDictionary') + { + $checkDesiredValue = $DesiredValues.ContainsKey($_) + } + else + { + # If DesiredValue is a CimInstance. + $checkDesiredValue = $false + if (([System.Boolean]($DesiredValues.PSObject.Properties.Name -contains $_)) -eq $true) + { + if ($null -ne $DesiredValues.$_) + { + $checkDesiredValue = $true + } + } + } + + if ($checkDesiredValue) + { + $desiredType = $DesiredValues.$_.GetType() + $fieldName = $_ + if ($desiredType.IsArray -eq $true) + { + if (($CurrentValues.ContainsKey($fieldName) -eq $false) ` + -or ($null -eq $CurrentValues.$fieldName)) + { + Write-Verbose -Message ($script:localizedData.PropertyValidationError -f $fieldName) -Verbose + + $returnValue = $false + } + else + { + $arrayCompare = Compare-Object -ReferenceObject $CurrentValues.$fieldName ` + -DifferenceObject $DesiredValues.$fieldName + if ($null -ne $arrayCompare) + { + Write-Verbose -Message ($script:localizedData.PropertiesDoesNotMatch -f $fieldName) -Verbose + + $arrayCompare | ForEach-Object -Process { + Write-Verbose -Message ($script:localizedData.PropertyThatDoesNotMatch -f $_.InputObject, $_.SideIndicator) -Verbose + } + + $returnValue = $false + } + } + } + else + { + switch ($desiredType.Name) + { + 'String' + { + if (-not [System.String]::IsNullOrEmpty($CurrentValues.$fieldName) -or ` + -not [System.String]::IsNullOrEmpty($DesiredValues.$fieldName)) + { + Write-Verbose -Message ($script:localizedData.ValueOfTypeDoesNotMatch ` + -f $desiredType.Name, $fieldName, $($CurrentValues.$fieldName), $($DesiredValues.$fieldName)) -Verbose + + $returnValue = $false + } + } + + 'Int32' + { + if (-not ($DesiredValues.$fieldName -eq 0) -or ` + -not ($null -eq $CurrentValues.$fieldName)) + { + Write-Verbose -Message ($script:localizedData.ValueOfTypeDoesNotMatch ` + -f $desiredType.Name, $fieldName, $($CurrentValues.$fieldName), $($DesiredValues.$fieldName)) -Verbose + + $returnValue = $false + } + } + + { $_ -eq 'Int16' -or $_ -eq 'UInt16' -or $_ -eq 'Single' } + { + if (-not ($DesiredValues.$fieldName -eq 0) -or ` + -not ($null -eq $CurrentValues.$fieldName)) + { + Write-Verbose -Message ($script:localizedData.ValueOfTypeDoesNotMatch ` + -f $desiredType.Name, $fieldName, $($CurrentValues.$fieldName), $($DesiredValues.$fieldName)) -Verbose + + $returnValue = $false + } + } + + 'Boolean' + { + if ($CurrentValues.$fieldName -ne $DesiredValues.$fieldName) + { + Write-Verbose -Message ($script:localizedData.ValueOfTypeDoesNotMatch ` + -f $desiredType.Name, $fieldName, $($CurrentValues.$fieldName), $($DesiredValues.$fieldName)) -Verbose + + $returnValue = $false + } + } + + default + { + Write-Warning -Message ($script:localizedData.UnableToCompareProperty ` + -f $fieldName, $desiredType.Name) + + $returnValue = $false + } + } + } + } + } + } + } + + return $returnValue +} + +<# + .SYNOPSIS + Retrieves the localized string data based on the machine's culture. + Falls back to en-US strings if the machine's culture is not supported. + + .PARAMETER ResourceName + The name of the resource as it appears before '.strings.psd1' of the localized string file. + For example: + For WindowsOptionalFeature: MSFT_WindowsOptionalFeature + For Service: MSFT_ServiceResource + For Registry: MSFT_RegistryResource + For Helper: SqlServerDscHelper + + .PARAMETER ScriptRoot + Optional. The root path where to expect to find the culture folder. This is only needed + for localization in helper modules. This should not normally be used for resources. + + .NOTES + To be able to use localization in the helper function, this function must + be first in the file, before Get-LocalizedData is used by itself to load + localized data for this helper module (see directly after this function). +#> +function Get-LocalizedData +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $ResourceName, + + [Parameter()] + [ValidateNotNullOrEmpty()] + [System.String] + $ScriptRoot + ) + + if (-not $ScriptRoot) + { + $dscResourcesFolder = Join-Path -Path (Split-Path -Path (Split-Path -Path $PSScriptRoot -Parent) -Parent) -ChildPath 'DSCResources' + $resourceDirectory = Join-Path -Path $dscResourcesFolder -ChildPath $ResourceName + } + else + { + $resourceDirectory = $ScriptRoot + } + + $localizedStringFileLocation = Join-Path -Path $resourceDirectory -ChildPath $PSUICulture + + if (-not (Test-Path -Path $localizedStringFileLocation)) + { + # Fallback to en-US + $localizedStringFileLocation = Join-Path -Path $resourceDirectory -ChildPath 'en-US' + } + + Import-LocalizedData ` + -BindingVariable 'localizedData' ` + -FileName "$ResourceName.strings.psd1" ` + -BaseDirectory $localizedStringFileLocation + + return $localizedData +} + +<# + .SYNOPSIS + Creates and throws an invalid argument exception. + + .PARAMETER Message + The message explaining why this error is being thrown. + + .PARAMETER ArgumentName + The name of the invalid argument that is causing this error to be thrown. +#> +function New-InvalidArgumentException +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $Message, + + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $ArgumentName + ) + + $argumentException = New-Object -TypeName 'ArgumentException' ` + -ArgumentList @($Message, $ArgumentName) + + $newObjectParameters = @{ + TypeName = 'System.Management.Automation.ErrorRecord' + ArgumentList = @($argumentException, $ArgumentName, 'InvalidArgument', $null) + } + + $errorRecord = New-Object @newObjectParameters + + throw $errorRecord +} + +<# + .SYNOPSIS + Creates and throws an invalid data exception. + + .PARAMETER ErrorId + The error Id to assign to the exception. + + .PARAMETER ErrorMessage + The error message to assign to the exception. +#> +function New-InvalidDataException +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $ErrorId, + + [Parameter(Mandatory = $true)] + [System.String] + $ErrorMessage + ) + + $errorCategory = [System.Management.Automation.ErrorCategory]::InvalidData + $exception = New-Object ` + -TypeName System.InvalidOperationException ` + -ArgumentList $ErrorMessage + $errorRecord = New-Object ` + -TypeName System.Management.Automation.ErrorRecord ` + -ArgumentList $exception, $ErrorId, $errorCategory, $null + + throw $errorRecord +} + +<# + .SYNOPSIS + Creates and throws an invalid operation exception. + + .PARAMETER Message + The message explaining why this error is being thrown. + + .PARAMETER ErrorRecord + The error record containing the exception that is causing this terminating error. +#> +function New-InvalidOperationException +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $Message, + + [Parameter()] + [ValidateNotNull()] + [System.Management.Automation.ErrorRecord] + $ErrorRecord + ) + + if ($null -eq $ErrorRecord) + { + $invalidOperationException = New-Object -TypeName 'InvalidOperationException' ` + -ArgumentList @($Message) + } + else + { + $invalidOperationException = New-Object -TypeName 'InvalidOperationException' ` + -ArgumentList @($Message, $ErrorRecord.Exception) + } + + $newObjectParameters = @{ + TypeName = 'System.Management.Automation.ErrorRecord' + ArgumentList = @( + $invalidOperationException.ToString(), + 'MachineStateIncorrect', + 'InvalidOperation', + $null + ) + } + + $errorRecordToThrow = New-Object @newObjectParameters + + throw $errorRecordToThrow +} + +<# + .SYNOPSIS + Creates and throws an object not found exception. + + .PARAMETER Message + The message explaining why this error is being thrown. + + .PARAMETER ErrorRecord + The error record containing the exception that is causing this terminating error. +#> +function New-ObjectNotFoundException +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $Message, + + [Parameter()] + [ValidateNotNull()] + [System.Management.Automation.ErrorRecord] + $ErrorRecord + ) + + if ($null -eq $ErrorRecord) + { + $exception = New-Object -TypeName 'System.Exception' ` + -ArgumentList @($Message) + } + else + { + $exception = New-Object -TypeName 'System.Exception' ` + -ArgumentList @($Message, $ErrorRecord.Exception) + } + + $newObjectParameters = @{ + TypeName = 'System.Management.Automation.ErrorRecord' + ArgumentList = @( + $exception.ToString(), + 'MachineStateIncorrect', + 'ObjectNotFound', + $null + ) + } + + $errorRecordToThrow = New-Object @newObjectParameters + + throw $errorRecordToThrow +} + +<# + .SYNOPSIS + Creates and throws an invalid result exception. + + .PARAMETER Message + The message explaining why this error is being thrown. + + .PARAMETER ErrorRecord + The error record containing the exception that is causing this terminating error. +#> +function New-InvalidResultException +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $Message, + + [Parameter()] + [ValidateNotNull()] + [System.Management.Automation.ErrorRecord] + $ErrorRecord + ) + + if ($null -eq $ErrorRecord) + { + $exception = New-Object -TypeName 'System.Exception' ` + -ArgumentList @($Message) + } + else + { + $exception = New-Object -TypeName 'System.Exception' ` + -ArgumentList @($Message, $ErrorRecord.Exception) + } + + $newObjectParameters = @{ + TypeName = 'System.Management.Automation.ErrorRecord' + ArgumentList = @( + $exception.ToString(), + 'MachineStateIncorrect', + 'InvalidResult', + $null + ) + } + + $errorRecordToThrow = New-Object @newObjectParameters + + throw $errorRecordToThrow +} + +function New-NotImplementedException +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $Message, + + [Parameter()] + [ValidateNotNull()] + [System.Management.Automation.ErrorRecord] + $ErrorRecord + ) + + if ($null -eq $ErrorRecord) + { + $invalidOperationException = New-Object -TypeName 'NotImplementedException' ` + -ArgumentList @($Message) + } + else + { + $invalidOperationException = New-Object -TypeName 'NotImplementedException' ` + -ArgumentList @($Message, $ErrorRecord.Exception) + } + + $newObjectParameters = @{ + TypeName = 'System.Management.Automation.ErrorRecord' + ArgumentList = @( + $invalidOperationException.ToString(), + 'MachineStateIncorrect', + 'NotImplemented', + $null + ) + } + + $errorRecordToThrow = New-Object @newObjectParameters + + throw $errorRecordToThrow +} + +<# + .SYNOPSIS + Sets the Global DSCMachineStatus variable to a value of 1. +#> +function Set-DscMachineRebootRequired +{ + # Suppressing this rule because $global:DSCMachineStatus is used to trigger a reboot. + [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidGlobalVars', '')] + <# + Suppressing this rule because $global:DSCMachineStatus is only set, + never used (by design of Desired State Configuration). + #> + [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', '')] + [CmdletBinding()] + param + ( + ) + + $global:DSCMachineStatus = 1 +} + +<# + .SYNOPSIS + Builds a string of the common parameters shared across all resources in a set. + + .PARAMETER KeyParameterName + The name of the key parameter for the resource. + + .PARAMETER Parameters + The hashtable of all parameters to the resource set (PSBoundParameters). + + .EXAMPLE + $parameters = @{ + KeyParameter = @( 'MyKeyParameter1', 'MyKeyParameter2' ) + CommonParameter1 = 'CommonValue1' + CommonParameter2 = 2 + } + + New-ResourceSetCommonParameterString -KeyParameterName 'KeyParameter' -Parameters $parameters + + OUTPUT (as string): + CommonParameter1 = "CommonValue1"`r`nCommonParameter2 = $CommonParameter2 +#> +function New-ResourceSetCommonParameterString +{ + [OutputType([System.String])] + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $KeyParameterName, + + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.Collections.Hashtable] + $Parameters + ) + + $stringBuilder = New-Object -TypeName 'System.Text.StringBuilder' + + foreach ($parameterName in $Parameters.Keys) + { + # All composite resources have an extra parameter 'InstanceName' + if ($parameterName -ine $KeyParameterName -and $parameterName -ine 'InstanceName') + { + $parameterValue = $Parameters[$parameterName] + + if ($null -ne $parameterValue) + { + if ($parameterValue -is [System.String]) + { + $null = $stringBuilder.AppendFormat('{0} = "{1}"', $parameterName, $parameterValue) + } + else + { + $null = $stringBuilder.Append($parameterName + ' = $' + $parameterName) + } + + $null = $stringBuilder.AppendLine() + } + } + } + + return $stringBuilder.ToString() +} + +<# + .SYNOPSIS + Creates a string representing a configuration script for a set of resources. + + .PARAMETER ResourceName + The name of the resource to create a set of. + + .PARAMETER ModuleName + The name of the module to import the resource from. + + .PARAMETER KeyParameterName + The name of the key parameter that will differentiate each resource. + + .PARAMETER KeyParameterValues + An array of the values of the key parameter that will differentiate each resource. + + .PARAMETER CommonParameterString + A string representing the common parameters for each resource. + Can be retrieved from New-ResourceSetCommonParameterString. + + .EXAMPLE + New-ResourceSetConfigurationString ` + -ResourceName 'xWindowsFeature' ` + -ModuleName 'xPSDesiredStateConfiguration' ` + -KeyParameterName 'Name' ` + -KeyParameterValues @( 'Telnet-Client', 'Web-Server' ) ` + -CommonParameterString 'Ensure = "Present"`r`nIncludeAllSubFeature = $true' + + OUTPUT (as a String): + Import-Module -Name xWindowsFeature -ModuleName xPSDesiredStateConfiguration + + xWindowsFeature Resource0 + { + Name = "Telnet-Client" + Ensure = "Present" + IncludeAllSubFeature = $true + } + + xWindowsFeature Resource1 + { + Name = "Web-Server" + Ensure = "Present" + IncludeAllSubFeature = $true + } +#> +function New-ResourceSetConfigurationString +{ + [OutputType([System.String])] + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $ResourceName, + + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $ModuleName, + + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $KeyParameterName, + + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String[]] + $KeyParameterValues, + + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $CommonParameterString + ) + + $stringBuilder = New-Object -TypeName 'System.Text.StringBuilder' + + $null = $stringBuilder.AppendFormat('Import-DscResource -Name {0} -ModuleName {1}', $ResourceName, $ModuleName) + $null = $stringBuilder.AppendLine() + + $resourceCount = 0 + foreach ($keyParameterValue in $KeyParameterValues) + { + $null = $stringBuilder.AppendFormat('{0} Resource{1}', $ResourceName, $resourceCount) + $null = $stringBuilder.AppendLine() + $null = $stringBuilder.AppendLine('{') + $null = $stringBuilder.AppendFormat($KeyParameterName + ' = "{0}"', $keyParameterValue) + $null = $stringBuilder.AppendLine() + $null = $stringBuilder.Append($CommonParameterString) + $null = $stringBuilder.AppendLine('}') + + $resourceCount++ + } + + return $stringBuilder.ToString() +} + +<# + .SYNOPSIS + Creates a configuration script block for a set of resources. + + .PARAMETER ResourceName + The name of the resource to create a set of. + + .PARAMETER ModuleName + The name of the module to import the resource from. + + .PARAMETER KeyParameterName + The name of the key parameter that will differentiate each resource. + + .PARAMETER Parameters + The hashtable of all parameters to the resource set (PSBoundParameters). + + .EXAMPLE + # From the xGroupSet composite resource + + $newResourceSetConfigurationParams = @{ + ResourceName = 'xGroup' + ModuleName = 'xPSDesiredStateConfiguration' + KeyParameterName = 'GroupName' + CommonParameterNames = @( 'Ensure', 'MembersToInclude', 'MembersToExclude', 'Credential' ) + Parameters = $PSBoundParameters + } + + $configurationScriptBlock = New-ResourceSetConfigurationScriptBlock @newResourceSetConfigurationParams + + .NOTES + Only allows one key parameter to be defined for each node. + For resources with multiple key parameters, only one key can be different for each resource. + See xProcessSet for an example of a resource set with two key parameters. +#> +function New-ResourceSetConfigurationScriptBlock +{ + [OutputType([System.Management.Automation.ScriptBlock])] + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $ResourceName, + + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $ModuleName, + + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $KeyParameterName, + + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.Collections.Hashtable] + $Parameters + ) + + $commonParameterString = New-ResourceSetCommonParameterString -KeyParameterName $KeyParameterName -Parameters $Parameters + + $newResourceSetConfigurationStringParams = @{ + ResourceName = $ResourceName + ModuleName = $ModuleName + KeyParameterName = $KeyParameterName + KeyParameterValues = $Parameters[$KeyParameterName] + CommonParameterString = $commonParameterString + } + + $resourceString = New-ResourceSetConfigurationString @newResourceSetConfigurationStringParams + + return [System.Management.Automation.ScriptBlock]::Create($resourceString) +} + +$script:localizedData = Get-LocalizedData -ResourceName 'xPSDesiredStateConfiguration.Common' -ScriptRoot $PSScriptRoot diff --git a/deployment/dsc/azshcihost/xPSDesiredStateConfiguration/9.1.0/Modules/xPSDesiredStateConfiguration.Firewall/en-US/xPSDesiredStateConfiguration.Firewall.strings.psd1 b/deployment/dsc/azshcihost/xPSDesiredStateConfiguration/9.1.0/Modules/xPSDesiredStateConfiguration.Firewall/en-US/xPSDesiredStateConfiguration.Firewall.strings.psd1 new file mode 100644 index 0000000..58c9d5a --- /dev/null +++ b/deployment/dsc/azshcihost/xPSDesiredStateConfiguration/9.1.0/Modules/xPSDesiredStateConfiguration.Firewall/en-US/xPSDesiredStateConfiguration.Firewall.strings.psd1 @@ -0,0 +1,4 @@ +# Localized resources for xPSDesiredStateConfiguration.Firewall + +ConvertFrom-StringData @' +'@ diff --git a/deployment/dsc/azshcihost/xPSDesiredStateConfiguration/9.1.0/Modules/xPSDesiredStateConfiguration.Firewall/xPSDesiredStateConfiguration.Firewall.psd1 b/deployment/dsc/azshcihost/xPSDesiredStateConfiguration/9.1.0/Modules/xPSDesiredStateConfiguration.Firewall/xPSDesiredStateConfiguration.Firewall.psd1 new file mode 100644 index 0000000..d04f3d1 --- /dev/null +++ b/deployment/dsc/azshcihost/xPSDesiredStateConfiguration/9.1.0/Modules/xPSDesiredStateConfiguration.Firewall/xPSDesiredStateConfiguration.Firewall.psd1 @@ -0,0 +1,59 @@ +@{ + # Version number of this module. + ModuleVersion = '0.0.1' + + # ID used to uniquely identify this module + GUID = 'e400c89a-064f-4b3c-8197-6a7be06d55f3' + + # Author of this module + Author = 'DSC Community' + + # Company or vendor of this module + CompanyName = 'DSC Community' + + # Copyright statement for this module + Copyright = 'Copyright the DSC Community contributors. All rights reserved.' + + # Description of the functionality provided by this module + Description = 'Functions used by the DSC Web Service resource to configure firewall settings.' + + # Functions to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no functions to export. + FunctionsToExport = @( + 'Add-PullServerFirewallConfiguration', + 'Remote-PullServerFirewallConfiguration', + 'Test-PullServerFirewallConfiguration' + ) + + # Cmdlets to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no cmdlets to export. + CmdletsToExport = @() + + # Variables to export from this module + VariablesToExport = @() + + # Aliases to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no aliases to export. + AliasesToExport = @() + + # Private data to pass to the module specified in RootModule/ModuleToProcess. This may also contain a PSData hashtable with additional module metadata used by PowerShell. + PrivateData = @{ + + PSData = @{ + # Set to a prerelease string value if the release should be a prerelease. + Prerelease = '' + + # Tags applied to this module. These help with module discovery in online galleries. + Tags = @('DesiredStateConfiguration', 'DSC', 'DSCResourceKit', 'DSCResource') + + # A URL to the license for this module. + LicenseUri = 'https://github.com/dsccommunity/xPSDesiredStateConfiguration/blob/master/LICENSE' + + # A URL to the main website for this project. + ProjectUri = 'https://github.com/dsccommunity/xPSDesiredStateConfiguration' + + # A URL to an icon representing this module. + IconUri = 'https://dsccommunity.org/images/DSC_Logo_300p.png' + + # ReleaseNotes of this module + ReleaseNotes = '' + } # End of PSData hashtable + } # End of PrivateData hashtable +} diff --git a/deployment/dsc/azshcihost/xPSDesiredStateConfiguration/9.1.0/Modules/xPSDesiredStateConfiguration.Firewall/xPSDesiredStateConfiguration.Firewall.psm1 b/deployment/dsc/azshcihost/xPSDesiredStateConfiguration/9.1.0/Modules/xPSDesiredStateConfiguration.Firewall/xPSDesiredStateConfiguration.Firewall.psm1 new file mode 100644 index 0000000..0dbe5f1 --- /dev/null +++ b/deployment/dsc/azshcihost/xPSDesiredStateConfiguration/9.1.0/Modules/xPSDesiredStateConfiguration.Firewall/xPSDesiredStateConfiguration.Firewall.psm1 @@ -0,0 +1,106 @@ +$modulePath = Split-Path -Path $PSScriptRoot -Parent + +# Import the shared modules +Import-Module -Name (Join-Path -Path $modulePath ` + -ChildPath (Join-Path -Path 'xPSDesiredStateConfiguration.Common' ` + -ChildPath 'xPSDesiredStateConfiguration.Common.psm1')) + +# Import Localization Strings +$script:localizedData = Get-LocalizedData -ResourceName 'xPSDesiredStateConfiguration.Firewall' -ScriptRoot $PSScriptRoot + +New-Variable -Name FireWallRuleDisplayName -Value 'DSCPullServer_IIS_Port' -Option ReadOnly -Scope Script -Force +New-Variable -Name netsh -Value "$env:windir\system32\netsh.exe" -Option ReadOnly -Scope Script -Force + +<# + .SYNOPSIS + Create a firewall exception so that DSC clients are able to access the configured Pull Server + .PARAMETER Port + The TCP port used to create the firewall exception +#> +function Add-PullServerFirewallConfiguration +{ + [CmdletBinding()] + param + ( + [Parameter()] + [ValidateRange(1, 65535)] + [System.UInt32] + $Port + ) + + Write-Verbose -Message 'Disable Inbound Firewall Notification' + $null = & $script:netsh advfirewall set currentprofile settings inboundusernotification disable + + $ruleName = $FireWallRuleDisplayName + + # Remove all existing rules with that displayName + $null = & $script:netsh advfirewall firewall delete rule name=$ruleName protocol=tcp localport=$Port + + Write-Verbose -Message "Add Firewall Rule for port $Port" + $null = & $script:netsh advfirewall firewall add rule name=$ruleName dir=in action=allow protocol=TCP localport=$Port +} + +<# + .SYNOPSIS + Delete the Pull Server firewall exception + .PARAMETER Port + The TCP port for which the firewall exception should be deleted +#> +function Remove-PullServerFirewallConfiguration +{ + [CmdletBinding()] + param + ( + [Parameter()] + [ValidateRange(1, 65535)] + [System.UInt32] + $Port + ) + + if (Test-PullServerFirewallConfiguration -Port $Port) + { + # remove all existing rules with that displayName + Write-Verbose -Message "Delete Firewall Rule for port $Port" + $ruleName = $FireWallRuleDisplayName + + # backwards compatibility with old code + if (Get-Command -Name Get-NetFirewallRule -CommandType Cmdlet -ErrorAction:SilentlyContinue) + { + # Remove all rules with that name + Get-NetFirewallRule -DisplayName $ruleName | Remove-NetFirewallRule + } + else + { + $null = & $script:netsh advfirewall firewall delete rule name=$ruleName protocol=tcp localport=$Port + } + } + else + { + Write-Verbose -Message "No DSC PullServer firewall rule found with port $Port. No cleanup required" + } +} + +<# + .SYNOPSIS + Tests if a Pull Server firewall exception exists for a specific port + .PARAMETER Port + The TCP port for which the firewall exception should be tested +#> +function Test-PullServerFirewallConfiguration +{ + [CmdletBinding()] + [OutputType([System.Boolean])] + param + ( + [Parameter()] + [ValidateRange(1, 65535)] + [System.UInt32] + $Port + ) + + # Remove all existing rules with that displayName + Write-Verbose -Message "Testing Firewall Rule for port $Port" + $ruleName = $FireWallRuleDisplayName + $result = & $script:netsh advfirewall firewall show rule name=$ruleName | Select-String -Pattern "LocalPort:\s*$Port" + return -not [string]::IsNullOrWhiteSpace($result) +} diff --git a/deployment/dsc/azshcihost/xPSDesiredStateConfiguration/9.1.0/Modules/xPSDesiredStateConfiguration.PSWSIIS/en-US/xPSDesiredStateConfiguration.PSWSIIS.strings.psd1 b/deployment/dsc/azshcihost/xPSDesiredStateConfiguration/9.1.0/Modules/xPSDesiredStateConfiguration.PSWSIIS/en-US/xPSDesiredStateConfiguration.PSWSIIS.strings.psd1 new file mode 100644 index 0000000..ae88e01 --- /dev/null +++ b/deployment/dsc/azshcihost/xPSDesiredStateConfiguration/9.1.0/Modules/xPSDesiredStateConfiguration.PSWSIIS/en-US/xPSDesiredStateConfiguration.PSWSIIS.strings.psd1 @@ -0,0 +1,4 @@ +# Localized resources for xPSDesiredStateConfiguration.PSWSIIS + +ConvertFrom-StringData @' +'@ diff --git a/deployment/dsc/azshcihost/xPSDesiredStateConfiguration/9.1.0/Modules/xPSDesiredStateConfiguration.PSWSIIS/xPSDesiredStateConfiguration.PSWSIIS.psd1 b/deployment/dsc/azshcihost/xPSDesiredStateConfiguration/9.1.0/Modules/xPSDesiredStateConfiguration.PSWSIIS/xPSDesiredStateConfiguration.PSWSIIS.psd1 new file mode 100644 index 0000000..43c362a --- /dev/null +++ b/deployment/dsc/azshcihost/xPSDesiredStateConfiguration/9.1.0/Modules/xPSDesiredStateConfiguration.PSWSIIS/xPSDesiredStateConfiguration.PSWSIIS.psd1 @@ -0,0 +1,63 @@ +@{ + # Version number of this module. + ModuleVersion = '0.0.1' + + # ID used to uniquely identify this module + GUID = '63b5d3ab-7f33-4647-970b-cbab5532116f' + + # Author of this module + Author = 'DSC Community' + + # Company or vendor of this module + CompanyName = 'DSC Community' + + # Copyright statement for this module + Copyright = 'Copyright the DSC Community contributors. All rights reserved.' + + # Description of the functionality provided by this module + Description = 'This module contains a utility to perform PSWS IIS Endpoint setup.' + + # Functions to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no functions to export. + FunctionsToExport = @( + 'New-ResourceSetConfigurationScriptBlock', + 'New-PSWSEndpoint', + 'Set-AppSettingsInWebconfig', + 'Set-BindingRedirectSettingInWebConfig', + 'Remove-PSWSEndpoint' + ) + + # Cmdlets to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no cmdlets to export. + CmdletsToExport = @() + + # Variables to export from this module + VariablesToExport = @( + 'DscWebServiceDefaultAppPoolName' + ) + + # Aliases to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no aliases to export. + AliasesToExport = @() + + # Private data to pass to the module specified in RootModule/ModuleToProcess. This may also contain a PSData hashtable with additional module metadata used by PowerShell. + PrivateData = @{ + + PSData = @{ + # Set to a prerelease string value if the release should be a prerelease. + Prerelease = '' + + # Tags applied to this module. These help with module discovery in online galleries. + Tags = @('DesiredStateConfiguration', 'DSC', 'DSCResourceKit', 'DSCResource') + + # A URL to the license for this module. + LicenseUri = 'https://github.com/dsccommunity/xPSDesiredStateConfiguration/blob/master/LICENSE' + + # A URL to the main website for this project. + ProjectUri = 'https://github.com/dsccommunity/xPSDesiredStateConfiguration' + + # A URL to an icon representing this module. + IconUri = 'https://dsccommunity.org/images/DSC_Logo_300p.png' + + # ReleaseNotes of this module + ReleaseNotes = '' + } # End of PSData hashtable + } # End of PrivateData hashtable +} diff --git a/deployment/dsc/azshcihost/xPSDesiredStateConfiguration/9.1.0/Modules/xPSDesiredStateConfiguration.PSWSIIS/xPSDesiredStateConfiguration.PSWSIIS.psm1 b/deployment/dsc/azshcihost/xPSDesiredStateConfiguration/9.1.0/Modules/xPSDesiredStateConfiguration.PSWSIIS/xPSDesiredStateConfiguration.PSWSIIS.psm1 new file mode 100644 index 0000000..6a7c47e --- /dev/null +++ b/deployment/dsc/azshcihost/xPSDesiredStateConfiguration/9.1.0/Modules/xPSDesiredStateConfiguration.PSWSIIS/xPSDesiredStateConfiguration.PSWSIIS.psm1 @@ -0,0 +1,1022 @@ +$modulePath = Split-Path -Path $PSScriptRoot -Parent + +# Import the shared modules +Import-Module -Name (Join-Path -Path $modulePath ` + -ChildPath (Join-Path -Path 'xPSDesiredStateConfiguration.Common' ` + -ChildPath 'xPSDesiredStateConfiguration.Common.psm1')) + +# Import Localization Strings +$script:localizedData = Get-LocalizedData -ResourceName 'xPSDesiredStateConfiguration.PSWSIIS' -ScriptRoot $PSScriptRoot + +New-Variable -Name DscWebServiceDefaultAppPoolName -Value 'PSWS' -Option ReadOnly -Force -Scope Script + +<# + .SYNOPSIS + Validate supplied configuration to setup the PSWS Endpoint Function + checks for the existence of PSWS Schema files, IIS config Also validate + presence of IIS on the target machine +#> +function Initialize-Endpoint +{ + [CmdletBinding()] + param + ( + [Parameter()] + [ValidateNotNullOrEmpty()] + [System.String] + $appPool, + + [Parameter()] + [ValidateNotNullOrEmpty()] + [System.String] + $site, + + [Parameter()] + [ValidateNotNullOrEmpty()] + [System.String] + $path, + + [Parameter()] + [ValidateScript({Test-Path -Path $_})] + [System.String] + $cfgfile, + + [Parameter()] + [System.Int32] + $port, + + [Parameter()] + [ValidateNotNullOrEmpty()] + [System.String] + $app, + + [Parameter()] + [ValidateNotNullOrEmpty()] + [System.String] + $applicationPoolIdentityType, + + [Parameter()] + [ValidateScript({Test-Path -Path $_})] + [System.String] + $svc, + + [Parameter()] + [ValidateScript({Test-Path -Path $_})] + [System.String] + $mof, + + [Parameter()] + [ValidateNotNullOrEmpty()] + [System.String] + $dispatch, + + [Parameter()] + [ValidateScript({Test-Path -Path $_})] + [System.String] + $asax, + + [Parameter()] + [ValidateNotNullOrEmpty()] + [System.String[]] + $dependentBinaries, + + [Parameter()] + [ValidateNotNullOrEmpty()] + [System.String] + $language, + + [Parameter()] + [ValidateNotNullOrEmpty()] + [System.String[]] + $dependentMUIFiles, + + [Parameter()] + [System.String[]] + $psFiles, + + [Parameter()] + [System.Boolean] + $removeSiteFiles = $false, + + [Parameter()] + [System.String] + $certificateThumbPrint, + + [Parameter()] + [System.Boolean] + $enable32BitAppOnWin64 + ) + + if ($certificateThumbPrint -ne 'AllowUnencryptedTraffic') + { + Write-Verbose -Message 'Verify that the certificate with the provided thumbprint exists in CERT:\LocalMachine\MY\' + + $certificate = Get-ChildItem -Path CERT:\LocalMachine\MY\ | Where-Object -FilterScript { + $_.Thumbprint -eq $certificateThumbPrint + } + + if (!$Certificate) + { + throw "ERROR: Certificate with thumbprint $certificateThumbPrint does not exist in CERT:\LocalMachine\MY\" + } + } + + Test-IISInstall + + # First remove the site so that the binding count on the application pool is reduced + Update-Site -siteName $site -siteAction Remove + + Remove-AppPool -appPool $appPool + + # Check for existing binding, there should be no binding with the same port + $allWebBindingsOnPort = Get-WebBinding | Where-Object -FilterScript { + $_.BindingInformation -eq "*:$($port):" + } + + if ($allWebBindingsOnPort.Count -gt 0) + { + throw "ERROR: Port $port is already used, please review existing sites and change the port to be used." + } + + if ($removeSiteFiles) + { + if (Test-Path -Path $path) + { + Remove-Item -Path $path -Recurse -Force + } + } + + Copy-PSWSConfigurationToIISEndpointFolder -path $path ` + -cfgfile $cfgfile ` + -svc $svc ` + -mof $mof ` + -dispatch $dispatch ` + -asax $asax ` + -dependentBinaries $dependentBinaries ` + -language $language ` + -dependentMUIFiles $dependentMUIFiles ` + -psFiles $psFiles + + New-IISWebSite -site $site ` + -path $path ` + -port $port ` + -app $app ` + -apppool $appPool ` + -applicationPoolIdentityType $applicationPoolIdentityType ` + -certificateThumbPrint $certificateThumbPrint ` + -enable32BitAppOnWin64 $enable32BitAppOnWin64 +} + +<# + .SYNOPSIS + Validate if IIS and all required dependencies are installed on the + target machine +#> +function Test-IISInstall +{ + [CmdletBinding()] + param () + + Write-Verbose -Message 'Checking IIS requirements' + $iisVersion = (Get-ItemProperty HKLM:\SOFTWARE\Microsoft\InetStp -ErrorAction silentlycontinue).MajorVersion + + if ($iisVersion -lt 7) + { + throw "ERROR: IIS Version detected is $iisVersion , must be running higher than 7.0" + } + + $wsRegKey = (Get-ItemProperty hklm:\SYSTEM\CurrentControlSet\Services\W3SVC -ErrorAction silentlycontinue).ImagePath + if ($null -eq $wsRegKey) + { + throw 'ERROR: Cannot retrive W3SVC key. IIS Web Services may not be installed' + } + + if ((Get-Service w3svc).Status -ne 'running') + { + throw 'ERROR: service W3SVC is not running' + } +} + +<# + .SYNOPSIS + Verify if a given IIS Site exists +#> +function Test-ForIISSite +{ + [CmdletBinding()] + [OutputType([System.Boolean])] + param + ( + [Parameter()] + [System.String] + $siteName + ) + + if (Get-Website -Name $siteName) + { + return $true + } + + return $false +} + +<# + .SYNOPSIS + Perform an action (such as stop, start, delete) for a given IIS Site +#> +function Update-Site +{ + param + ( + [Parameter(ParameterSetName = 'SiteName', Mandatory = $true, Position = 0)] + [ValidateNotNullOrEmpty()] + [System.String] + $siteName, + + [Parameter(ParameterSetName = 'Site', Mandatory = $true, Position = 0)] + [System.Object] + $site, + + [Parameter(ParameterSetName = 'SiteName', Mandatory = $true, Position = 1)] + [Parameter(ParameterSetName = 'Site', Mandatory = $true, Position = 1)] + [System.String] + [ValidateSet('Start', 'Stop', 'Remove')] + $siteAction + ) + + if ('SiteName' -eq $PSCmdlet.ParameterSetName) + { + $site = Get-Website -Name $siteName + } + + if ($site) + { + switch ($siteAction) + { + 'Start' + { + Write-Verbose -Message "Starting IIS Website [$($site.name)]" + Start-Website -Name $site.name + } + + 'Stop' + { + if ('Started' -eq $site.state) + { + Write-Verbose -Message "Stopping WebSite $($site.name)" + $website = Stop-Website -Name $site.name -Passthru + + if ('Started' -eq $website.state) + { + throw "Unable to stop WebSite $($site.name)" + } + + <# + There may be running requests, wait a little + I had an issue where the files were still in use + when I tried to delete them + #> + Write-Verbose -Message 'Waiting for IIS to stop website' + Start-Sleep -Milliseconds 1000 + } + else + { + Write-Verbose -Message "IIS Website [$($site.name)] already stopped" + } + } + + 'Remove' + { + Update-Site -site $site -siteAction Stop + Write-Verbose -Message "Removing IIS Website [$($site.name)]" + Remove-Website -Name $site.name + } + } + } + else + { + Write-Verbose -Message "IIS Website [$siteName] not found" + } +} + +<# + .SYNOPSIS + Returns the list of bound sites and applications for a given IIS Application pool + + .PARAMETER appPool + The application pool name +#> +function Get-AppPoolBinding +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $AppPool + ) + + if (Test-Path -Path "IIS:\AppPools\$AppPool") + { + $sites = Get-WebConfigurationProperty ` + -Filter "/system.applicationHost/sites/site/application[@applicationPool=`'$AppPool`'and @path='/']/parent::*" ` + -PSPath 'machine/webroot/apphost' ` + -Name name + $apps = Get-WebConfigurationProperty ` + -Filter "/system.applicationHost/sites/site/application[@applicationPool=`'$AppPool`'and @path!='/']" ` + -PSPath 'machine/webroot/apphost' ` + -Name path + $sites, $apps | ForEach-Object { + $_.Value + } + } +} + +<# + .SYNOPSIS + Delete the given IIS Application Pool. This is required to cleanup any + existing conflicting apppools before setting up the endpoint. +#> +function Remove-AppPool +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $AppPool + ) + + if ($DscWebServiceDefaultAppPoolName -eq $AppPool) + { + # Without this tests we may get a breaking error here, despite SilentlyContinue + if (Test-Path -Path "IIS:\AppPools\$AppPool") + { + $bindingCount = (Get-AppPoolBinding -AppPool $AppPool | Measure-Object).Count + + if (0 -ge $bindingCount) + { + Remove-WebAppPool -Name $AppPool -ErrorAction SilentlyContinue + } + else + { + Write-Verbose -Message "Application pool [$AppPool] can't be deleted because it's still bound to a site or application" + } + } + } + else + { + Write-Verbose -Message "ApplicationPool can't be deleted because the name is different from built-in name [$DscWebServiceDefaultAppPoolName]." + } +} + +<# + .SYNOPSIS + Generate an IIS Site Id while setting up the endpoint. The Site Id will + be the max available in IIS config + 1. +#> +function New-SiteID +{ + [CmdletBinding()] + param () + + return ((Get-Website | Foreach-Object -Process { $_.Id } | Measure-Object -Maximum).Maximum + 1) +} + +<# + .SYNOPSIS + Copies the supplied PSWS config files to the IIS endpoint in inetpub +#> +function Copy-PSWSConfigurationToIISEndpointFolder +{ + [CmdletBinding()] + param + ( + [Parameter()] + [System.String] + $path, + + [Parameter()] + [ValidateScript({Test-Path -Path $_})] + [System.String] + $cfgfile, + + [Parameter()] + [ValidateScript({Test-Path -Path $_})] + [System.String] + $svc, + + [Parameter()] + [ValidateScript({Test-Path -Path $_})] + [System.String] + $mof, + + [Parameter()] + [System.String] + $dispatch, + + [Parameter()] + [ValidateScript({Test-Path -Path $_})] + [System.String] + $asax, + + [Parameter()] + [System.String[]] + $dependentBinaries, + + [Parameter()] + [System.String] + $language, + + [Parameter()] + [System.String[]] + $dependentMUIFiles, + + [Parameter()] + [System.String[]] + $psFiles + ) + + if (!(Test-Path -Path $path)) + { + $null = New-Item -ItemType container -Path $path + } + + foreach ($dependentBinary in $dependentBinaries) + { + if (!(Test-Path -Path $dependentBinary)) + { + throw "ERROR: $dependentBinary does not exist" + } + } + + Write-Verbose -Message 'Create the bin folder for deploying custom dependent binaries required by the endpoint' + $binFolderPath = Join-Path -Path $path -ChildPath 'bin' + $null = New-Item -Path $binFolderPath -ItemType 'directory' -Force + Copy-Item -Path $dependentBinaries -Destination $binFolderPath -Force + + foreach ($psFile in $psFiles) + { + if (!(Test-Path -Path $psFile)) + { + throw "ERROR: $psFile does not exist" + } + + Copy-Item -Path $psFile -Destination $path -Force + } + + Copy-Item -Path $cfgfile (Join-Path -Path $path -ChildPath 'web.config') -Force + Copy-Item -Path $svc -Destination $path -Force + Copy-Item -Path $mof -Destination $path -Force + + if ($dispatch) + { + Copy-Item -Path $dispatch -Destination $path -Force + } + + if ($asax) + { + Copy-Item -Path $asax -Destination $path -Force + } +} + +<# + .SYNOPSIS + Setup IIS Apppool, Site and Application +#> +function New-IISWebSite +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $site, + + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $path, + + [Parameter(Mandatory = $true)] + [System.Int32] + $port, + + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $app, + + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $appPool, + + [Parameter()] + [System.String] + $applicationPoolIdentityType, + + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $certificateThumbPrint, + + [Parameter()] + [System.Boolean] + $enable32BitAppOnWin64 + ) + + $siteID = New-SiteID + + if (Test-Path IIS:\AppPools\$appPool) + { + Write-Verbose -Message "Application Pool [$appPool] already exists" + } + else + { + Write-Verbose -Message "Adding App Pool [$appPool]" + $null = New-WebAppPool -Name $appPool + + Write-Verbose -Message 'Set App Pool Properties' + $appPoolIdentity = 4 + + if ($applicationPoolIdentityType) + { + # LocalSystem = 0, LocalService = 1, NetworkService = 2, SpecificUser = 3, ApplicationPoolIdentity = 4 + switch ($applicationPoolIdentityType) + { + 'LocalSystem' + { + $appPoolIdentity = 0 + } + + 'LocalService' + { + $appPoolIdentity = 1 + } + + 'NetworkService' + { + $appPoolIdentity = 2 + } + + 'ApplicationPoolIdentity' + { + $appPoolIdentity = 4 + } + + default { + throw "Invalid value [$applicationPoolIdentityType] for parameter -applicationPoolIdentityType" + } + } + } + + $appPoolItem = Get-Item -Path IIS:\AppPools\$appPool + $appPoolItem.managedRuntimeVersion = 'v4.0' + $appPoolItem.enable32BitAppOnWin64 = $enable32BitAppOnWin64 + $appPoolItem.processModel.identityType = $appPoolIdentity + $appPoolItem | Set-Item + + } + + Write-Verbose -Message 'Add and Set Site Properties' + + if ($certificateThumbPrint -eq 'AllowUnencryptedTraffic') + { + $null = New-WebSite -Name $site -Id $siteID -Port $port -IPAddress "*" -PhysicalPath $path -ApplicationPool $appPool + } + else + { + $null = New-WebSite -Name $site -Id $siteID -Port $port -IPAddress "*" -PhysicalPath $path -ApplicationPool $appPool -Ssl + + # Remove existing binding for $port + Remove-Item IIS:\SSLBindings\0.0.0.0!$port -ErrorAction Ignore + + # Create a new binding using the supplied certificate + $null = Get-Item CERT:\LocalMachine\MY\$certificateThumbPrint | New-Item IIS:\SSLBindings\0.0.0.0!$port + } + + Update-Site -siteName $site -siteAction Start +} + +<# + .SYNOPSIS + Enable & Clear PSWS Operational/Analytic/Debug ETW Channels. +#> +function Enable-PSWSETW +{ + # Disable Analytic Log + $null = & $script:wevtutil sl Microsoft-Windows-ManagementOdataService/Analytic /e:false /q + + # Disable Debug Log + $null = & $script:wevtutil sl Microsoft-Windows-ManagementOdataService/Debug /e:false /q + + # Clear Operational Log + $null = & $script:wevtutil cl Microsoft-Windows-ManagementOdataService/Operational + + # Enable/Clear Analytic Log + $null = & $script:wevtutil sl Microsoft-Windows-ManagementOdataService/Analytic /e:true /q + + # Enable/Clear Debug Log + $null = & $script:wevtutil sl Microsoft-Windows-ManagementOdataService/Debug /e:true /q +} + +<# + .SYNOPSIS + Create PowerShell WebServices IIS Endpoint + + .DESCRIPTION + Creates a PSWS IIS Endpoint by consuming PSWS Schema and related + dependent files + + .EXAMPLE + New PSWS Endpoint [@ http://Server:39689/PSWS_Win32Process] by + consuming PSWS Schema Files and any dependent scripts/binaries: + + New-PSWSEndpoint + -site Win32Process + -path $env:SystemDrive\inetpub\PSWS_Win32Process + -cfgfile Win32Process.config + -port 39689 + -app Win32Process + -svc PSWS.svc + -mof Win32Process.mof + -dispatch Win32Process.xml + -dependentBinaries ConfigureProcess.ps1, Rbac.dll + -psFiles Win32Process.psm1 +#> +function New-PSWSEndpoint +{ + [CmdletBinding()] + param + ( + # Unique Name of the IIS Site + [Parameter()] + [ValidateNotNullOrEmpty()] + [System.String] + $site = 'PSWS', + + # Physical path for the IIS Endpoint on the machine (under inetpub) + [Parameter()] + [ValidateNotNullOrEmpty()] + [System.String] + $path = "$env:SystemDrive\inetpub\PSWS", + + # Web.config file + [Parameter()] + [ValidateNotNullOrEmpty()] + [System.String] + $cfgfile = 'web.config', + + # Port # for the IIS Endpoint + [Parameter()] + [System.Int32] + $port = 8080, + + # IIS Application Name for the Site + [Parameter()] + [ValidateNotNullOrEmpty()] + [System.String] + $app = 'PSWS', + + # IIS Application Name for the Site + [Parameter()] + [System.String] + $appPool, + + # IIS App Pool Identity Type - must be one of LocalService, LocalSystem, NetworkService, ApplicationPoolIdentity + [Parameter()] + [ValidateSet('LocalService', 'LocalSystem', 'NetworkService', 'ApplicationPoolIdentity')] + [System.String] + $applicationPoolIdentityType, + + # WCF Service SVC file + [Parameter()] + [ValidateNotNullOrEmpty()] + [System.String] + $svc = 'PSWS.svc', + + # PSWS Specific MOF Schema File + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $mof, + + # PSWS Specific Dispatch Mapping File [Optional] + [Parameter()] + [ValidateNotNullOrEmpty()] + [System.String] + $dispatch, + + # Global.asax file [Optional] + [Parameter()] + [ValidateNotNullOrEmpty()] + [System.String] + $asax, + + # Any dependent binaries that need to be deployed to the IIS endpoint, in the bin folder + [Parameter()] + [ValidateNotNullOrEmpty()] + [System.String[]] + $dependentBinaries, + + # MUI Language [Optional] + [Parameter()] + [ValidateNotNullOrEmpty()] + [System.String] + $language, + + # Any dependent binaries that need to be deployed to the IIS endpoint, in the bin\mui folder [Optional] + [Parameter()] + [ValidateNotNullOrEmpty()] + [System.String[]] + $dependentMUIFiles, + + # Any dependent PowerShell Scipts/Modules that need to be deployed to the IIS endpoint application root + [Parameter()] + [System.String[]] + $psFiles, + + # True to remove all files for the site at first, false otherwise + [Parameter()] + [System.Boolean] + $removeSiteFiles = $false, + + # Enable and Clear PSWS ETW + [Parameter()] + [System.Management.Automation.SwitchParameter] + $EnablePSWSETW, + + # Thumbprint of the Certificate in CERT:\LocalMachine\MY\ for Pull Server + [Parameter()] + [System.String] + $certificateThumbPrint = 'AllowUnencryptedTraffic', + + # When this property is set to true, Pull Server will run on a 32 bit process on a 64 bit machine + [Parameter()] + [System.Boolean] + $Enable32BitAppOnWin64 = $false + ) + + if (-not $appPool) + { + $appPool = $DscWebServiceDefaultAppPoolName + } + + $script:wevtutil = "$env:windir\system32\Wevtutil.exe" + + $svcName = Split-Path $svc -Leaf + $protocol = 'https:' + + if ($certificateThumbPrint -eq 'AllowUnencryptedTraffic') + { + $protocol = 'http:' + } + + # Get Machine Name + $cimInstance = Get-CimInstance -ClassName Win32_ComputerSystem -Verbose:$false + + Write-Verbose -Message "Setting up endpoint at - $protocol//$($cimInstance.Name):$port/$svcName" + Initialize-Endpoint ` + -appPool $appPool ` + -site $site ` + -path $path ` + -cfgfile $cfgfile ` + -port $port ` + -app $app ` + -applicationPoolIdentityType $applicationPoolIdentityType ` + -svc $svc ` + -mof $mof ` + -dispatch $dispatch ` + -asax $asax ` + -dependentBinaries $dependentBinaries ` + -language $language ` + -dependentMUIFiles $dependentMUIFiles ` + -psFiles $psFiles ` + -removeSiteFiles $removeSiteFiles ` + -certificateThumbPrint $certificateThumbPrint ` + -enable32BitAppOnWin64 $Enable32BitAppOnWin64 + + if ($EnablePSWSETW) + { + Enable-PSWSETW + } +} + +<# + .SYNOPSIS + Removes a DSC WebServices IIS Endpoint + + .DESCRIPTION + Removes a PSWS IIS Endpoint + + .EXAMPLE + Remove the endpoint with the specified name: + + Remove-PSWSEndpoint -siteName PSDSCPullServer +#> +function Remove-PSWSEndpoint +{ + [CmdletBinding()] + param + ( + # Unique Name of the IIS Site + [Parameter()] + [ValidateNotNullOrEmpty()] + [System.String] + $siteName + ) + + # Get the site to remove + $site = Get-Website -Name $siteName + + if ($site) + { + # And the pool it is using + $pool = $site.applicationPool + # Get the path so we can delete the files + $filePath = $site.PhysicalPath + + # Remove the actual site. + Update-Site -site $site -siteAction Remove + + # Remove the files for the site + if (Test-Path -Path $filePath) + { + Get-ChildItem -Path $filePath -Recurse | Remove-Item -Recurse -Force + Remove-Item -Path $filePath -Force + } + + Remove-AppPool -appPool $pool + } + else + { + Write-Verbose -Message "Website with name [$siteName] does not exist" + } +} + +<# + .SYNOPSIS + Set the option into the web.config for an endpoint + + .DESCRIPTION + Set the options into the web.config for an endpoint allowing + customization. +#> +function Set-AppSettingsInWebconfig +{ + [CmdletBinding()] + param + ( + # Physical path for the IIS Endpoint on the machine (possibly under inetpub) + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $Path, + + # Key to add/update + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $Key, + + # Value + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $Value + ) + + $webconfig = Join-Path -Path $Path -ChildPath 'web.config' + [System.Boolean] $Found = $false + + if (Test-Path -Path $webconfig) + { + $xml = [System.Xml.XmlDocument] (Get-Content -Path $webconfig) + $root = $xml.get_DocumentElement() + + foreach ($item in $root.appSettings.add) + { + if ($item.key -eq $Key) + { + $item.value = $Value; + $Found = $true; + } + } + + if (-not $Found) + { + $newElement = $xml.CreateElement('add') + $nameAtt1 = $xml.CreateAttribute('key') + $nameAtt1.psbase.value = $Key; + $null = $newElement.SetAttributeNode($nameAtt1) + + $nameAtt2 = $xml.CreateAttribute('value') + $nameAtt2.psbase.value = $Value; + $null = $newElement.SetAttributeNode($nameAtt2) + + $null = $xml.configuration['appSettings'].AppendChild($newElement) + } + } + + $xml.Save($webconfig) +} + +<# + .SYNOPSIS + Set the binding redirect setting in the web.config to redirect 10.0.0.0 + version of microsoft.isam.esent.interop to 6.3.0.0. + + .DESCRIPTION + This function creates the following section in the web.config: + <runtime> + <assemblyBinding xmlns='urn:schemas-microsoft-com:asm.v1'> + <dependentAssembly> + <assemblyIdentity name='microsoft.isam.esent.interop' publicKeyToken='31bf3856ad364e35' /> + <bindingRedirect oldVersion='10.0.0.0' newVersion='6.3.0.0' /> + </dependentAssembly> + </assemblyBinding> + </runtime> +#> +function Set-BindingRedirectSettingInWebConfig +{ + [CmdletBinding()] + param + ( + # Physical path for the IIS Endpoint on the machine (possibly under inetpub) + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $path, + + # old version of the assembly + [Parameter()] + [System.String] + $oldVersion = '10.0.0.0', + + # new version to redirect to + [Parameter()] + [System.String] + $newVersion = '6.3.0.0' + ) + + $webconfig = Join-Path $path 'web.config' + + if (Test-Path -Path $webconfig) + { + $xml = [System.Xml.XmlDocument] (Get-Content -Path $webconfig) + + if (-not($xml.get_DocumentElement().runtime)) + { + # Create the <runtime> section + $runtimeSetting = $xml.CreateElement('runtime') + + # Create the <assemblyBinding> section + $assemblyBindingSetting = $xml.CreateElement('assemblyBinding') + $xmlnsAttribute = $xml.CreateAttribute('xmlns') + $xmlnsAttribute.Value = 'urn:schemas-microsoft-com:asm.v1' + $assemblyBindingSetting.Attributes.Append($xmlnsAttribute) + + # The <assemblyBinding> section goes inside <runtime> + $null = $runtimeSetting.AppendChild($assemblyBindingSetting) + + # Create the <dependentAssembly> section + $dependentAssemblySetting = $xml.CreateElement('dependentAssembly') + + # The <dependentAssembly> section goes inside <assemblyBinding> + $null = $assemblyBindingSetting.AppendChild($dependentAssemblySetting) + + # Create the <assemblyIdentity> section + $assemblyIdentitySetting = $xml.CreateElement('assemblyIdentity') + $nameAttribute = $xml.CreateAttribute('name') + $nameAttribute.Value = 'microsoft.isam.esent.interop' + $publicKeyTokenAttribute = $xml.CreateAttribute('publicKeyToken') + $publicKeyTokenAttribute.Value = '31bf3856ad364e35' + $null = $assemblyIdentitySetting.Attributes.Append($nameAttribute) + $null = $assemblyIdentitySetting.Attributes.Append($publicKeyTokenAttribute) + + # <assemblyIdentity> section goes inside <dependentAssembly> + $dependentAssemblySetting.AppendChild($assemblyIdentitySetting) + + # Create the <bindingRedirect> section + $bindingRedirectSetting = $xml.CreateElement('bindingRedirect') + $oldVersionAttribute = $xml.CreateAttribute('oldVersion') + $newVersionAttribute = $xml.CreateAttribute('newVersion') + $oldVersionAttribute.Value = $oldVersion + $newVersionAttribute.Value = $newVersion + $null = $bindingRedirectSetting.Attributes.Append($oldVersionAttribute) + $null = $bindingRedirectSetting.Attributes.Append($newVersionAttribute) + + # The <bindingRedirect> section goes inside <dependentAssembly> section + $dependentAssemblySetting.AppendChild($bindingRedirectSetting) + + # The <runtime> section goes inside <Configuration> section + $xml.configuration.AppendChild($runtimeSetting) + + $xml.Save($webconfig) + } + } +} diff --git a/deployment/dsc/azshcihost/xPSDesiredStateConfiguration/9.1.0/Modules/xPSDesiredStateConfiguration.Security/en-US/xPSDesiredStateConfiguration.Security.strings.psd1 b/deployment/dsc/azshcihost/xPSDesiredStateConfiguration/9.1.0/Modules/xPSDesiredStateConfiguration.Security/en-US/xPSDesiredStateConfiguration.Security.strings.psd1 new file mode 100644 index 0000000..58c9d5a --- /dev/null +++ b/deployment/dsc/azshcihost/xPSDesiredStateConfiguration/9.1.0/Modules/xPSDesiredStateConfiguration.Security/en-US/xPSDesiredStateConfiguration.Security.strings.psd1 @@ -0,0 +1,4 @@ +# Localized resources for xPSDesiredStateConfiguration.Firewall + +ConvertFrom-StringData @' +'@ diff --git a/deployment/dsc/azshcihost/xPSDesiredStateConfiguration/9.1.0/Modules/xPSDesiredStateConfiguration.Security/xPSDesiredStateConfiguration.Security.psd1 b/deployment/dsc/azshcihost/xPSDesiredStateConfiguration/9.1.0/Modules/xPSDesiredStateConfiguration.Security/xPSDesiredStateConfiguration.Security.psd1 new file mode 100644 index 0000000..d8a9681 --- /dev/null +++ b/deployment/dsc/azshcihost/xPSDesiredStateConfiguration/9.1.0/Modules/xPSDesiredStateConfiguration.Security/xPSDesiredStateConfiguration.Security.psd1 @@ -0,0 +1,58 @@ +@{ + # Version number of this module. + ModuleVersion = '0.0.1' + + # ID used to uniquely identify this module + GUID = 'ad2e23bf-3d74-4399-8fbe-93caa4713b6f' + + # Author of this module + Author = 'DSC Community' + + # Company or vendor of this module + CompanyName = 'DSC Community' + + # Copyright statement for this module + Copyright = 'Copyright the DSC Community contributors. All rights reserved.' + + # Description of the functionality provided by this module + Description = 'Functions used by the DSC Web Service resource to configure system security settings.' + + # Functions to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no functions to export. + FunctionsToExport = @( + 'Set-UseSecurityBestPractice', + 'Test-UseSecurityBestPractice' + ) + + # Cmdlets to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no cmdlets to export. + CmdletsToExport = @() + + # Variables to export from this module + VariablesToExport = @() + + # Aliases to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no aliases to export. + AliasesToExport = @() + + # Private data to pass to the module specified in RootModule/ModuleToProcess. This may also contain a PSData hashtable with additional module metadata used by PowerShell. + PrivateData = @{ + + PSData = @{ + # Set to a prerelease string value if the release should be a prerelease. + Prerelease = '' + + # Tags applied to this module. These help with module discovery in online galleries. + Tags = @('DesiredStateConfiguration', 'DSC', 'DSCResourceKit', 'DSCResource') + + # A URL to the license for this module. + LicenseUri = 'https://github.com/dsccommunity/xPSDesiredStateConfiguration/blob/master/LICENSE' + + # A URL to the main website for this project. + ProjectUri = 'https://github.com/dsccommunity/xPSDesiredStateConfiguration' + + # A URL to an icon representing this module. + IconUri = 'https://dsccommunity.org/images/DSC_Logo_300p.png' + + # ReleaseNotes of this module + ReleaseNotes = '' + } # End of PSData hashtable + } # End of PrivateData hashtable +} diff --git a/deployment/dsc/azshcihost/xPSDesiredStateConfiguration/9.1.0/Modules/xPSDesiredStateConfiguration.Security/xPSDesiredStateConfiguration.Security.psm1 b/deployment/dsc/azshcihost/xPSDesiredStateConfiguration/9.1.0/Modules/xPSDesiredStateConfiguration.Security/xPSDesiredStateConfiguration.Security.psm1 new file mode 100644 index 0000000..d6b82d0 --- /dev/null +++ b/deployment/dsc/azshcihost/xPSDesiredStateConfiguration/9.1.0/Modules/xPSDesiredStateConfiguration.Security/xPSDesiredStateConfiguration.Security.psm1 @@ -0,0 +1,120 @@ +$modulePath = Split-Path -Path $PSScriptRoot -Parent + +# Import the shared modules +Import-Module -Name (Join-Path -Path $modulePath ` + -ChildPath (Join-Path -Path 'xPSDesiredStateConfiguration.Common' ` + -ChildPath 'xPSDesiredStateConfiguration.Common.psm1')) + +# Import Localization Strings +$script:localizedData = Get-LocalizedData -ResourceName 'xPSDesiredStateConfiguration.Security' -ScriptRoot $PSScriptRoot + +# Best Practice Security Settings Block +$insecureProtocols = @("SSL 2.0", "SSL 3.0", "TLS 1.0", "PCT 1.0", "Multi-Protocol Unified Hello") +$secureProtocols = @("TLS 1.1", "TLS 1.2") + +<# + This list corresponds to the ValueMap definition of DisableSecurityBestPractices + parameter defined in MSFT_xDSCWebService.Schema.mof +#> +$SecureTLSProtocols = 'SecureTLSProtocols' + +<# + .SYNOPSIS + This function tests if the SChannel protocols are enabled. +#> +function Test-SChannelProtocol +{ + [CmdletBinding()] + param () + + foreach ($protocol in $insecureProtocols) + { + $registryPath = "HKLM:\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Protocols\$protocol\Server" + + if ((Test-Path -Path $registryPath) ` + -and ($null -ne (Get-ItemProperty -Path $registryPath)) ` + -and ((Get-ItemProperty -Path $registryPath).Enabled -ne 0)) + { + return $false + } + } + + foreach ($protocol in $secureProtocols) + { + $registryPath = "HKLM:\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Protocols\$protocol\Server" + + if ((-not (Test-Path -Path $registryPath)) ` + -or ($null -eq (Get-ItemProperty -Path $registryPath)) ` + -or ((Get-ItemProperty -Path $registryPath).Enabled -eq 0)) + { + return $false + } + } + + return $true +} + +<# + .SYNOPSIS + This function enables the SChannel protocols. +#> +function Set-SChannelProtocol +{ + [CmdletBinding()] + param () + + foreach ($protocol in $insecureProtocols) + { + $registryPath = "HKLM:\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Protocols\$protocol\Server" + $null = New-Item -Path $registryPath -Force + $null = New-ItemProperty -Path $registryPath -Name Enabled -Value 0 -PropertyType 'DWord' -Force + } + + foreach ($protocol in $secureProtocols) + { + $registryPath = "HKLM:\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Protocols\$protocol\Server" + $null = New-Item -Path $registryPath -Force + $null = New-ItemProperty -Path $registryPath -Name Enabled -Value '0xffffffff' -PropertyType 'DWord' -Force + $null = New-ItemProperty -Path $registryPath -Name DisabledByDefault -Value 0 -PropertyType 'DWord' -Force + } +} + +<# + .SYNOPSIS + This function tests whether the node uses security best practices for non-disabled items +#> +function Test-UseSecurityBestPractice +{ + [CmdletBinding()] + [OutputType([System.Boolean])] + param + ( + [Parameter()] + [System.String[]] + $DisableSecurityBestPractices + ) + + $usedProtocolsBestPractices = ($DisableSecurityBestPractices -icontains $SecureTLSProtocols) -or (Test-SChannelProtocol) + + return $usedProtocolsBestPractices +} + +<# + .SYNOPSIS + This function sets the node to use security best practices for non-disabled items +#> +function Set-UseSecurityBestPractice +{ + [CmdletBinding()] + param + ( + [Parameter()] + [System.String[]] + $DisableSecurityBestPractices + ) + + if (-not ($DisableSecurityBestPractices -icontains $SecureTLSProtocols)) + { + Set-SChannelProtocol + } +} diff --git a/deployment/dsc/azshcihost/xPSDesiredStateConfiguration/9.1.0/ResourceDesignerScripts/GenerateXRemoteFileSchema.ps1 b/deployment/dsc/azshcihost/xPSDesiredStateConfiguration/9.1.0/ResourceDesignerScripts/GenerateXRemoteFileSchema.ps1 new file mode 100644 index 0000000..ae92288 --- /dev/null +++ b/deployment/dsc/azshcihost/xPSDesiredStateConfiguration/9.1.0/ResourceDesignerScripts/GenerateXRemoteFileSchema.ps1 @@ -0,0 +1,9 @@ +$DestinationPath = New-xDscResourceProperty -Name DestinationPath -Type String -Attribute Key -Description 'Path under which downloaded or copied file should be accessible after operation.' +$Uri = New-xDscResourceProperty -Name Uri -Type String -Attribute Required -Description 'Uri of a file which should be copied or downloaded. This parameter supports HTTP and HTTPS values.' +$Headers = New-xDscResourceProperty -Name Headers -Type Hashtable[] -Attribute Write -Description 'Headers of the web request.' +$UserAgent = New-xDscResourceProperty -Name UserAgent -Type String -Attribute Write -Description 'User agent for the web request.' +$Ensure = New-xDscResourceProperty -Name Ensure -Type String -Attribute Read -ValidateSet "Present", "Absent" -Description 'Says whether DestinationPath exists on the machine' +$Credential = New-xDscResourceProperty -Name Credential -Type PSCredential -Attribute Write -Description 'Specifies a user account that has permission to send the request.' + +New-xDscResource -Name MSFT_xRemoteFile -Property @($DestinationPath, $Uri, $Headers, $UserAgent, $Ensure, $Credential, $CertificateThumbprint) -ModuleName xPSDesiredStateConfiguration2 -FriendlyName xRemoteFile + diff --git a/deployment/dsc/azshcihost/xPSDesiredStateConfiguration/9.1.0/ResourceDesignerScripts/New-PSSessionConfigurationResource.ps1 b/deployment/dsc/azshcihost/xPSDesiredStateConfiguration/9.1.0/ResourceDesignerScripts/New-PSSessionConfigurationResource.ps1 new file mode 100644 index 0000000..5b22f12 --- /dev/null +++ b/deployment/dsc/azshcihost/xPSDesiredStateConfiguration/9.1.0/ResourceDesignerScripts/New-PSSessionConfigurationResource.ps1 @@ -0,0 +1,18 @@ +Import-Module -Name 'xDSCResourceDesigner' + +$resProperties = @{ + Name = New-xDscResourceProperty -Description 'Name of the PS Remoting Endpoint' ` + -Name Name -Type String -Attribute Key + RunAsCred = New-xDscResourceProperty -Description 'Credential for Running under different user context' ` + -Name RunAsCredential -Type PSCredential -Attribute Write + SDDL = New-xDscResourceProperty -Description 'SDDL for allowed users to connect to this endpoint. 'Default' means the default SDDL' ` + -Name SecurityDescriptorSDDL -Type String -Attribute Write + StartupScript = New-xDscResourceProperty -Description 'Path for the startup script. Empty string clears the value'` + -Name StartupScriptPath -Type String -Attribute Write + Ensure = New-xDscResourceProperty -Description 'Whether to create the endpoint or delete it' ` + -Name Ensure -Type String -Attribute Write -ValidateSet @('Present', 'Absent') + AccessMode = New-xDscResourceProperty -Description 'Whether the endpoint is remotely accessible or has local access only or no access' ` + -Name AccessMode -Type String -Attribute Write -ValidateSet @('Local', 'Remote', 'Disabled') +} + +New-xDscResource -Name MSFT_xPSSessionConfiguration -Property $resProperties.Values -Path $home\desktop -ModuleName xPSDesiredStateConfiguration -FriendlyName xPSEndpoint -Force diff --git a/deployment/dsc/azshcihost/xPSDesiredStateConfiguration/9.1.0/en-US/about_xPSDesiredStateConfiguration.help.txt b/deployment/dsc/azshcihost/xPSDesiredStateConfiguration/9.1.0/en-US/about_xPSDesiredStateConfiguration.help.txt new file mode 100644 index 0000000..eb30503 --- /dev/null +++ b/deployment/dsc/azshcihost/xPSDesiredStateConfiguration/9.1.0/en-US/about_xPSDesiredStateConfiguration.help.txt @@ -0,0 +1,29 @@ +TOPIC + about_xPSDesiredStateConfiguration + +SHORT DESCRIPTION + DSC resources for configuring common operating systems features, files and + settings. + +LONG DESCRIPTION + This module contains DSC resources for configuring common operating systems + features, files and settings. + +EXAMPLES + PS C:\> Get-DscResource -Module xPSDesiredStateConfiguration + +NOTE: + Thank you to the DSC Community contributors who contributed to this module by + writing code, sharing opinions, and provided feedback. + +TROUBLESHOOTING NOTE: + Go to the Github repository for read about issues, submit a new issue, and read + about new releases. https://github.com/dsccommunity/xPSDesiredStateConfiguration + +SEE ALSO + - https://github.com/dsccommunity/xPSDesiredStateConfiguration + +KEYWORDS + DSC, DscResource, Archive, Environment, Group, MSI, Package, File, + RemoteFile, Registry, Script, Service, User, + WindowsFeature, WindowsOptionalFeature, WindowsPackageCab, WindowsProcess diff --git a/deployment/dsc/azshcihost/xPSDesiredStateConfiguration/9.1.0/xPSDesiredStateConfiguration.psd1 b/deployment/dsc/azshcihost/xPSDesiredStateConfiguration/9.1.0/xPSDesiredStateConfiguration.psd1 new file mode 100644 index 0000000..74dfd8f --- /dev/null +++ b/deployment/dsc/azshcihost/xPSDesiredStateConfiguration/9.1.0/xPSDesiredStateConfiguration.psd1 @@ -0,0 +1,200 @@ +@{ + # Root module + RootModule = 'Modules\DscPullServerSetup\DscPullServerSetup.psm1' + + # Version number of this module. + moduleVersion = '9.1.0' + + # ID used to uniquely identify this module + GUID = 'cc8dc021-fa5f-4f96-8ecf-dfd68a6d9d48' + + # Author of this module + Author = 'DSC Community' + + # Company or vendor of this module + CompanyName = 'DSC Community' + + # Copyright statement for this module + Copyright = 'Copyright the DSC Community contributors. All rights reserved.' + + # Description of the functionality provided by this module + Description = 'DSC resources for configuring common operating systems features, files and settings.' + + # Minimum version of the Windows PowerShell engine required by this module + PowerShellVersion = '4.0' + + # Minimum version of the common language runtime (CLR) required by this module + CLRVersion = '4.0' + + # Functions to export from this module + FunctionsToExport = @( + 'Publish-DscModuleAndMof', + 'Publish-ModulesAndChecksum', + 'Publish-MofsInSource', + 'Publish-ModuleToPullServer', + 'Publish-MofToPullServer' + ) + + # Cmdlets to export from this module + CmdletsToExport = @() + + # Variables to export from this module + VariablesToExport = @() + + # Aliases to export from this module + AliasesToExport = @() + + # DSC resources to export from this module + DscResourcesToExport = @( + 'xArchive', 'xDSCWebService', 'xEnvironment','xGroup','xMsiPackage', + 'xPackage','xPSEndpoint','xRegistry','xRemoteFile', + 'xScript','xService','xUser','xWindowsFeature','xWindowsOptionalFeature', + 'xWindowsPackageCab','xWindowsProcess' + ) + + # Private data to pass to the module specified in RootModule/ModuleToProcess. This may also contain a PSData hashtable with additional module metadata used by PowerShell. + PrivateData = @{ + + PSData = @{ + # Set to a prerelease string value if the release should be a prerelease. + Prerelease = '' + + # Tags applied to this module. These help with module discovery in online galleries. + Tags = @('DesiredStateConfiguration', 'DSC', 'DSCResourceKit', 'DSCResource') + + # A URL to the license for this module. + LicenseUri = 'https://github.com/dsccommunity/xPSDesiredStateConfiguration/blob/master/LICENSE' + + # A URL to the main website for this project. + ProjectUri = 'https://github.com/dsccommunity/xPSDesiredStateConfiguration' + + # A URL to an icon representing this module. + IconUri = 'https://dsccommunity.org/images/DSC_Logo_300p.png' + + # ReleaseNotes of this module + ReleaseNotes = '## [9.1.0] - 2020-02-19 + +### Changed + +- Examples + - Removed the version number of the xPSDesiredStateConfiguration module from the #Requires headers. - + [issue #653](https://github.com/dsccommunity/xPSDesiredStateConfiguration/issues/653) + +### Fixed + +- xPSDesiredStateConfiguration + - Export `Publish-*` functions in DscPullServerSetup module - Fixes + [issue #673](https://github.com/PowerShell/PSDscResources/issues/673). + +- DSC_xRegistryResource + - Fixed an issue that failed to create a registry with ":" in the path. + [issue #671](https://github.com/dsccommunity/xPSDesiredStateConfiguration/issues/671) + +## [9.0.0] - 2020-01-15 + +### Added + +- xPSDesiredStateConfiguration + - Added support for Checksum on xRemoteFile - [issue #423](https://github.com/PowerShell/PSDscResources/issues/423) + - Added `Test-DscParameterState` support function to `xPSDesiredStateConfiguration.Common.psm1`. + - Added standard unit tests for `xPSDesiredStateConfiguration.Common.psm1`. + - Added automatic release with a new CI pipeline. + +### Changed + +- xPSDesiredStateConfiguration + - PublishModulesAndMofsToPullServer.psm1: + - Fixes issue in Publish-MOFToPullServer that incorrectly tries to create a + new MOF file instead of reading the existing one. + [issue #575](https://github.com/PowerShell/xPSDesiredStateConfiguration/issues/575) + - Fix minor style issues with missing spaces between `param` statements and ''(''. + - MSFT_xDSCWebService: + - Removal of commented out code. + - Updated to meet HQRM style guidelines - Fixes [issue #623](https://github.com/PowerShell/xPSDesiredStateConfiguration/issues/623) + - Added MOF descriptions. + - Corrected minor style issues. + - Fix minor style issues in hashtable layout. + - Shared modules moved to `source/Module` folder and renamed: + - `CommonResourceHelper.psm1` -> `xPSDesiredStateConfiguration.Common.psm1` + - Moved functions from `ResourceSetHelper.psm1` into + `xPSDesiredStateConfiguration.Common.psm1`. + - BREAKING CHANGE: Changed resource prefix from MSFT to DSC. + - Pinned `ModuleBuilder` to v1.0.0. + - Updated build badges in README.MD. + - Remove unused localization strings. + - Adopt DSC Community Code of Conduct. + - DSC_xPSSessionConfiguration: + - Moved strings to localization file. + - DSC_xScriptResource + - Updated parameter descriptions to match MOF file. + - Correct miscellaneous style issues. + - DSC_xWindowsOptionalFeature + - Fix localization strings. + - DSC_xEnvironmentResource + - Remove unused localization strings. + - DSC_xRemoteFile + - Updated end-to-end tests to use the same pattern as the other end-to-end + tests in this module. + - DSC_xDSCWebService + - Moved `PSWSIISEndpoint.psm1` module into `xPSDesiredStateConfiguration.PSWSIIS`. + - Moved `Firewall.psm1` module into `xPSDesiredStateConfiguration.Firewall`. + - Moved `SecureTLSProtocols.psm1` and `UseSecurityBestPractices.psm1` module + into `xPSDesiredStateConfiguration.Security`. + - Fix issue with `Get-TargetResource` when a DSC Pull Server website is not + installed. + - DSC_xWindowsFeature + - Changed tests to be able to run on machines without `*-WindowsFeature` cmdlets. + - Changed `Assert-SingleInstanceOfFeature` to accept an array. + - BREAKING CHANGE: Renamed `PublishModulesAndMofsToPullServer` module to + `DscPullServerSetup` and moved to Modules folder. + - Moved test helper modules into `tests\TestHelpers` folder. +- DSCPullServerSetup + - Fixed markdown errors in README.MD. + - Moved strings to Localization file. + - Corrected style violations. +- Updated build badges to reflect correct Azure DevOps build Definition Id - fixes + [issue #656](https://github.com/PowerShell/xPSDesiredStateConfiguration/issues/656). +- Set `testRunTitle` for PublishTestResults steps so that a helpful name is + displayed in Azure DevOps for each test run. +- Set a display name on all the jobs and tasks in the CI + pipeline - fixes [issue #663](https://github.com/PowerShell/xPSDesiredStateConfiguration/issues/663). + +### Deprecated + +- None + +### Removed + +- xPSDesiredStateConfiguration + - Removed files no longer required by new CI process. + +### Fixed + +- MSFT_xRegistryResource + - Fixes issue that the `Set-TargetResource` does not determine + the type of registry value correctly. + [issue #436](https://github.com/dsccommunity/xPSDesiredStateConfiguration/issues/436) +- Fixed Pull Server example links in `README.MD` - fixes + [issue #659](https://github.com/PowerShell/xPSDesiredStateConfiguration/issues/659). +- Fixed `GitVersion.yml` feature and fix Regex - fixes + [issue #660](https://github.com/PowerShell/xPSDesiredStateConfiguration/issues/660). +- Fix import statement in all tests, making sure it throws if module + DscResource.Test cannot be imported - fixes + [issue #666](https://github.com/PowerShell/xPSDesiredStateConfiguration/issues/666). +- Fix deploy stage in CI pipeline to prevent it executing against forks + of the repository - fixes [issue #665](https://github.com/PowerShell/xPSDesiredStateConfiguration/issues/665). +- Fix deploy fork detection in CI pipeline - fixes [issue #668](https://github.com/PowerShell/xPSDesiredStateConfiguration/issues/668). + +### Security + +- None + +' + } # End of PSData hashtable + } # End of PrivateData hashtable +} + + + + + diff --git a/deployment/helpers/Install-AzsRolesandFeatures.ps1 b/deployment/helpers/Install-AzsRolesandFeatures.ps1 new file mode 100644 index 0000000..d389d97 --- /dev/null +++ b/deployment/helpers/Install-AzsRolesandFeatures.ps1 @@ -0,0 +1,38 @@ +configuration AzSHCIHost +{ + Import-DscResource -ModuleName 'PSDesiredStateConfiguration' + + node localhost + { + LocalConfigurationManager { + RebootNodeIfNeeded = $true + ActionAfterReboot = 'ContinueConfiguration' + ConfigurationMode = 'ApplyAndAutoCorrect' + ConfigurationModeFrequencyMins = 1440 + } + + WindowsFeatureSet "AzSHCI Required Roles" + { + Ensure = 'Present' + Name = @("File-Services", "FS-FileServer", "FS-Data-Deduplication", "BitLocker", "Data-Center-Bridging", "EnhancedStorage", "Failover-Clustering", "RSAT", "RSAT-Feature-Tools", "RSAT-DataCenterBridging-LLDP-Tools", "RSAT-Clustering", "RSAT-Clustering-PowerShell", "RSAT-Role-Tools", "RSAT-AD-Tools", "RSAT-AD-PowerShell", "RSAT-Hyper-V-Tools", "Hyper-V-PowerShell") + } + } +} + +$date = get-date -f yyyy-MM-dd +$logFile = Join-Path -Path "C:\temp" -ChildPath $('AzSHCIHost-Transcipt-' + $date + '.log') +$DscConfigLocation = "c:\temp\AzSHCIHost" + +Start-Transcript -Path $logFile + +Remove-DscConfigurationDocument -Stage Current, Previous, Pending -Force + +AzSHCIHost -OutputPath $DscConfigLocation + +Set-DscLocalConfigurationManager -Path $DscConfigLocation -Verbose + +Start-DscConfiguration -Path $DscConfigLocation -Wait -Verbose + +Stop-Transcript + +Logoff \ No newline at end of file diff --git a/deployment/helpers/Register-AzSHCI.ps1 b/deployment/helpers/Register-AzSHCI.ps1 new file mode 100644 index 0000000..fd667b6 --- /dev/null +++ b/deployment/helpers/Register-AzSHCI.ps1 @@ -0,0 +1,16 @@ +Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Force +Install-Module Az.StackHCI + +Invoke-Command -ComputerName AZSHCINODE01 -ScriptBlock { + Get-AzureStackHCI +} + +$azshciNodeCreds = Get-Credential -UserName "azshci\azureuser" -Message "Enter the azshci\azureuser password" +Register-AzStackHCI ` + -SubscriptionId "your-subscription-ID-here" ` + -ResourceName "azshciclus" ` + -ResourceGroupName "AZSHCICLUS_RG" ` + -Region "EastUS" ` + -EnvironmentName "AzureCloud" ` + -ComputerName "AZSHCINODE01.azshci.local" ` + -Credential $azshciNodeCreds diff --git a/deployment/helpers/Update-AD.ps1 b/deployment/helpers/Update-AD.ps1 new file mode 100644 index 0000000..18eee78 --- /dev/null +++ b/deployment/helpers/Update-AD.ps1 @@ -0,0 +1,55 @@ +$targetHost = $env:COMPUTERNAME +$AzureStackHCIHosts = Get-VM -Name "*AZSHCINODE*" +$AzureStackHCIClusterName = "AZSHCICLUS" +$ouName = "AzSHCICluster" + +$dn = Get-ADOrganizationalUnit -Filter * | Where-Object name -eq $ouName +if (-not ($dn)) { + $dn = New-ADOrganizationalUnit -Name $ouName -PassThru +} + +#Get Wac Computer Object +$targetHostObject = Get-ADComputer -Filter * | Where-Object name -eq $targetHost +if (-not ($targetHostObject)) { + $targetHostObject = New-ADComputer -Name $targetHost -Enabled $false -PassThru +} + +# Creates Azure Stack HCI hosts if not exist +if ($AzureStackHCIHosts.Name) { + $AzureStackHCIHosts.Name | ForEach-Object { + $comp = Get-ADComputer -Filter * | Where-Object Name -eq $_ + if (-not ($comp)) { + New-ADComputer -Name $_ -Enabled $false -Path $dn -PrincipalsAllowedToDelegateToAccount $targetHostObject + } + else { + $comp | Set-ADComputer -PrincipalsAllowedToDelegateToAccount $targetHostObject + $comp | Move-AdObject -TargetPath $dn + } + } +} + +# Creates Azure Stack HCI Cluster CNO if not exist +$AzureStackHCIClusterObject = Get-ADComputer -Filter * | Where-Object name -eq $AzureStackHCIClusterName +if (-not ($AzureStackHCIClusterObject)) { + $AzureStackHCIClusterObject = New-ADComputer -Name $AzureStackHCIClusterName -Enabled $false ` + -Path $dn -PrincipalsAllowedToDelegateToAccount $targetHostObject -PassThru +} +else { + $AzureStackHCIClusterObject | Set-ADComputer -PrincipalsAllowedToDelegateToAccount $targetHostObject + $AzureStackHCIClusterObject | Move-AdObject -TargetPath $dn +} + +#read OU DACL +$acl = Get-Acl -Path "AD:\$dn" + +# Set properties to allow Cluster CNO to Full Control on the new OU +$principal = New-Object System.Security.Principal.SecurityIdentifier ($AzureStackHCIClusterObject).SID +$ace = New-Object System.DirectoryServices.ActiveDirectoryAccessRule($principal, ` + [System.DirectoryServices.ActiveDirectoryRights]::GenericAll, [System.Security.AccessControl.AccessControlType]::Allow, ` + [DirectoryServices.ActiveDirectorySecurityInheritance]::All) + +#modify DACL +$acl.AddAccessRule($ace) + +#Re-apply the modified DACL to the OU +Set-ACL -ACLObject $acl -Path "AD:\$dn" \ No newline at end of file diff --git a/deployment/json/azshcihost.json b/deployment/json/azshcihost.json new file mode 100644 index 0000000..6d7ceb8 --- /dev/null +++ b/deployment/json/azshcihost.json @@ -0,0 +1,618 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "virtualMachineName": { + "type": "string", + "defaultValue": "AzSHCIHost001", + "metadata": { + "description": "Keep the VM name to less than 15 characters" + } + }, + "virtualMachineSize": { + "type": "string", + "defaultValue": "Standard_E16s_v4", + "allowedValues": [ + "Standard_D16s_v3", + "Standard_D32s_v3", + "Standard_D64s_v3", + "Standard_D16s_v4", + "Standard_D32s_v4", + "Standard_D64s_v4", + "Standard_D16d_v4", + "Standard_D32d_v4", + "Standard_D64d_v4", + "Standard_D16ds_v4", + "Standard_D32ds_v4", + "Standard_D64ds_v4", + "Standard_E8s_v3", + "Standard_E16s_v3", + "Standard_E16-4s_v3", + "Standard_E16-8s_v3", + "Standard_E20s_v3", + "Standard_E32s_v3", + "Standard_E32-8s_v3", + "Standard_E32-16s_v3", + "Standard_E48s_v3", + "Standard_E64s_v3", + "Standard_E64-16s_v3", + "Standard_E64-32s_v3", + "Standard_E8s_v4", + "Standard_E16s_v4", + "Standard_E16-8s_v4", + "Standard_E20s_v4", + "Standard_E32s_v4", + "Standard_E32-8s_v4", + "Standard_E32-16s_v4", + "Standard_E48s_v4", + "Standard_E64s_v4", + "Standard_E64-16s_v4", + "Standard_E64-32s_v4", + "Standard_E8d_v4", + "Standard_E16d_v4", + "Standard_E20d_v4", + "Standard_E32d_v4", + "Standard_E48d_v4", + "Standard_E64d_v4", + "Standard_E8ds_v4", + "Standard_E16ds_v4", + "Standard_E20ds_v4", + "Standard_E32ds_v4", + "Standard_E48ds_v4", + "Standard_E64ds_v4", + "Standard_E64-16ds_v4", + "Standard_E64-32ds_v4" + ] + }, + "virtualMachineGeneration": { + "type": "string", + "defaultValue": "Generation 2", + "allowedValues": [ + "Generation 1", + "Generation 2" + ], + "metadata": { + "description": "Select your VM generation, ideally Gen 2. Not all Azure regions support Gen 2 VMs." + } + }, + "domainName": { + "type": "string", + "defaultValue": "azshci.local", + "metadata": { + "description": "The FQDN that will be used in the environment" + } + }, + "dataDiskType": { + "type": "string", + "defaultValue": "StandardSSD_LRS", + "allowedValues": [ + "StandardSSD_LRS", + "Premium_LRS" + ], + "metadata": { + "description": "The Storage type of the VM data disk. If your VM contains an 's' in the VM size, you can select Premium_LRS storage for increased performance, but at a higher cost." + } + }, + "dataDiskSize": { + "type": "string", + "defaultValue": "32", + "allowedValues": [ + "32", + "64", + "128", + "256", + "512", + "1024" + ], + "metadata": { + "description": "The size of the individual data disks in GiB. 8 of these will be provisioned therefore 32GiB is the recommended default." + } + }, + "adminUsername": { + "type": "string", + "defaultValue": "AzureUser" + }, + "adminPassword": { + "type": "securestring" + }, + "enableDHCP": { + "type": "string", + "defaultValue": "Enabled", + "allowedValues": [ + "Enabled", + "Disabled" + ], + "metadata": { + "description": "Choose whether you wish to enable DHCP in the environment. If you choose Disabled, it can be enabled after deployment." + } + }, + "customRdpPort": { + "type": "string", + "defaultValue": "3389", + "metadata": { + "description": "If you wish to use a different port to RDP into the VM (between 0 and 65535), change it here, otherwise, leave the default." + } + }, + "autoShutdownStatus": { + "type": "string", + "defaultValue": "Enabled", + "allowedValues": [ + "Enabled", + "Disabled" + ] + }, + "autoShutdownTime": { + "type": "string", + "defaultValue": "22:00" + }, + "autoShutdownTimeZone": { + "type": "string", + "defaultValue": "UTC", + "allowedValues": [ + "Afghanistan Standard Time", + "Alaskan Standard Time", + "Aleutian Standard Time", + "Altai Standard Time", + "Arab Standard Time", + "Arabian Standard Time", + "Arabic Standard Time", + "Argentina Standard Time", + "Astrakhan Standard Time", + "Atlantic Standard Time", + "AUS Central Standard Time", + "Aus Central W. Standard Time", + "AUS Eastern Standard Time", + "Azerbaijan Standard Time", + "Azores Standard Time", + "Bahia Standard Time", + "Bangladesh Standard Time", + "Belarus Standard Time", + "Bougainville Standard Time", + "Canada Central Standard Time", + "Cape Verde Standard Time", + "Caucasus Standard Time", + "Cen. Australia Standard Time", + "Central America Standard Time", + "Central Asia Standard Time", + "Central Brazilian Standard Time", + "Central Europe Standard Time", + "Central European Standard Time", + "Central Pacific Standard Time", + "Central Standard Time", + "Central Standard Time (Mexico)", + "Chatham Islands Standard Time", + "China Standard Time", + "Cuba Standard Time", + "Dateline Standard Time", + "E. Africa Standard Time", + "E. Australia Standard Time", + "E. Europe Standard Time", + "E. South America Standard Time", + "Easter Island Standard Time", + "Eastern Standard Time", + "Eastern Standard Time (Mexico)", + "Egypt Standard Time", + "Ekaterinburg Standard Time", + "Fiji Standard Time", + "FLE Standard Time", + "Georgian Standard Time", + "GMT Standard Time", + "Greenland Standard Time", + "Greenwich Standard Time", + "GTB Standard Time", + "Haiti Standard Time", + "Hawaiian Standard Time", + "India Standard Time", + "Iran Standard Time", + "Israel Standard Time", + "Jordan Standard Time", + "Kaliningrad Standard Time", + "Korea Standard Time", + "Libya Standard Time", + "Line Islands Standard Time", + "Lord Howe Standard Time", + "Magadan Standard Time", + "Magallanes Standard Time", + "Marquesas Standard Time", + "Mauritius Standard Time", + "Middle East Standard Time", + "Montevideo Standard Time", + "Morocco Standard Time", + "Mountain Standard Time", + "Mountain Standard Time (Mexico)", + "Myanmar Standard Time", + "N. Central Asia Standard Time", + "Namibia Standard Time", + "Nepal Standard Time", + "New Zealand Standard Time", + "Newfoundland Standard Time", + "Norfolk Standard Time", + "North Asia East Standard Time", + "North Asia Standard Time", + "North Korea Standard Time", + "Omsk Standard Time", + "Pacific SA Standard Time", + "Pacific Standard Time", + "Pacific Standard Time (Mexico)", + "Pakistan Standard Time", + "Paraguay Standard Time", + "Romance Standard Time", + "Russia Time Zone 10", + "Russia Time Zone 11", + "Russia Time Zone 3", + "Russian Standard Time", + "SA Eastern Standard Time", + "SA Pacific Standard Time", + "SA Western Standard Time", + "Saint Pierre Standard Time", + "Sakhalin Standard Time", + "Samoa Standard Time", + "Sao Tome Standard Time", + "Saratov Standard Time", + "SE Asia Standard Time", + "Singapore Standard Time", + "South Africa Standard Time", + "Sri Lanka Standard Time", + "Sudan Standard Time", + "Syria Standard Time", + "Taipei Standard Time", + "Tasmania Standard Time", + "Tocantins Standard Time", + "Tokyo Standard Time", + "Tomsk Standard Time", + "Tonga Standard Time", + "Transbaikal Standard Time", + "Turkey Standard Time", + "Turks And Caicos Standard Time", + "Ulaanbaatar Standard Time", + "US Eastern Standard Time", + "US Mountain Standard Time", + "UTC", + "UTC-02", + "UTC-08", + "UTC-09", + "UTC-11", + "UTC+12", + "UTC+13", + "Venezuela Standard Time", + "Vladivostok Standard Time", + "W. Australia Standard Time", + "W. Central Africa Standard Time", + "W. Europe Standard Time", + "W. Mongolia Standard Time", + "West Asia Standard Time", + "West Bank Standard Time", + "West Pacific Standard Time", + "Yakutsk Standard Time" + ] + }, + "alreadyHaveAWindowsServerLicense": { + "type": "string", + "defaultValue": "No", + "allowedValues": [ + "Yes", + "No" + ], + "metadata": { + "description": "By selecting Yes, you confirm you have an eligible Windows Server license with Software Assurance or Windows Server subscription to apply this Azure Hybrid Benefit. You can read more about compliance here: http://go.microsoft.com/fwlink/?LinkId=859786" + } + } + }, + "variables": { + "dataDisksCount": 8, + "dscUri": "https://github.com/Azure/AzureStackHCI-EvalGuide/raw/main/deployment/dsc/azshcihost.zip", + "artifactsLocation": "https://raw.githubusercontent.com/Azure/AzureStackHCI-EvalGuide/main/deployment/", + "randomGUID": "[substring(uniqueString(subscription().subscriptionId, resourceGroup().id, parameters('virtualMachineName')),0,6)]", + "dnsNameForPublicIP": "[toLower(concat(parameters('virtualMachineName'), variables('randomGUID')))]", + "environment": "AD Domain", + "virtualNetworkName": "AzSHCILabvNet", + "networkInterfaceName": "AzSHCILabNIC1", + "networkSecurityGroupName": "AzSHCILabNSG", + "addressPrefix": "10.0.0.0/16", + "privateIPAddress": "10.0.0.4", + "subnetName": "AzSHCILabSubnet", + "subnetPrefix": "10.0.0.0/24", + "publicIpAddressName": "AzSHCILabPubIP", + "publicIpAddressType": "Dynamic", + "publicIpAddressSku": "Basic", + "vnetId": "[resourceId('Microsoft.Network/virtualNetworks', variables('virtualNetworkName'))]", + "subnetRef": "[concat(variables('vnetId'), '/subnets/', variables('subnetName'))]" + }, + "resources": [ + { + "name": "[variables('publicIpAddressName')]", + "type": "Microsoft.Network/publicIpAddresses", + "apiVersion": "2020-11-01", + "location": "[resourceGroup().location]", + "sku": { + "name": "[variables('publicIpAddressSku')]" + }, + "properties": { + "publicIpAllocationMethod": "[variables('publicIpAddressType')]", + "dnsSettings": { + "domainNameLabel": "[variables('dnsNameForPublicIP')]" + } + } + }, + { + "type": "Microsoft.Resources/deployments", + "apiVersion": "2021-04-01", + "name": "[variables('virtualNetworkName')]", + "properties": { + "mode": "Incremental", + "templateLink": { + "uri": "[uri(variables('artifactsLocation'), concat('json/vnet.json'))]", + "contentVersion": "1.0.0.0" + }, + "parameters": { + "virtualNetworkName": { + "value": "[variables('virtualNetworkName')]" + }, + "virtualNetworkAddressRange": { + "value": "[variables('addressPrefix')]" + }, + "subnetName": { + "value": "[variables('subnetName')]" + }, + "subnetRange": { + "value": "[variables('subnetPrefix')]" + }, + "location": { + "value": "[resourceGroup().location]" + } + } + } + }, + { + "name": "[variables('networkInterfaceName')]", + "type": "Microsoft.Network/networkInterfaces", + "apiVersion": "2020-11-01", + "location": "[resourceGroup().location]", + "dependsOn": [ + "[variables('virtualNetworkName')]", + "[concat('Microsoft.Network/publicIpAddresses/', variables('publicIpAddressName'))]", + "[concat('Microsoft.Network/networkSecurityGroups/', variables('networkSecurityGroupName'))]" + ], + "properties": { + "ipConfigurations": [ + { + "name": "ipconfig1", + "properties": { + "subnet": { + "id": "[variables('subnetRef')]" + }, + "privateIPAllocationMethod": "Static", + "privateIPAddress": "[variables('privateIPAddress')]", + "publicIpAddress": { + "id": "[resourceId('Microsoft.Network/publicIpAddresses', variables('publicIpAddressName'))]" + } + } + } + ], + "networkSecurityGroup": { + "id": "[resourceId('Microsoft.Network/networkSecurityGroups', variables('networkSecurityGroupName'))]" + } + } + }, + { + "name": "[variables('networkSecurityGroupName')]", + "type": "Microsoft.Network/networkSecurityGroups", + "apiVersion": "2020-11-01", + "location": "[resourceGroup().location]", + "properties": { + "securityRules": [ + { + "name": "rdp", + "properties": { + "priority": 1000, + "protocol": "Tcp", + "access": "Allow", + "direction": "Inbound", + "sourceAddressPrefix": "*", + "sourcePortRange": "*", + "destinationAddressPrefix": "*", + "destinationPortRange": "[parameters('customRdpPort')]" + } + } + ] + } + }, + { + "name": "[parameters('virtualMachineName')]", + "type": "Microsoft.Compute/virtualMachines", + "apiVersion": "2020-12-01", + "location": "[resourceGroup().location]", + "dependsOn": [ + "[concat('Microsoft.Network/networkInterfaces/', variables('networkInterfaceName'))]" + ], + "properties": { + "osProfile": { + "computerName": "[parameters('virtualMachineName')]", + "adminUsername": "[parameters('adminUsername')]", + "adminPassword": "[parameters('adminPassword')]", + "windowsConfiguration": { + "provisionVmAgent": true + } + }, + "hardwareProfile": { + "vmSize": "[parameters('virtualMachineSize')]" + }, + "storageProfile": { + "imageReference": { + "publisher": "MicrosoftWindowsServer", + "offer": "WindowsServer", + "sku": "[if(equals(parameters('virtualMachineGeneration'), 'Generation 2'), '2019-datacenter-gensecond', '2019-Datacenter')]", + "version": "latest" + }, + "osDisk": { + "createOption": "FromImage", + "managedDisk": { + "storageAccountType": "Standard_LRS" + } + }, + "copy": [ + { + "name": "dataDisks", + "count": "[variables('dataDisksCount')]", + "input": { + "name": "[concat(parameters('virtualMachineName'),'DataDisk',copyIndex('dataDisks'))]", + "diskSizeGB": "[parameters('dataDiskSize')]", + "lun": "[copyIndex('dataDisks')]", + "createOption": "Empty", + "caching": "None", + "managedDisk": { + "storageAccountType": "[parameters('dataDiskType')]" + } + } + } + ] + }, + "networkProfile": { + "networkInterfaces": [ + { + "id": "[resourceId('Microsoft.Network/networkInterfaces', variables('networkInterfaceName'))]" + } + ] + }, + "diagnosticsProfile": { + "bootDiagnostics": { + "enabled": true + } + }, + "licenseType": "[if(equals(parameters('alreadyHaveAWindowsServerLicense'), 'Yes'), 'Windows_Server', 'None')]" + }, + "resources": [ + { + "type": "extensions", + "name": "InstallWAC", + "apiVersion": "2021-03-01", + "location": "[resourceGroup().location]", + "dependsOn": [ + "[parameters('virtualMachineName')]" + ], + "properties": { + "publisher": "Microsoft.Compute", + "type": "CustomScriptExtension", + "typeHandlerVersion": "1.10", + "autoUpgradeMinorVersion": true, + "settings": { + "fileUris": [ + "[uri(variables('artifactsLocation'), concat('scripts/installWac.ps1'))]" + ] + }, + "protectedSettings": { + "commandToExecute": "[concat('powershell -ExecutionPolicy Unrestricted -File installWac.ps1', ' -userName ', parameters('adminUsername'))]" + } + } + }, + { + "type": "extensions", + "name": "ConfigureAzSHCIHost", + "apiVersion": "2021-03-01", + "location": "[resourceGroup().location]", + "dependsOn": [ + "[parameters('virtualMachineName')]", + "[resourceId('Microsoft.Compute/virtualMachines/extensions', parameters('virtualMachineName'), 'InstallWAC')]" + ], + "properties": { + "publisher": "Microsoft.Powershell", + "type": "DSC", + "typeHandlerVersion": "2.77", + "autoUpgradeMinorVersion": true, + "settings": { + "wmfVersion": "latest", + "configuration": { + "url": "[variables('dscUri')]", + "script": "azshcihost.ps1", + "function": "AzSHCIHost" + }, + "configurationArguments": { + "DomainName": "[parameters('domainName')]", + "environment": "[variables('environment')]", + "enableDHCP": "[parameters('enableDHCP')]", + "customRdpPort": "[parameters('customRdpPort')]" + } + }, + "protectedSettings": { + "configurationArguments": { + "adminCreds": { + "UserName": "[parameters('adminUsername')]", + "Password": "[parameters('adminPassword')]" + } + } + } + } + } + ] + }, + { + "name": "[concat('shutdown-computevm-', parameters('virtualMachineName'))]", + "type": "Microsoft.DevTestLab/schedules", + "apiVersion": "2018-09-15", + "location": "[resourceGroup().location]", + "properties": { + "status": "[parameters('autoShutdownStatus')]", + "taskType": "ComputeVmShutdownTask", + "dailyRecurrence": { + "time": "[parameters('autoShutdownTime')]" + }, + "timeZoneId": "[parameters('autoShutdownTimeZone')]", + "targetResourceId": "[resourceId('Microsoft.Compute/virtualMachines', parameters('virtualMachineName'))]" + }, + "dependsOn": [ + "[concat('Microsoft.Compute/virtualMachines/', parameters('virtualMachineName'))]" + ] + }, + { + "type": "Microsoft.Resources/deployments", + "apiVersion": "2021-04-01", + "name": "[concat(variables('virtualNetworkName'),'-UpdateDNS')]", + "dependsOn": [ + "[resourceId('Microsoft.Compute/virtualMachines/extensions', parameters('virtualMachineName'), 'ConfigureAzSHCIHost')]" + ], + "properties": { + "mode": "Incremental", + "templateLink": { + "uri": "[uri(variables('artifactsLocation'), concat('json/updatevnet.json'))]", + "contentVersion": "1.0.0.0" + }, + "parameters": { + "virtualNetworkName": { + "value": "[variables('virtualNetworkName')]" + }, + "virtualNetworkAddressRange": { + "value": "[variables('addressPrefix')]" + }, + "subnetName": { + "value": "[variables('subnetName')]" + }, + "subnetRange": { + "value": "[variables('subnetPrefix')]" + }, + "DNSServerAddress": { + "value": [ + "[variables('privateIPAddress')]" + ] + }, + "location": { + "value": "[resourceGroup().location]" + } + } + } + } + ], + "outputs": { + "adminUsername": { + "type": "string", + "value": "[parameters('adminUsername')]" + }, + "rdpPort": { + "type": "string", + "value": "[parameters('customRdpPort')]" + }, + "fqdn": { + "value": "[reference(resourceId('Microsoft.Network/publicIPAddresses',variables('publicIPAddressName'))).dnsSettings.fqdn]", + "type": "string" + } + } +} \ No newline at end of file diff --git a/deployment/json/updatevnet.json b/deployment/json/updatevnet.json new file mode 100644 index 0000000..1d09efb --- /dev/null +++ b/deployment/json/updatevnet.json @@ -0,0 +1,68 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "virtualNetworkName": { + "type": "string", + "metadata": { + "description": "The name of the Virtual Network to Create" + } + }, + "virtualNetworkAddressRange": { + "type": "string", + "metadata": { + "description": "The address range of the new VNET in CIDR format" + } + }, + "subnetName": { + "type": "string", + "metadata": { + "description": "The name of the subnet created in the new VNET" + } + }, + "subnetRange": { + "type": "string", + "metadata": { + "description": "The address range of the subnet created in the new VNET" + } + }, + "DNSServerAddress": { + "type": "array", + "metadata": { + "description": "The DNS address(es) of the DNS Server(s) used by the VNET" + } + }, + "location": { + "type": "string", + "metadata": { + "description": "Location for all resources." + } + } + }, + "resources": [ + { + "type": "Microsoft.Network/virtualNetworks", + "name": "[parameters('virtualNetworkName')]", + "apiVersion": "2020-11-01", + "location": "[parameters('location')]", + "properties": { + "addressSpace": { + "addressPrefixes": [ + "[parameters('virtualNetworkAddressRange')]" + ] + }, + "dhcpOptions": { + "dnsServers": "[parameters('DNSServerAddress')]" + }, + "subnets": [ + { + "name": "[parameters('subnetName')]", + "properties": { + "addressPrefix": "[parameters('subnetRange')]" + } + } + ] + } + } + ] +} \ No newline at end of file diff --git a/deployment/json/vnet.json b/deployment/json/vnet.json new file mode 100644 index 0000000..6f9f9e5 --- /dev/null +++ b/deployment/json/vnet.json @@ -0,0 +1,59 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "virtualNetworkName": { + "type": "string", + "metadata": { + "description": "The name of the Virtual Network to Create" + } + }, + "virtualNetworkAddressRange": { + "type": "string", + "metadata": { + "description": "The address range of the new VNET in CIDR format" + } + }, + "subnetName": { + "type": "string", + "metadata": { + "description": "The name of the subnet created in the new VNET" + } + }, + "subnetRange": { + "type": "string", + "metadata": { + "description": "The address range of the subnet created in the new VNET" + } + }, + "location": { + "type": "string", + "metadata": { + "description": "Location for all resources." + } + } + }, + "resources": [ + { + "type": "Microsoft.Network/virtualNetworks", + "name": "[parameters('virtualNetworkName')]", + "apiVersion": "2020-11-01", + "location": "[parameters('location')]", + "properties": { + "addressSpace": { + "addressPrefixes": [ + "[parameters('virtualNetworkAddressRange')]" + ] + }, + "subnets": [ + { + "name": "[parameters('subnetName')]", + "properties": { + "addressPrefix": "[parameters('subnetRange')]" + } + } + ] + } + } + ] +} \ No newline at end of file diff --git a/deployment/media/Add-NetNatStaticMapping.png b/deployment/media/Add-NetNatStaticMapping.png new file mode 100644 index 0000000..f49a8bf Binary files /dev/null and b/deployment/media/Add-NetNatStaticMapping.png differ diff --git a/deployment/media/Login-AzAccount.png b/deployment/media/Login-AzAccount.png new file mode 100644 index 0000000..e699085 Binary files /dev/null and b/deployment/media/Login-AzAccount.png differ diff --git a/deployment/media/aad_permissions.png b/deployment/media/aad_permissions.png new file mode 100644 index 0000000..878a07b Binary files /dev/null and b/deployment/media/aad_permissions.png differ diff --git a/deployment/media/access_web_app.png b/deployment/media/access_web_app.png new file mode 100644 index 0000000..fd8d6d6 Binary files /dev/null and b/deployment/media/access_web_app.png differ diff --git a/deployment/media/add_cluster_method1.png b/deployment/media/add_cluster_method1.png new file mode 100644 index 0000000..0587861 Binary files /dev/null and b/deployment/media/add_cluster_method1.png differ diff --git a/deployment/media/add_feed.png b/deployment/media/add_feed.png new file mode 100644 index 0000000..bdf9934 Binary files /dev/null and b/deployment/media/add_feed.png differ diff --git a/deployment/media/add_nodes_ga.png b/deployment/media/add_nodes_ga.png new file mode 100644 index 0000000..6e48eeb Binary files /dev/null and b/deployment/media/add_nodes_ga.png differ diff --git a/deployment/media/aks_azure_reg.png b/deployment/media/aks_azure_reg.png new file mode 100644 index 0000000..8ddafe1 Binary files /dev/null and b/deployment/media/aks_azure_reg.png differ diff --git a/deployment/media/aks_basics_arc.png b/deployment/media/aks_basics_arc.png new file mode 100644 index 0000000..18c1750 Binary files /dev/null and b/deployment/media/aks_basics_arc.png differ diff --git a/deployment/media/aks_basics_cluster_details (1).png b/deployment/media/aks_basics_cluster_details (1).png new file mode 100644 index 0000000..619bedf Binary files /dev/null and b/deployment/media/aks_basics_cluster_details (1).png differ diff --git a/deployment/media/aks_basics_cluster_details.png b/deployment/media/aks_basics_cluster_details.png new file mode 100644 index 0000000..eef51c8 Binary files /dev/null and b/deployment/media/aks_basics_cluster_details.png differ diff --git a/deployment/media/aks_basics_cluster_details_single.png b/deployment/media/aks_basics_cluster_details_single.png new file mode 100644 index 0000000..58bf96d Binary files /dev/null and b/deployment/media/aks_basics_cluster_details_single.png differ diff --git a/deployment/media/aks_basics_primarynp.png b/deployment/media/aks_basics_primarynp.png new file mode 100644 index 0000000..fa9885d Binary files /dev/null and b/deployment/media/aks_basics_primarynp.png differ diff --git a/deployment/media/aks_create.png b/deployment/media/aks_create.png new file mode 100644 index 0000000..52f9528 Binary files /dev/null and b/deployment/media/aks_create.png differ diff --git a/deployment/media/aks_create_complete.png b/deployment/media/aks_create_complete.png new file mode 100644 index 0000000..7e3cab4 Binary files /dev/null and b/deployment/media/aks_create_complete.png differ diff --git a/deployment/media/aks_create_start.png b/deployment/media/aks_create_start.png new file mode 100644 index 0000000..958b52a Binary files /dev/null and b/deployment/media/aks_create_start.png differ diff --git a/deployment/media/aks_dashboard.png b/deployment/media/aks_dashboard.png new file mode 100644 index 0000000..26eafbd Binary files /dev/null and b/deployment/media/aks_dashboard.png differ diff --git a/deployment/media/aks_deploy_started.png b/deployment/media/aks_deploy_started.png new file mode 100644 index 0000000..e668e31 Binary files /dev/null and b/deployment/media/aks_deploy_started.png differ diff --git a/deployment/media/aks_deploy_success.png b/deployment/media/aks_deploy_success.png new file mode 100644 index 0000000..f81f838 Binary files /dev/null and b/deployment/media/aks_deploy_success.png differ diff --git a/deployment/media/aks_extension.png b/deployment/media/aks_extension.png new file mode 100644 index 0000000..4a242d8 Binary files /dev/null and b/deployment/media/aks_extension.png differ diff --git a/deployment/media/aks_host_mgmtconfirm.png b/deployment/media/aks_host_mgmtconfirm.png new file mode 100644 index 0000000..45a13b8 Binary files /dev/null and b/deployment/media/aks_host_mgmtconfirm.png differ diff --git a/deployment/media/aks_hostconfig_credssp.png b/deployment/media/aks_hostconfig_credssp.png new file mode 100644 index 0000000..0b83472 Binary files /dev/null and b/deployment/media/aks_hostconfig_credssp.png differ diff --git a/deployment/media/aks_hostconfig_hostdetails.png b/deployment/media/aks_hostconfig_hostdetails.png new file mode 100644 index 0000000..cac3352 Binary files /dev/null and b/deployment/media/aks_hostconfig_hostdetails.png differ diff --git a/deployment/media/aks_hostconfig_hostdetails_single.png b/deployment/media/aks_hostconfig_hostdetails_single.png new file mode 100644 index 0000000..9ee7974 Binary files /dev/null and b/deployment/media/aks_hostconfig_hostdetails_single.png differ diff --git a/deployment/media/aks_hostconfig_lb.png b/deployment/media/aks_hostconfig_lb.png new file mode 100644 index 0000000..cb686cf Binary files /dev/null and b/deployment/media/aks_hostconfig_lb.png differ diff --git a/deployment/media/aks_hostconfig_vmnet.png b/deployment/media/aks_hostconfig_vmnet.png new file mode 100644 index 0000000..2cef674 Binary files /dev/null and b/deployment/media/aks_hostconfig_vmnet.png differ diff --git a/deployment/media/aks_hostconfig_vmnet_int.png b/deployment/media/aks_hostconfig_vmnet_int.png new file mode 100644 index 0000000..3d3f283 Binary files /dev/null and b/deployment/media/aks_hostconfig_vmnet_int.png differ diff --git a/deployment/media/aks_in_arc.png b/deployment/media/aks_in_arc.png new file mode 100644 index 0000000..9fd3925 Binary files /dev/null and b/deployment/media/aks_in_arc.png differ diff --git a/deployment/media/aks_node_pools.png b/deployment/media/aks_node_pools.png new file mode 100644 index 0000000..8f8579c Binary files /dev/null and b/deployment/media/aks_node_pools.png differ diff --git a/deployment/media/aks_runtime.png b/deployment/media/aks_runtime.png new file mode 100644 index 0000000..fa3ee72 Binary files /dev/null and b/deployment/media/aks_runtime.png differ diff --git a/deployment/media/aks_virtual_networking.png b/deployment/media/aks_virtual_networking.png new file mode 100644 index 0000000..1ed3088 Binary files /dev/null and b/deployment/media/aks_virtual_networking.png differ diff --git a/deployment/media/akshci_config.png b/deployment/media/akshci_config.png new file mode 100644 index 0000000..288ea4b Binary files /dev/null and b/deployment/media/akshci_config.png differ diff --git a/deployment/media/akshci_config_new.png b/deployment/media/akshci_config_new.png new file mode 100644 index 0000000..008528a Binary files /dev/null and b/deployment/media/akshci_config_new.png differ diff --git a/deployment/media/akshci_powershell_folders.png b/deployment/media/akshci_powershell_folders.png new file mode 100644 index 0000000..3adf54c Binary files /dev/null and b/deployment/media/akshci_powershell_folders.png differ diff --git a/deployment/media/akshci_wac_folders.png b/deployment/media/akshci_wac_folders.png new file mode 100644 index 0000000..0deafd7 Binary files /dev/null and b/deployment/media/akshci_wac_folders.png differ diff --git a/deployment/media/akshcievalguide.png b/deployment/media/akshcievalguide.png new file mode 100644 index 0000000..75ce1de Binary files /dev/null and b/deployment/media/akshcievalguide.png differ diff --git a/deployment/media/akshcihost_in_wac.png b/deployment/media/akshcihost_in_wac.png new file mode 100644 index 0000000..02cab73 Binary files /dev/null and b/deployment/media/akshcihost_in_wac.png differ diff --git a/deployment/media/allow_popup_edge.png b/deployment/media/allow_popup_edge.png new file mode 100644 index 0000000..475c636 Binary files /dev/null and b/deployment/media/allow_popup_edge.png differ diff --git a/deployment/media/auto_shutdown.png b/deployment/media/auto_shutdown.png new file mode 100644 index 0000000..46fbb70 Binary files /dev/null and b/deployment/media/auto_shutdown.png differ diff --git a/deployment/media/available_extensions.png b/deployment/media/available_extensions.png new file mode 100644 index 0000000..72b2861 Binary files /dev/null and b/deployment/media/available_extensions.png differ diff --git a/deployment/media/azure_arm.png b/deployment/media/azure_arm.png new file mode 100644 index 0000000..31da8cc Binary files /dev/null and b/deployment/media/azure_arm.png differ diff --git a/deployment/media/azure_blob_ga.png b/deployment/media/azure_blob_ga.png new file mode 100644 index 0000000..d49931e Binary files /dev/null and b/deployment/media/azure_blob_ga.png differ diff --git a/deployment/media/azure_cloud_witness_ga.png b/deployment/media/azure_cloud_witness_ga.png new file mode 100644 index 0000000..76645e9 Binary files /dev/null and b/deployment/media/azure_cloud_witness_ga.png differ diff --git a/deployment/media/azure_keys_ga.png b/deployment/media/azure_keys_ga.png new file mode 100644 index 0000000..1be82a8 Binary files /dev/null and b/deployment/media/azure_keys_ga.png differ diff --git a/deployment/media/azure_login_reg.png b/deployment/media/azure_login_reg.png new file mode 100644 index 0000000..b9d449d Binary files /dev/null and b/deployment/media/azure_login_reg.png differ diff --git a/deployment/media/azure_portal_hcicluster.png b/deployment/media/azure_portal_hcicluster.png new file mode 100644 index 0000000..5f8d194 Binary files /dev/null and b/deployment/media/azure_portal_hcicluster.png differ diff --git a/deployment/media/azure_subscriptions_ga.png b/deployment/media/azure_subscriptions_ga.png new file mode 100644 index 0000000..60de8b8 Binary files /dev/null and b/deployment/media/azure_subscriptions_ga.png differ diff --git a/deployment/media/azure_vm_custom_template.png b/deployment/media/azure_vm_custom_template.png new file mode 100644 index 0000000..739cb5d Binary files /dev/null and b/deployment/media/azure_vm_custom_template.png differ diff --git a/deployment/media/azure_vm_custom_template_complete.png b/deployment/media/azure_vm_custom_template_complete.png new file mode 100644 index 0000000..b0314bf Binary files /dev/null and b/deployment/media/azure_vm_custom_template_complete.png differ diff --git a/deployment/media/azure_vm_custom_template_completed.png b/deployment/media/azure_vm_custom_template_completed.png new file mode 100644 index 0000000..dca00a4 Binary files /dev/null and b/deployment/media/azure_vm_custom_template_completed.png differ diff --git a/deployment/media/azure_vm_custom_template_new.png b/deployment/media/azure_vm_custom_template_new.png new file mode 100644 index 0000000..42e0279 Binary files /dev/null and b/deployment/media/azure_vm_custom_template_new.png differ diff --git a/deployment/media/azure_vm_search.png b/deployment/media/azure_vm_search.png new file mode 100644 index 0000000..96be863 Binary files /dev/null and b/deployment/media/azure_vm_search.png differ diff --git a/deployment/media/azure_vm_search_ga.png b/deployment/media/azure_vm_search_ga.png new file mode 100644 index 0000000..759ca17 Binary files /dev/null and b/deployment/media/azure_vm_search_ga.png differ diff --git a/deployment/media/azure_vote_app.png b/deployment/media/azure_vote_app.png new file mode 100644 index 0000000..fdcb3b5 Binary files /dev/null and b/deployment/media/azure_vote_app.png differ diff --git a/deployment/media/connect_to_vm.png b/deployment/media/connect_to_vm.png new file mode 100644 index 0000000..ac824d5 Binary files /dev/null and b/deployment/media/connect_to_vm.png differ diff --git a/deployment/media/connect_to_vm_properties.png b/deployment/media/connect_to_vm_properties.png new file mode 100644 index 0000000..acc203c Binary files /dev/null and b/deployment/media/connect_to_vm_properties.png differ diff --git a/deployment/media/create_cluster_method1.png b/deployment/media/create_cluster_method1.png new file mode 100644 index 0000000..0587861 Binary files /dev/null and b/deployment/media/create_cluster_method1.png differ diff --git a/deployment/media/create_cluster_method2.png b/deployment/media/create_cluster_method2.png new file mode 100644 index 0000000..eeef2bc Binary files /dev/null and b/deployment/media/create_cluster_method2.png differ diff --git a/deployment/media/default_browser.png b/deployment/media/default_browser.png new file mode 100644 index 0000000..1582cd1 Binary files /dev/null and b/deployment/media/default_browser.png differ diff --git a/deployment/media/deployment_complete.png b/deployment/media/deployment_complete.png new file mode 100644 index 0000000..9033d1f Binary files /dev/null and b/deployment/media/deployment_complete.png differ diff --git a/deployment/media/extension_update.png b/deployment/media/extension_update.png new file mode 100644 index 0000000..df0fb61 Binary files /dev/null and b/deployment/media/extension_update.png differ diff --git a/deployment/media/extension_update_needed.png b/deployment/media/extension_update_needed.png new file mode 100644 index 0000000..caa75fc Binary files /dev/null and b/deployment/media/extension_update_needed.png differ diff --git a/deployment/media/flow_chart_ga.png b/deployment/media/flow_chart_ga.png new file mode 100644 index 0000000..c4416cd Binary files /dev/null and b/deployment/media/flow_chart_ga.png differ diff --git a/deployment/media/get-dscconfigurationstatus.png b/deployment/media/get-dscconfigurationstatus.png new file mode 100644 index 0000000..293d18d Binary files /dev/null and b/deployment/media/get-dscconfigurationstatus.png differ diff --git a/deployment/media/get-dscconfigurationstatus2.png b/deployment/media/get-dscconfigurationstatus2.png new file mode 100644 index 0000000..be6fee9 Binary files /dev/null and b/deployment/media/get-dscconfigurationstatus2.png differ diff --git a/deployment/media/get_akshcicluster.png b/deployment/media/get_akshcicluster.png new file mode 100644 index 0000000..db1f8e5 Binary files /dev/null and b/deployment/media/get_akshcicluster.png differ diff --git a/deployment/media/get_akshcicluster_2.png b/deployment/media/get_akshcicluster_2.png new file mode 100644 index 0000000..7118107 Binary files /dev/null and b/deployment/media/get_akshcicluster_2.png differ diff --git a/deployment/media/get_akshcicluster_3.png b/deployment/media/get_akshcicluster_3.png new file mode 100644 index 0000000..ac97544 Binary files /dev/null and b/deployment/media/get_akshcicluster_3.png differ diff --git a/deployment/media/get_akshcicluster_new.png b/deployment/media/get_akshcicluster_new.png new file mode 100644 index 0000000..24b3092 Binary files /dev/null and b/deployment/media/get_akshcicluster_new.png differ diff --git a/deployment/media/get_akshcicluster_wac.png b/deployment/media/get_akshcicluster_wac.png new file mode 100644 index 0000000..4f9300e Binary files /dev/null and b/deployment/media/get_akshcicluster_wac.png differ diff --git a/deployment/media/get_akshcicluster_wac1.png b/deployment/media/get_akshcicluster_wac1.png new file mode 100644 index 0000000..aa8b64e Binary files /dev/null and b/deployment/media/get_akshcicluster_wac1.png differ diff --git a/deployment/media/get_akshcicluster_wac2.png b/deployment/media/get_akshcicluster_wac2.png new file mode 100644 index 0000000..0e17f1a Binary files /dev/null and b/deployment/media/get_akshcicluster_wac2.png differ diff --git a/deployment/media/get_akshcicluster_wac3.png b/deployment/media/get_akshcicluster_wac3.png new file mode 100644 index 0000000..6ea0f98 Binary files /dev/null and b/deployment/media/get_akshcicluster_wac3.png differ diff --git a/deployment/media/get_akshcicred.png b/deployment/media/get_akshcicred.png new file mode 100644 index 0000000..cd61f13 Binary files /dev/null and b/deployment/media/get_akshcicred.png differ diff --git a/deployment/media/get_akshcicred_2.png b/deployment/media/get_akshcicred_2.png new file mode 100644 index 0000000..b735cac Binary files /dev/null and b/deployment/media/get_akshcicred_2.png differ diff --git a/deployment/media/get_akshcikubernetesversion.png b/deployment/media/get_akshcikubernetesversion.png new file mode 100644 index 0000000..692fda2 Binary files /dev/null and b/deployment/media/get_akshcikubernetesversion.png differ diff --git a/deployment/media/get_akshcinodepool_wac.png b/deployment/media/get_akshcinodepool_wac.png new file mode 100644 index 0000000..df71440 Binary files /dev/null and b/deployment/media/get_akshcinodepool_wac.png differ diff --git a/deployment/media/get_module_functions.png b/deployment/media/get_module_functions.png new file mode 100644 index 0000000..c178f11 Binary files /dev/null and b/deployment/media/get_module_functions.png differ diff --git a/deployment/media/get_net_nat.png b/deployment/media/get_net_nat.png new file mode 100644 index 0000000..126eb52 Binary files /dev/null and b/deployment/media/get_net_nat.png differ diff --git a/deployment/media/grant_folder_permissions.png b/deployment/media/grant_folder_permissions.png new file mode 100644 index 0000000..4023f7a Binary files /dev/null and b/deployment/media/grant_folder_permissions.png differ diff --git a/deployment/media/initialize_akshcinode.png b/deployment/media/initialize_akshcinode.png new file mode 100644 index 0000000..97e7818 Binary files /dev/null and b/deployment/media/initialize_akshcinode.png differ diff --git a/deployment/media/install_akshci.png b/deployment/media/install_akshci.png new file mode 100644 index 0000000..3e261fa Binary files /dev/null and b/deployment/media/install_akshci.png differ diff --git a/deployment/media/installed_extensions.png b/deployment/media/installed_extensions.png new file mode 100644 index 0000000..c45092d Binary files /dev/null and b/deployment/media/installed_extensions.png differ diff --git a/deployment/media/installed_extensions_cluster.png b/deployment/media/installed_extensions_cluster.png new file mode 100644 index 0000000..44c7a1a Binary files /dev/null and b/deployment/media/installed_extensions_cluster.png differ diff --git a/deployment/media/kubectl_apply.png b/deployment/media/kubectl_apply.png new file mode 100644 index 0000000..065e63d Binary files /dev/null and b/deployment/media/kubectl_apply.png differ diff --git a/deployment/media/kubectl_get_nodes.png b/deployment/media/kubectl_get_nodes.png new file mode 100644 index 0000000..a789431 Binary files /dev/null and b/deployment/media/kubectl_get_nodes.png differ diff --git a/deployment/media/kubectl_get_pods.png b/deployment/media/kubectl_get_pods.png new file mode 100644 index 0000000..1e9bec7 Binary files /dev/null and b/deployment/media/kubectl_get_pods.png differ diff --git a/deployment/media/kubectl_get_pods_scaled.png b/deployment/media/kubectl_get_pods_scaled.png new file mode 100644 index 0000000..896c104 Binary files /dev/null and b/deployment/media/kubectl_get_pods_scaled.png differ diff --git a/deployment/media/kubectl_get_service.png b/deployment/media/kubectl_get_service.png new file mode 100644 index 0000000..2f3d3ac Binary files /dev/null and b/deployment/media/kubectl_get_service.png differ diff --git a/deployment/media/kubectl_scale.png b/deployment/media/kubectl_scale.png new file mode 100644 index 0000000..8ee6b40 Binary files /dev/null and b/deployment/media/kubectl_scale.png differ diff --git a/deployment/media/kubectl_service.png b/deployment/media/kubectl_service.png new file mode 100644 index 0000000..c56024a Binary files /dev/null and b/deployment/media/kubectl_service.png differ diff --git a/deployment/media/nested.png b/deployment/media/nested.png new file mode 100644 index 0000000..fe94adb Binary files /dev/null and b/deployment/media/nested.png differ diff --git a/deployment/media/nested_virt.png b/deployment/media/nested_virt.png new file mode 100644 index 0000000..da1cb4a Binary files /dev/null and b/deployment/media/nested_virt.png differ diff --git a/deployment/media/nested_virt_akshci_ga.png b/deployment/media/nested_virt_akshci_ga.png new file mode 100644 index 0000000..b4e828a Binary files /dev/null and b/deployment/media/nested_virt_akshci_ga.png differ diff --git a/deployment/media/nested_virt_arch_ga.png b/deployment/media/nested_virt_arch_ga.png new file mode 100644 index 0000000..11d3072 Binary files /dev/null and b/deployment/media/nested_virt_arch_ga.png differ diff --git a/deployment/media/nested_virt_arch_ga_oct21.png b/deployment/media/nested_virt_arch_ga_oct21.png new file mode 100644 index 0000000..fe93540 Binary files /dev/null and b/deployment/media/nested_virt_arch_ga_oct21.png differ diff --git a/deployment/media/nested_virt_nodes_ga.png b/deployment/media/nested_virt_nodes_ga.png new file mode 100644 index 0000000..ac10fb6 Binary files /dev/null and b/deployment/media/nested_virt_nodes_ga.png differ diff --git a/deployment/media/new_akshcicluster.png b/deployment/media/new_akshcicluster.png new file mode 100644 index 0000000..a41eaaa Binary files /dev/null and b/deployment/media/new_akshcicluster.png differ diff --git a/deployment/media/new_security_rule.png b/deployment/media/new_security_rule.png new file mode 100644 index 0000000..78bf1ea Binary files /dev/null and b/deployment/media/new_security_rule.png differ diff --git a/deployment/media/powershell_vm_deployed.png b/deployment/media/powershell_vm_deployed.png new file mode 100644 index 0000000..3da8b89 Binary files /dev/null and b/deployment/media/powershell_vm_deployed.png differ diff --git a/deployment/media/reg_check.png b/deployment/media/reg_check.png new file mode 100644 index 0000000..df5ee15 Binary files /dev/null and b/deployment/media/reg_check.png differ diff --git a/deployment/media/register_azshci_ga.png b/deployment/media/register_azshci_ga.png new file mode 100644 index 0000000..f6be1a4 Binary files /dev/null and b/deployment/media/register_azshci_ga.png differ diff --git a/deployment/media/register_wac_azure.png b/deployment/media/register_wac_azure.png new file mode 100644 index 0000000..55014e7 Binary files /dev/null and b/deployment/media/register_wac_azure.png differ diff --git a/deployment/media/registration_rg_ga.png b/deployment/media/registration_rg_ga.png new file mode 100644 index 0000000..f325ac8 Binary files /dev/null and b/deployment/media/registration_rg_ga.png differ diff --git a/deployment/media/registration_status.png b/deployment/media/registration_status.png new file mode 100644 index 0000000..b5fe4ff Binary files /dev/null and b/deployment/media/registration_status.png differ diff --git a/deployment/media/vm_connect_ga.png b/deployment/media/vm_connect_ga.png new file mode 100644 index 0000000..a6ee91a Binary files /dev/null and b/deployment/media/vm_connect_ga.png differ diff --git a/deployment/media/vm_deployment_error.png b/deployment/media/vm_deployment_error.png new file mode 100644 index 0000000..3261791 Binary files /dev/null and b/deployment/media/vm_deployment_error.png differ diff --git a/deployment/media/wac_azshciclus_ga.png b/deployment/media/wac_azshciclus_ga.png new file mode 100644 index 0000000..ddaa6b9 Binary files /dev/null and b/deployment/media/wac_azshciclus_ga.png differ diff --git a/deployment/media/wac_azure_arc_register_error.png b/deployment/media/wac_azure_arc_register_error.png new file mode 100644 index 0000000..2937c75 Binary files /dev/null and b/deployment/media/wac_azure_arc_register_error.png differ diff --git a/deployment/media/wac_azure_connect (1).png b/deployment/media/wac_azure_connect (1).png new file mode 100644 index 0000000..a7d1dbb Binary files /dev/null and b/deployment/media/wac_azure_connect (1).png differ diff --git a/deployment/media/wac_azure_connect (2).png b/deployment/media/wac_azure_connect (2).png new file mode 100644 index 0000000..4f85133 Binary files /dev/null and b/deployment/media/wac_azure_connect (2).png differ diff --git a/deployment/media/wac_azure_connect.png b/deployment/media/wac_azure_connect.png new file mode 100644 index 0000000..a382d3d Binary files /dev/null and b/deployment/media/wac_azure_connect.png differ diff --git a/deployment/media/wac_azure_key_ga.png b/deployment/media/wac_azure_key_ga.png new file mode 100644 index 0000000..c4f7a0f Binary files /dev/null and b/deployment/media/wac_azure_key_ga.png differ diff --git a/deployment/media/wac_azure_permissions.png b/deployment/media/wac_azure_permissions.png new file mode 100644 index 0000000..64c4434 Binary files /dev/null and b/deployment/media/wac_azure_permissions.png differ diff --git a/deployment/media/wac_azure_reg_dashboard.png b/deployment/media/wac_azure_reg_dashboard.png new file mode 100644 index 0000000..79b4b5d Binary files /dev/null and b/deployment/media/wac_azure_reg_dashboard.png differ diff --git a/deployment/media/wac_azure_reg_dashboard_2.png b/deployment/media/wac_azure_reg_dashboard_2.png new file mode 100644 index 0000000..3d4b820 Binary files /dev/null and b/deployment/media/wac_azure_reg_dashboard_2.png differ diff --git a/deployment/media/wac_azure_reg_dashboard_3.png b/deployment/media/wac_azure_reg_dashboard_3.png new file mode 100644 index 0000000..40955a5 Binary files /dev/null and b/deployment/media/wac_azure_reg_dashboard_3.png differ diff --git a/deployment/media/wac_azure_register.png b/deployment/media/wac_azure_register.png new file mode 100644 index 0000000..07f6247 Binary files /dev/null and b/deployment/media/wac_azure_register.png differ diff --git a/deployment/media/wac_azuread_confirm.png b/deployment/media/wac_azuread_confirm.png new file mode 100644 index 0000000..c2f41f9 Binary files /dev/null and b/deployment/media/wac_azuread_confirm.png differ diff --git a/deployment/media/wac_azuread_grant.png b/deployment/media/wac_azuread_grant.png new file mode 100644 index 0000000..ce839ba Binary files /dev/null and b/deployment/media/wac_azuread_grant.png differ diff --git a/deployment/media/wac_azureadapp.png b/deployment/media/wac_azureadapp.png new file mode 100644 index 0000000..dae8819 Binary files /dev/null and b/deployment/media/wac_azureadapp.png differ diff --git a/deployment/media/wac_check_drives_ga.png b/deployment/media/wac_check_drives_ga.png new file mode 100644 index 0000000..4bc80a0 Binary files /dev/null and b/deployment/media/wac_check_drives_ga.png differ diff --git a/deployment/media/wac_clean_drives_ga.png b/deployment/media/wac_clean_drives_ga.png new file mode 100644 index 0000000..20beb6c Binary files /dev/null and b/deployment/media/wac_clean_drives_ga.png differ diff --git a/deployment/media/wac_cloud_witness_new_ga.png b/deployment/media/wac_cloud_witness_new_ga.png new file mode 100644 index 0000000..2f6b43d Binary files /dev/null and b/deployment/media/wac_cloud_witness_new_ga.png differ diff --git a/deployment/media/wac_cluster_success_ga.png b/deployment/media/wac_cluster_success_ga.png new file mode 100644 index 0000000..5732a5c Binary files /dev/null and b/deployment/media/wac_cluster_success_ga.png differ diff --git a/deployment/media/wac_cluster_type_ga.png b/deployment/media/wac_cluster_type_ga.png new file mode 100644 index 0000000..37a9173 Binary files /dev/null and b/deployment/media/wac_cluster_type_ga.png differ diff --git a/deployment/media/wac_compute_vswitch_ga.png b/deployment/media/wac_compute_vswitch_ga.png new file mode 100644 index 0000000..93256a3 Binary files /dev/null and b/deployment/media/wac_compute_vswitch_ga.png differ diff --git a/deployment/media/wac_create_clus_dhcp_ga.png b/deployment/media/wac_create_clus_dhcp_ga.png new file mode 100644 index 0000000..390b352 Binary files /dev/null and b/deployment/media/wac_create_clus_dhcp_ga.png differ diff --git a/deployment/media/wac_create_clus_static_ga.png b/deployment/media/wac_create_clus_static_ga.png new file mode 100644 index 0000000..3d22d7b Binary files /dev/null and b/deployment/media/wac_create_clus_static_ga.png differ diff --git a/deployment/media/wac_credssp_ga.png b/deployment/media/wac_credssp_ga.png new file mode 100644 index 0000000..3d4241c Binary files /dev/null and b/deployment/media/wac_credssp_ga.png differ diff --git a/deployment/media/wac_define_network_ga.png b/deployment/media/wac_define_network_ga.png new file mode 100644 index 0000000..6590a84 Binary files /dev/null and b/deployment/media/wac_define_network_ga.png differ diff --git a/deployment/media/wac_domain_joined_ga.png b/deployment/media/wac_domain_joined_ga.png new file mode 100644 index 0000000..a09a1ec Binary files /dev/null and b/deployment/media/wac_domain_joined_ga.png differ diff --git a/deployment/media/wac_enable_dedup_ga.png b/deployment/media/wac_enable_dedup_ga.png new file mode 100644 index 0000000..9b93a8a Binary files /dev/null and b/deployment/media/wac_enable_dedup_ga.png differ diff --git a/deployment/media/wac_enable_rdma.png b/deployment/media/wac_enable_rdma.png new file mode 100644 index 0000000..2c57cd8 Binary files /dev/null and b/deployment/media/wac_enable_rdma.png differ diff --git a/deployment/media/wac_fs_witness_new_ga.png b/deployment/media/wac_fs_witness_new_ga.png new file mode 100644 index 0000000..3792d5d Binary files /dev/null and b/deployment/media/wac_fs_witness_new_ga.png differ diff --git a/deployment/media/wac_host_config.png b/deployment/media/wac_host_config.png new file mode 100644 index 0000000..52ac064 Binary files /dev/null and b/deployment/media/wac_host_config.png differ diff --git a/deployment/media/wac_installed.png b/deployment/media/wac_installed.png new file mode 100644 index 0000000..9f33f62 Binary files /dev/null and b/deployment/media/wac_installed.png differ diff --git a/deployment/media/wac_installed_features_ga.png b/deployment/media/wac_installed_features_ga.png new file mode 100644 index 0000000..1f5537c Binary files /dev/null and b/deployment/media/wac_installed_features_ga.png differ diff --git a/deployment/media/wac_management_nic_ga.png b/deployment/media/wac_management_nic_ga.png new file mode 100644 index 0000000..5850d18 Binary files /dev/null and b/deployment/media/wac_management_nic_ga.png differ diff --git a/deployment/media/wac_move_ga.png b/deployment/media/wac_move_ga.png new file mode 100644 index 0000000..cb025be Binary files /dev/null and b/deployment/media/wac_move_ga.png differ diff --git a/deployment/media/wac_restart_ga.png b/deployment/media/wac_restart_ga.png new file mode 100644 index 0000000..e2060a1 Binary files /dev/null and b/deployment/media/wac_restart_ga.png differ diff --git a/deployment/media/wac_s2d_enabled_ga.png b/deployment/media/wac_s2d_enabled_ga.png new file mode 100644 index 0000000..76b883a Binary files /dev/null and b/deployment/media/wac_s2d_enabled_ga.png differ diff --git a/deployment/media/wac_singlemgmt_ga.png b/deployment/media/wac_singlemgmt_ga.png new file mode 100644 index 0000000..22b7e96 Binary files /dev/null and b/deployment/media/wac_singlemgmt_ga.png differ diff --git a/deployment/media/wac_storage_validated_ga.png b/deployment/media/wac_storage_validated_ga.png new file mode 100644 index 0000000..8b36d11 Binary files /dev/null and b/deployment/media/wac_storage_validated_ga.png differ diff --git a/deployment/media/wac_system_checks.png b/deployment/media/wac_system_checks.png new file mode 100644 index 0000000..8a30bef Binary files /dev/null and b/deployment/media/wac_system_checks.png differ diff --git a/deployment/media/wac_system_checks_single.png b/deployment/media/wac_system_checks_single.png new file mode 100644 index 0000000..fa2a7fe Binary files /dev/null and b/deployment/media/wac_system_checks_single.png differ diff --git a/deployment/media/wac_teamedmgmt_ga.png b/deployment/media/wac_teamedmgmt_ga.png new file mode 100644 index 0000000..9b87b18 Binary files /dev/null and b/deployment/media/wac_teamedmgmt_ga.png differ diff --git a/deployment/media/wac_validate_storage_ga.png b/deployment/media/wac_validate_storage_ga.png new file mode 100644 index 0000000..76161e5 Binary files /dev/null and b/deployment/media/wac_validate_storage_ga.png differ diff --git a/deployment/media/wac_validated_ga.png b/deployment/media/wac_validated_ga.png new file mode 100644 index 0000000..779ae7b Binary files /dev/null and b/deployment/media/wac_validated_ga.png differ diff --git a/deployment/media/wac_verify_network_ga.png b/deployment/media/wac_verify_network_ga.png new file mode 100644 index 0000000..883dade Binary files /dev/null and b/deployment/media/wac_verify_network_ga.png differ diff --git a/deployment/media/wac_vm001_ga.png b/deployment/media/wac_vm001_ga.png new file mode 100644 index 0000000..03a93f2 Binary files /dev/null and b/deployment/media/wac_vm001_ga.png differ diff --git a/deployment/media/wac_vm_storage_deployed_ga.png b/deployment/media/wac_vm_storage_deployed_ga.png new file mode 100644 index 0000000..c9ef29e Binary files /dev/null and b/deployment/media/wac_vm_storage_deployed_ga.png differ diff --git a/deployment/media/wac_vm_storage_ga.png b/deployment/media/wac_vm_storage_ga.png new file mode 100644 index 0000000..6fa7361 Binary files /dev/null and b/deployment/media/wac_vm_storage_ga.png differ diff --git a/deployment/media/wac_vswitches_ga.png b/deployment/media/wac_vswitches_ga.png new file mode 100644 index 0000000..6f10eeb Binary files /dev/null and b/deployment/media/wac_vswitches_ga.png differ diff --git a/deployment/scripts/installWac.ps1 b/deployment/scripts/installWac.ps1 new file mode 100644 index 0000000..c8f5f84 --- /dev/null +++ b/deployment/scripts/installWac.ps1 @@ -0,0 +1,11 @@ +$ProgressPreference = "SilentlyContinue" + +mkdir -Path "C:\WAC" + +## Download the MSI file +Invoke-WebRequest -UseBasicParsing -Uri 'https://aka.ms/WACDownload' -OutFile "C:\WAC\WindowsAdminCenter.msi" +#Invoke-WebRequest -UseBasicParsing -Uri 'https://download.microsoft.com/download/1/0/5/1059800B-F375-451C-B37E-758FFC7C8C8B/WindowsAdminCenter2009.msi' -OutFile "C:\WAC\WindowsAdminCenter.msi" + +## install Windows Admin Center +$msiArgs = @("/i", "C:\WAC\WindowsAdminCenter.msi", "/qn", "/L*v", "log.txt", "SME_PORT=443", "SSL_CERTIFICATE_OPTION=generate") +Start-Process msiexec.exe -Wait -ArgumentList $msiArgs \ No newline at end of file diff --git a/deployment/steps/1_DeployAzureVM.md b/deployment/steps/1_DeployAzureVM.md new file mode 100644 index 0000000..249725e --- /dev/null +++ b/deployment/steps/1_DeployAzureVM.md @@ -0,0 +1,233 @@ +Deploy your Azure VM (Prerequisite) +============== +Overview +----------- +With the introduction of [nested virtualization support in Azure](https://azure.microsoft.com/en-us/blog/nested-virtualization-in-azure/ "Nested virtualization announcement blog post") back in 2017, Microsoft opened the door to a number of new and interesting scenarios. Nested virtualization in Azure is particularly useful for validating configurations that would require additional hardware in your environment, such as running Hyper-V hosts and clusters. + +In this guide, you'll walk through the steps to stand up an Azure Stack HCI 20H2 infrastructure. At a high level, this will consist of deploying an Azure VM, running Windows Server 2019, to act as your main Hyper-V host - this will be automatically configured with the relevant roles and features needed for this guide. It will also download all required binaries, and deploy 2 Azure Stack HCI 20H2 nodes, ready for clustering. + +Contents +----------- +- [Overview](#overview) +- [Contents](#contents) +- [Architecture](#architecture) +- [Important Note](#important-note) +- [Get an Azure subscription](#get-an-azure-subscription) +- [Azure VM Size Considerations](#azure-vm-size-considerations) +- [Deploying the Azure VM](#deploying-the-azure-vm) +- [Access your Azure VM](#access-your-azure-vm) +- [Please Read - Finish Setup](#please-read---finish-setup) +- [Next Steps](#next-steps) +- [Troubleshooting](#troubleshooting) +- [Product improvements](#product-improvements) +- [Raising issues](#raising-issues) + +Architecture +----------- + +From an architecture perspective, the following graphic showcases the different layers and interconnections between the different components: + +![Architecture diagram for Azure Stack HCI in Azure](/deployment/media/nested_virt_arch_ga_oct21.png "Architecture diagram for Azure Stack HCI in Azure") + +The outer box represents the Azure Resource Group, which will contain all of the artifacts deployed in Azure, including the virtual machine itself, and accompaying network adapter, storage and so on. You'll deploy an Azure VM running Windows Server 2019 Datacenter. On top of this, you'll run a **2-node Azure Stack HCI 20H2 cluster**. + +Important Note +----------- +The steps outlined in this guide are **specific to running inside an Azure VM**, running on a single Windows Server 2019 OS. If you plan to use these steps in an alternative environment, such as one nested/physical on-premises, the steps may differ and certain procedures may not work. If that is the case, please refer to the official documentation. + +Get an Azure subscription +----------- +To evaluate Azure Stack HCI 20H2 in Azure, you'll need an Azure subscription. If you already have one provided by your company, you can skip this step, but if not, you have a couple of options. + +The first option would apply to Visual Studio subscribers, where you can use Azure at no extra charge. With your monthly Azure DevTest individual credit, Azure is your personal sandbox for dev/test. You can provision virtual machines, cloud services, and other Azure resources. Credit amounts vary by subscription level, but if you manage your AzSHCIHost VM run-time efficiently, you can test the scenario well within your subscription limits. + +The second option would be to sign up for a [free trial](https://azure.microsoft.com/en-us/free/ "Azure free trial link"), which gives you $200 credit for the first 30 days, and 12 months of popular services for free. The credit for the first 30 days will give you plenty of headroom to validate Azure Stack HCI. + +You can also use this same Azure subscription to integrate with Azure Arc, once the deployment is completed. + +Azure VM Size Considerations +----------- + +Now, before you deploy the VM in Azure, it's important to choose a **size** that's appropriate for your needs for this workshop, along with a preferred region. It's highly recommended to choose a VM size that has **at least 64GB memory**. This deployment, by default, recommends using a **Standard_E16s_v4**, which is a memory-optimized VM size, with 16 vCPUs, 128 GiB memory, and no temporary SSD storage. The OS drive will be the default 127 GiB in size and the Azure VM deployment will add an additional 8 data disks (32 GiB each by default), so you'll have around 256GiB to deploy Azure Stack HCI 20H2. You can also make this larger after deployment, if you wish. + +This is just one VM size that we recommend - you can adjust accordingly to suit your needs, even after deployment. The point here is, think about how large an Azure Stack HCI 20H2 infrastructure you'd like to deploy inside this Azure VM, and select an Azure VM size from there. Some potential examples would be: + +**D-series VMs (General purpose) with at least 64GB memory** + +| Size | vCPU | Memory: GiB | Temp storage (SSD): GiB | Premium Storage | +|:--|---|---|---|---| +| Standard_D16s_v3 | 16 | 64 | 128 | Yes | +| Standard_D16_v4 | 16 | 64 | 0 | No | +| **Standard_D16s_v4** | **16** | **64** | **0** | **Yes** | +| Standard_D16d_v4 | 16 | 64 | 600 | No | +| Standard_D16ds_v4 | 16 | 64 | 600 | Yes | + +For reference, the Standard_D16s_v4 VM size costs approximately US $0.77 per hour based on East US region, under a Visual Studio subscription. + +**E-series VMs (Memory optimized - Recommended for this Hybrid Workshop) with at least 64GB memory** + +| Size | vCPU | Memory: GiB | Temp storage (SSD): GiB | Premium Storage | +|:--|---|---|---|---| +| Standard_E8s_v3 | 8 | 64 | 128 | Yes | +| Standard_E8_v4 | 8 | 64 | 0 | No | +| **Standard_E8s_v4** | **8** | **64** | **0** | **Yes** | +| Standard_E8d_v4 | 8 | 64 | 300 | No | +| Standard_E8ds_v4 | 8 | 64 | 300 | Yes | +| Standard_E16s_v3 | 16 | 128 | 256 | Yes | +| **Standard_E16s_v4** | **16** | **128** | **0** | **Yes** | +| Standard_E16d_v4 | 16 | 128 | 600 | No | +| Standard_E16ds_v4 | 16 | 128 | 600 | Yes | + +For reference, the Standard_E8s_v4 VM size costs approximately US $0.50 per hour based on East US region, under a Visual Studio subscription. + +**NOTE 1** - A number of these VM sizes include temp storage, which offers high performance, but is not persistent through reboots, Azure host migrations and more. It's therefore advisable, that if you are going to be running the Azure VM for a period of time, but shutting down frequently, that you choose a VM size with no temp storage, and ensure your nested VMs are placed on the persistent data drive within the OS. + +**NOTE 2** - It's strongly recommended that you choose a VM size that supports **premium storage** - when running nested virtual machines, increasing the number of available IOPS can have a significant impact on performance, hence choosing **premium storage** over Standard HDD or Standard SSD, is strongly advised. Refer to the table above to make the most appropriate selection. + +**NOTE 3** - Please ensure that whichever VM size you choose, it [supports nested virtualization](https://docs.microsoft.com/en-us/azure/virtual-machines/acu "Nested virtualization support") and is [available in your chosen region](https://azure.microsoft.com/en-us/global-infrastructure/services/?products=virtual-machines "Virtual machines available by region"). + +Deploying the Azure VM +----------- +The guidance below provides a simple template-based option for deploying the Azure VM. The template deployment will be automated to the point of which you can proceed immediately to start creating your Azure Stack HCI 20H2 cluster, and progress through your workshop. + +### Deployment detail ### +As part of the deployment, the following steps will be **automated for you**: + +1. A Windows Server 2019 Datacenter VM will be deployed in Azure +2. 8 x 32GiB (by default) Azure Managed Disks will be attached and provisioned with a Simple Storage Space for optimal nested VM performance +3. The Hyper-V role and management tools will be installed and configured +4. An Internal vSwitch will be created and NAT configured to enable outbound networking +5. The DNS role and accompanying management tools will be installed and DNS fully configured +6. The DHCP role and accompanying management tools will be installed and DHCP fully configured. DHCP Scope will be **enabled** +7. Windows Admin Center will be installed and pre-installed extensions updated +8. The Microsoft Edge browser will be installed +9. The Azure Stack HCI 20H2 binaries will be downloaded +10. 2 x Azure Stack HCI 20H2 nodes will be created and deployed, ready to start cluster creation + +This automated deployment **should take around 50 minutes**, due to the image creation of the Azure Stack HCI 20H2 nodes - these are created, **offline patched** and deployed, which takes time. + +### Creating the VM with an Azure Resource Manager JSON Template ### +To keep things simple, and graphical, we'll show you how to deploy your VM via an Azure Resource Manager template. To simplify things further, we'll use the following buttons. + +Firstly, the **Visualize** button will launch the ARMVIZ designer view, where you will see a graphic representing the core components of the deployment, including the VM, NIC, disk and more. If you want to open this in a new tab, **hold CTRL** when you click the button. + +[![Visualize your template deployment](https://raw.githubusercontent.com/Azure/azure-quickstart-templates/master/1-CONTRIBUTION-GUIDE/images/visualizebutton.png)](http://armviz.io/#/?load=https%3A%2F%2Fraw.githubusercontent.com%2FAzure%2FAzureStackHCI-EvalGuide%2Fmain%2Fdeployment%2Fjson%2Fazshcihost.json "Visualize your template deployment") + +Secondly, the **Deploy to Azure** button, when clicked, will take you directly to the Azure portal, and upon login, provide you with a form to complete. If you want to open this in a new tab, **hold CTRL** when you click the button. + +[![Deploy to Azure](https://aka.ms/deploytoazurebutton)](https://portal.azure.com/#create/Microsoft.Template/uri/https%3A%2F%2Fraw.githubusercontent.com%2FAzure%2FAzureStackHCI-EvalGuide%2Fmain%2Fdeployment%2Fjson%2Fazshcihost.json "Deploy to Azure") + +Upon clicking the **Deploy to Azure** button, enter the details, which should look something similar to those shown below, and click **Purchase**. + +![Custom template deployment in Azure](/deployment/media/azure_vm_custom_template_new.png "Custom template deployment in Azure") + +**NOTE** - For customers with Software Assurance, Azure Hybrid Benefit for Windows Server allows you to use your on-premises Windows Server licenses and run Windows virtual machines on Azure at a reduced cost. By selecting **Yes** for the "Already have a Windows Server License", **you confirm I have an eligible Windows Server license with Software Assurance or Windows Server subscription to apply this Azure Hybrid Benefit** and have reviewed the [Azure hybrid benefit compliance](http://go.microsoft.com/fwlink/?LinkId=859786 "Azure hybrid benefit compliance document") + +The custom template will be validated, and if all of your entries are correct, you can click **Create**. Within a few minutes, your VM will be created. + +![Custom template deployment in Azure completed](/deployment/media/azure_vm_custom_template_completed.png "Custom template deployment in Azure completed") + +If you chose to **enable** the auto-shutdown for the VM, and supplied a time, and time zone, but want to also add a notification alert, simply click on the **Go to resource group** button and then perform the following steps: + +1. In the **Resource group** overview blade, click the **AzSHCIHost001** virtual machine +2. Once on the overview blade for your VM, **scroll down on the left-hand navigation**, and click on **Auto-shutdown** +3. Ensure the Enabled slider is still set to **On** and that your **time** and **time zone** information is correct +4. Click **Yes** to enable notifications, and enter a Webhook URL, or Email address +5. Click **Save** + +You'll now be notified when the VM has been successfully shut down as the requested time. + +With that completed, skip on to [connecting to your Azure VM](#connect-to-your-azure-vm) + +#### Deployment errors #### +If your Azure VM fails to deploy successfully, and the error relates to the **AzSHCIHost001/ConfigureAzSHCIHost** PowerShell DSC extension, please refer to the [troubleshooting steps below](#troubleshooting). + +Access your Azure VM +----------- + +With your Azure VM (HybridHost001) successfully deployed and configured, you're ready to connect to the VM to start the deployment of the Azure Stack HCI 20H2. + +### Connect to your Azure VM ### +Firstly, you'll need to connect into the VM, with the easiest approach being via Remote Desktop. If you're not already logged into the Azure portal, visit https://portal.azure.com/, and login with the same credentials used earlier. Once logged in, using the search box on the dashboard, enter "**azshcihost**" and once the results are returned, **click on your AzSHCIHost001 virtual machine**. + +![Virtual machine located in Azure](/deployment/media/azure_vm_search.png "Virtual machine located in Azure") + +Once you're on the Overview blade for your VM, along the top of the blade, click on **Connect** and from the drop-down options. + +![Connect to a virtual machine in Azure](/deployment/media/connect_to_vm.png "Connect to a virtual machine in Azure") + +Select **RDP**. On the newly opened Connect blade, ensure the **Public IP** is selected. Ensure the RDP port matches what you provided at deployment time. By default, this should be **3389**. Then click **Download RDP File** and select a suitable folder to store the .rdp file. + +![Configure RDP settings for Azure VM](/deployment/media/connect_to_vm_properties.png "Configure RDP settings for Azure VM") + +Once downloaded, locate the .rdp file on your local machine, and double-click to open it. Click **connect** and when prompted, enter the credentials you supplied when creating the VM earlier. **NOTE**, this should be a **domain account**, which by default, is **azshci\azureuser**. + +**Username:** azshci\azureuser +**Password:** password-you-used-at-VM-deployment-time + +Accept any certificate prompts, and within a few moments, you should be successfully logged into the Windows Server 2019 VM. + +Please Read - Finish Setup +----------- +Once the Azure VM deployment process has completed, your Azure Stack HCI 20H2 nodes are still processing changes, including adding roles and features inside the nested hosts. Please allow ~5 minutes for this process to complete and stabilize. + +You can then optionally shut down your Azure VM, should you wish to continue your evaluation on another day. + +Next Steps +----------- +In this step, you've successfully created and automatically configured your Azure VM, which will serve as the host for your Azure Stack HCI 20H2. You're now ready to move on to the next step. + +* [**Part 2** - Configure your Azure Stack HCI 20H2 Cluster](/deployment/steps/2_DeployAzSHCI.md "Configure your Azure Stack HCI 20H2 Cluster") + +Troubleshooting +----------- +From time to time, a transient, random deployment error may cause the Azure VM to show a failed deployment. This is typically caused by reboots and timeouts within the VM as part of the PowerShell DSC configuration process, in particular, when the Hyper-V role is enabled and the system reboots multiple times in quick succession. We've also seen instances where changes with Chocolatey Package Manager cause deployment issues. + +![Azure VM deployment error](/deployment/media/vm_deployment_error.png "Azure VM deployment error") + +If the error is related to the **AzSHCIHost001/ConfigureAzSHCIHost**, most likely the installation did complete successfully in the end, but to double-check, you can perform these steps: + +1. Follow the steps above to [connect to your Azure VM](#connect-to-your-azure-vm) +2. Once successfully connected, open a **PowerShell console as administrator** and run the following command to confirm the status of the last run: + +```powershell +# Check for last run +Get-DscConfigurationStatus +``` + +**NOTE** - if you receive an error message similar to *"Get-DscConfigurationStatus : Cannot invoke the Get-DscConfigurationStatus cmdlet. The `<Some DSC Process`> cmdlet is in progress and must return before Get-DscConfigurationStatus can be invoked"* you will need to **wait** until the current DSC process has completed. Once completed, you should be able to successfully run the command. + +3. When you run **Get-DscConfigurationStatus**, if you get a status of **Failure** you can re-run the DSC configuration by **running the following commands**: + +```powershell +cd "C:\Packages\Plugins\Microsoft.Powershell.DSC\*\DSCWork\azshcihost.0\AzSHCIHost" +Set-DscLocalConfigurationManager -Path . -Force +Start-DscConfiguration -Path . -Wait -Force -Verbose +``` + +4. Depending on where the initial failure happened, your VM may reboot and you will be disconnected. If that's the case, log back into the VM and wait for deployment to complete. See #2 above to check progress. Generally speaking, once you see the **Edge** and **Windows Admin Center** icons on your desktop, the process has completed. + +![Edge and Windows Admin Center icons](/deployment/media/deployment_complete.png "Edge and Windows Admin Center icons") + +5. If all goes well, you should see the DSC configuration reapplied without issues. If you then re-run the following PowerShell command, you should see success, with over **100 resources** deployed/configured. + +```powershell +# Check for last run +Get-DscConfigurationStatus +``` + +![Result of Get-DscConfigurationStatus](/deployment/media/get-dscconfigurationstatus.png "Result of Get-DscConfigurationStatus") + +**NOTE** - If this doesn't fix your issue, consider redeploying your Azure VM. If the issue persists, please **raise an issue!** + +Product improvements +----------- +If, while you work through this guide, you have an idea to make the product better, whether it's something in Azure Stack HCI, Windows Admin Center, or the Azure Arc integration and experience, let us know! We want to hear from you! + +For **Azure Stack HCI**, [Head on over to the Azure Stack HCI 20H2 Q&A forum](https://docs.microsoft.com/en-us/answers/topics/azure-stack-hci.html "Azure Stack HCI 20H2 Q&A"), where you can share your thoughts and ideas about making the technologies better and raise an issue if you're having trouble with the technology. + +Raising issues +----------- +If you notice something is wrong with this guide, such as a step isn't working, or something just doesn't make sense - help us to make this guide better! Raise an issue in GitHub, and we'll be sure to fix this as quickly as possible! + +If you're having an issue with Azure Stack HCI 20H2 **outside** of this guide, [head on over to the Azure Stack HCI 20H2 Q&A forum](https://docs.microsoft.com/en-us/answers/topics/azure-stack-hci.html "Azure Stack HCI 20H2 Q&A"), where Microsoft experts and valuable members of the community will do their best to help you. \ No newline at end of file diff --git a/deployment/steps/2_DeployAzSHCI.md b/deployment/steps/2_DeployAzSHCI.md new file mode 100644 index 0000000..fbf8f83 --- /dev/null +++ b/deployment/steps/2_DeployAzSHCI.md @@ -0,0 +1,314 @@ +Configure your Azure Stack HCI 20H2 Cluster +============== +Overview +----------- + +So far, you've deployed your Azure VM, that has all the relevant roles and features enabled, including Hyper-V, AD Domain Services, DNS and DHCP. The VM deployment also orchestrated the download of all required binaries, as well as creating and deploying 2 Azure Stack HCI 20H2 nodes, which you'll be configuring in this step. + +Contents +----------- +- [Overview](#overview) +- [Contents](#contents) +- [Architecture](#architecture) +- [Before you begin](#before-you-begin) +- [Allow popups in Edge browser](#allow-popups-in-edge-browser) +- [Creating a (local) cluster](#creating-a-local-cluster) +- [Configuring the cluster witness](#configuring-the-cluster-witness) +- [Next Steps](#next-steps) +- [Product improvements](#product-improvements) +- [Raising issues](#raising-issues) + +Architecture +----------- + +As shown on the architecture graphic below, in this step, you'll take the nodes that were previously deployed, and be **clustering them into an Azure Stack HCI 20H2 cluster**. You'll be focused on **creating a cluster in a single site**. + +![Architecture diagram for Azure Stack HCI 20H2 nested](/deployment/media/nested_virt_arch_ga_oct21.png "Architecture diagram for Azure Stack HCI 20H2 nested") + +Before you begin +----------- +With Windows Admin Center, you now have the ability to construct Azure Stack HCI 20H2 clusters from the vanilla nodes. There are no additional extensions to install, the workflow is built in and ready to go, however, it's worth checking to ensure that your Cluster Creation extension is fully up to date and making a few changes to the Edge browser. + +### Set Microsoft Edge as default browser ### + +To streamline things later, we'll set Microsoft Edge as the default browser over Internet Explorer. + +1. Inside your **AzSHCIHost001 VM**, click on Start, then type "**default browser**" (without quotes) and then under **Best match**, select **Choose a default web browser** + +![Set the default browser](/deployment/media/default_browser.png "Set the default browser") + +2. In the **Default apps** settings view, under **Web browser**, click on **Internet Explorer** +3. In the **Choose an app** popup, select **Microsoft Edge** then **close the Settings window** + +Allow popups in Edge browser +----------- +To give the optimal experience with Windows Admin Center, you should enable **Microsoft Edge** to allow popups for Windows Admin Center. + +1. Still inside your **AzSHCIHost001 VM**, double-click the **Microsoft Edge icon** on your desktop +2. Navigate to **edge://settings/content/popups** +3. Click the slider button to **disable** pop-up blocking +4. Close the **settings tab**. + +### Configure Windows Admin Center ### + +Your Azure VM deployment automatically installed the latest version of Windows Admin Center, however there are some additional configuration steps that must be performed before you can use it to deploy Azure Stack HCI. + +1. **Double-click the Windows Admin Center** shortcut on the desktop. +2. Once Windows Admin Center is open, you may receive notifications in the top-right corner, indicating that some extensions are updating automatically. **Let these finish updating before proceeding**. Windows Admin Center may refresh automatically during this process. +3. Once complete, navigate to **Settings**, then **Extensions** +4. Click on **Installed extensions** and you should see **Cluster Creation** listed as installed + +![Installed extensions in Windows Admin Center](/deployment/media/installed_extensions_cluster.png "Installed extensions in Windows Admin Center") + +____________ + +**NOTE** - Ensure that your Cluster Creation extension is the **latest available version**. If the **Status** is **Installed**, you have the latest version. If the **Status** shows **Update available (1.#.#)**, ensure you apply this update and refresh before proceeding. + +_____________ + +You're now ready to begin deployment of your Azure Stack HCI cluster with Windows Admin Center. Here are the major steps in the Create Cluster wizard in Windows Admin Center: + +* **Get Started** - ensures that each server meets the prerequisites for and features needed for cluster join +* **Networking** - assigns and configures network adapters and creates the virtual switches for each server +* **Clustering** - validates the cluster is set up correctly. For stretched clusters, also sets up up the two sites +* **Storage** - Configures Storage Spaces Direct + +### Decide on cluster type ### +Not only does Azure Stack HCI 20H2 support a cluster in a single site (or a **local cluster** as we'll refer to it going forward) consisting of between 2 and 16 nodes, but, also supports a **Stretch Cluster**, where a single cluster can have nodes distrubuted across two sites. + +* If you have 2 Azure Stack HCI 20H2 nodes, you will be able to create a **local cluster** +* If you have 4 Azure Stack HCI 20H2 nodes, you will have a choice of creating either a **local cluster** or a **stretch cluster** + +In this workshop, we'll be focusing on deploying a **local cluster** but if you're interested in deploying a stretch cluster, you can [check out the official docs](https://docs.microsoft.com/en-us/azure-stack/hci/concepts/stretched-clusters "Stretched clusters overview on Microsoft Docs") + +Creating a (local) cluster +----------- +This section will walk through the key steps for you to set up the Azure Stack HCI 20H2 cluster with the Windows Admin Center + +1. Connect to your **AzSHCIHost001**, and open **Windows Admin Center** using the shortcut on your desktop. +2. Once logged into Windows Admin Center, under **All connections**, click **Add** +3. On the **Add or create resources popup**, under **Server clusters**, click **Create new** to open the **Cluster Creation wizard** + +### Get started ### + +![Choose cluster type in the Create Cluster wizard](/deployment/media/wac_cluster_type_ga.png "Choose cluster type in the Create Cluster wizard") + +1. Ensure you select **Azure Stack HCI**, select **All servers in one site** and cick **Create** +2. On the **Check the prerequisites** page, review the requirements and click **Next** +3. On the **Add Servers** page, supply a **username**, which should be **azshci\azureuser** and **password-you-used-at-VM-deployment-time** and then one by one, enter the node names of your Azure Stack HCI 20H2 nodes (AZSHCINODE01 and AZSHCINODE02), clicking **Add** after each one has been located. Each node will be validated, and given a **Ready** status when fully validated. This may take a few moments - once you've added all nodes, click **Next** + +![Add servers in the Create Cluster wizard](/deployment/media/add_nodes_ga.png "Add servers in the Create Cluster wizard") + +4. On the **Join a domain** page, details should already be in place, as these nodes have already been joined to the domain to save time. If this wasn't the case, WAC would be able to configure this for you. Click **Next** + +![Joined the domain in the Create Cluster wizard](/deployment/media/wac_domain_joined_ga.png "Joined the domain in the Create Cluster wizard") + +1. On the **Install features** page, Windows Admin Center will query the nodes for currently installed features, and will typically request you install required features. In this case, all features have been previously installed to save time, as this would take a few moments. Once reviewed, click **Next** + +![Installing required features in the Create Cluster wizard](/deployment/media/wac_installed_features_ga.png "Installing required features in the Create Cluster wizard") + +6. On the **Install updates** page, Windows Admin Center will query the nodes for available updates, and will request you install any that are required. For the purpose of this guide and to save time, we'll ignore this and click **Next** +7. On the **Install hardware updates** page, in a nested environment this doesn't apply, so click **Next** +8. On the **Restart servers** page, if required, click **Restart servers**, otherwise, click **Next: Networking** + +![Restart nodes in the Create Cluster wizard](/deployment/media/wac_restart_ga.png "Restart nodes in the Create Cluster wizard") + +### Networking ### +With the servers configured with the appropriate features, updated and rebooted, you're ready to configure your network. You have a number of different choices here, so we'll try to explain why we're making each selection, so you can better apply it to your environment further down the road. + +Firstly, Windows Admin Center will verify your networking setup - it'll tell you how many NICs are in each node, along with relevant hardware information, MAC address and status information. Review for accuracy, and then click **Next** + +![Verify network in the Create Cluster wizard](/deployment/media/wac_verify_network_ga.png "Verify network in the Create Cluster wizard") + +The first key step with setting up the networking with Windows Admin Center, is to choose a management NIC that will be dedicated for management use. You can choose either a single NIC, or two NICs for redundancy. This step specifically designates 1 or 2 adapters that will be used by the Windows Admin Center to orchestrate the cluster creation flow. It's mandatory to select at least one of the adapters for management, and in a physical deployment, the 1GbE NICs are usually good candidates for this. + +As it stands, this is the way that the Windows Admin Center approaches the network configuration, however, if you were not using the Windows Admin Center, through PowerShell, there are a number of different ways to configure the network to meet your needs. We will work through the Windows Admin Center approach in this guide. + +#### Network Setup Overview #### +Each of your Azure Stack HCI 20H2 nodes should have 4 NICs. For this simple evaluation, you'll dedicate the NICs in the following way: + +* 1 NIC will be dedicated to management. This NIC will reside on the 192.168.0.0/16 subnet. No virtual switch will be attached to this NIC. +* 1 NIC will be dedicated to VM traffic. A virtual switch will be attached to this NIC and the Azure Stack HCI 20H2 host will no longer use this NIC for it's own traffic. +* 2 NICs will be dedicated to storage traffic. They will reside on 2 separate subnets, 10.10.11.0/24 and 10.10.12.0/24. No virtual switches will be attached to these NICs. + +Again, this is just one **example** network configuration for the simple purpose of evaluation. + +1. Back in the Windows Admin Center, on the **Select the adapters to use for management** page, ensure you select the **One physical network adapters for management** box + +![Select management adapter in the Create Cluster wizard](/deployment/media/wac_management_nic_ga.png "Select management adapter in the Create Cluster wizard") + +2. Then, for each node, **select the highlighted NIC** that will be dedicated for management. The reason only one NIC is highlighted, is because this is the only NICs that has an IP address on the same network as the WAC instance. Once you've finished your selections, scroll to the bottom, then click **Apply and test**. This will take a few moments. + +![Select management adapters in the Create Cluster wizard](/deployment/media/wac_singlemgmt_ga.png "Select management adapters in the Create Cluster wizard") + +3. Windows Admin Center will then apply the configuration to your NICs. When complete and successful, click **Next** +4. On the **Virtual Switch** page, you have a number of options + +![Select vSwitch in the Create Cluster wizard](/deployment/media/wac_vswitches_ga.png "Select vSwitch in the Create Cluster wizard") + +* **Create one virtual switch for compute and storage together** - in this configuration, your Azure Stack HCI 20H2 nodes will create a vSwitch, comprised of multiple NICs, and the bandwidth available across these NICs will be shared by the Azure Stack HCI 20H2 nodes themselves, for storage traffic, and in addition, any VMs you deploy on top of the nodes, will also share this bandwidth. +* **Create one virtual switch for compute only** - in this configuration, you would leave some NICs dedicated to storage traffic, and have a set of NICs attached to a vSwitch, to which your VMs traffic would be dedicated. +* **Create two virtual switches** - in this configuration, you can create separate vSwitches, each attached to different sets of underlying NICs. This may be useful if you wish to dedicate a set of underlying NICs to VM traffic, and another set to storage traffic, but wish to have vNICs used for storage communication instead of the underlying NICs. +* You also have a check-box for **Skip virtual switch creation** - if you want to define things later, that's fine too + +1. Select the **Create one virtual switch for compute only**, and select the NIC on each node with the **10.10.13.x IP address**, then click **Next** + +![Create single vSwitch for Compute in the Create Cluster wizard](/deployment/media/wac_compute_vswitch_ga.png "Create single vSwitch for Compute in the Create Cluster wizard") + +6. On the **RDMA** page, you're now able to configure the appropriate RDMA settings for your host networks. If you do choose to tick the box, in a nested environment, you'll be presented with an error, so click **Next** + +![Error message when configuring RDMA in a nested environment](/deployment/media/wac_enable_rdma.png "Error message when configuring RDMA in a nested environment") + +7. On the **Define networks** page, this is where you can define the specific networks, separate subnets, and optionally apply VLANs. In this **nested environment**, we now have 3 NICs remaining. Configure your remaining NICs as follows, by clicking on a field in the table and entering the appropriate information. + +**NOTE** - we have a simple flat network in this configuration. One of the NICs have been claimed by the Management NIC, The remaining NICs will be show in the table in WAC, so ensure they align with the information below. WAC won't allow you to proceed unless everything aligns correctly. + +| Node | Name | IP Address | Subnet Mask +| :-- | :-- | :-- | :-- | +| AZSHCINODE01 | Storage 1 | 10.10.11.1 | 24 +| AZSHCINODE01 | Storage 2 | 10.10.12.1 | 24 +| AZSHCINODE01 | VM | 10.10.13.1 | 24 +| AZSHCINODE02 | Storage 1 | 10.10.11.2 | 24 +| AZSHCINODE02 | Storage 2 | 10.10.12.2 | 24 +| AZSHCINODE02 | VM | 10.10.13.2 | 24 + +When you click **Apply and test**, Windows Admin Center validates network connectivity between the adapters in the same VLAN and subnet, which may take a few moments. Once complete, your configuration should look similar to this: + +![Define networks in the Create Cluster wizard](/deployment/media/wac_define_network_ga.png "Define networks in the Create Cluster wizard") + +**NOTE**, You *may* be prompted with a **Credential Security Service Provider (CredSSP)** box - read the information, then click **Yes** + +![Validate cluster in the Create Cluster wizard](/deployment/media/wac_credssp_ga.png "Validate cluster in the Create Cluster wizard") + +8. Once the networks have been verified, you can optionally review the networking test report, and once complete, click **Next** + +9. Once changes have been successfully applied, click **Next: Clustering** + +### Clustering ### +With the network configured for the workshop environment, it's time to construct the local cluster. + +1. At the start of the **Cluster** wizard, on the **Validate the cluster** page, click **Validate**. + +2. Cluster validation will then start, and will take a few moments to complete - once completed, you should see a successful message. + +**NOTE** - Cluster validation is intended to catch hardware or configuration problems before a cluster goes into production. Cluster validation helps to ensure that the Azure Stack HCI 20H2 solution that you're about to deploy is truly dependable. You can also use cluster validation on configured failover clusters as a diagnostic tool. If you're interested in learning more about Cluster Validation, [check out the official docs](https://docs.microsoft.com/en-us/azure-stack/hci/deploy/validate "Cluster validation official documentation"). + +![Validation complete in the Create Cluster wizard](/deployment/media/wac_validated_ga.png "Validation complete in the Create Cluster wizard") + +1. Optionally, if you want to review the validation report, click on **Download report** and open the file in your browser. +2. Back in the **Validate the cluster** screen, click **Next** +3. On the **Create the cluster** page, enter your **cluster name** as **AZSHCICLUS** (IMPORTANT - make sure you use AZSHCICLUS as the name of the cluster as we pre-created the AD object in Active Directory to reflect this name) +4. Under **IP address**, click **Specify one or more static addresses**, and enter **192.168.0.4** +5. Expand **Advanced** and review the settings, then click **Create cluster** + +![Finalize cluster creation in the Create Cluster wizard](/deployment/media/wac_create_clus_static_ga.png "Finalize cluster creation in the Create Cluster wizard") + +6. With all settings confirmed, click **Create cluster**. This will take a few moments. Once complete, click **Next: Storage** + +![Cluster creation successful in the Create Cluster wizard](/deployment/media/wac_cluster_success_ga.png "Cluster creation successful in the Create Cluster wizard") + + +With the cluster successfully created, you're now good to proceed on to configuring your storage. Whilst less important in a fresh nested environment, it's always good to start from a clean slate, so first, you'll clean the drives before configuring storage. + +1. On the storage landing page within the Create Cluster wizard, click **Erase Drives**, and when prompted, with **You're about to erase all existing data**, click **Erase drives**. Once complete, you should have a successful confirmation message, then click **Next** + +![Cleaning drives in the Create Cluster wizard](/deployment/media/wac_clean_drives_ga.png "Cleaning drives in the Create Cluster wizard") + +2. On the **Check drives** page, validate that all your drives have been detected, and show correctly. As these are virtual disks in a nested environment, they won't display as SSD or HDD etc. You should have **4 data drives** per node. Once verified, click **Next** + +![Verified drives in the Create Cluster wizard](/deployment/media/wac_check_drives_ga.png "Verified drives in the Create Cluster wizard") + +3. Storage Spaces Direct validation tests will then automatically run, which will take a few moments. + +![Verifying Storage Spaces Direct in the Create Cluster wizard](/deployment/media/wac_validate_storage_ga.png "Verifying Storage Spaces Direct in the Create Cluster wizard") + +4. Once completed, you should see a successful confirmation. You can scroll through the brief list of tests, or alternatively, click to **Download report** to view more detailed information, then click **Next** + +![Storage verified in the Create Cluster wizard](/deployment/media/wac_storage_validated_ga.png "Storage verified in the Create Cluster wizard") + +5. The final step with storage, is to **Enable Storage Spaces Direct**, so click **Enable**. This will take a few moments. + +![Storage Spaces Direct enabled in the Create Cluster wizard](/deployment/media/wac_s2d_enabled_ga.png "Storage Spaces Direct enabled in the Create Cluster wizard") + +6. With Storage Spaces Direct enabled, click **Next:SDN** + +### SDN ### + +With Storage configured, for the purpose of this lab, we will skip the SDN configuration. + +1. On the **Define the Network Controller cluster** page, click **Skip** +2. On the **confirmation page**, click on **Go to connections list** + +Configuring the cluster witness +----------- +By deploying an Azure Stack HCI 20H2 cluster, you're providing high availability for workloads. These resources are considered highly available if the nodes that host resources are up; however, the cluster generally requires more than half the nodes to be running, which is known as having quorum. + +Quorum is designed to prevent split-brain scenarios which can happen when there is a partition in the network and subsets of nodes cannot communicate with each other. This can cause both subsets of nodes to try to own the workload and write to the same disk which can lead to numerous problems. However, this is prevented with Failover Clustering's concept of quorum which forces only one of these groups of nodes to continue running, so only one of these groups will stay online. + +In this step, we're going to utilize a **Cloud witness** to help provide quorum. If you want to learn more about quorum, [check out the official documentation.](https://docs.microsoft.com/en-us/azure-stack/hci/concepts/quorum "Official documentation about Cluster quorum") + +As part of this guide, we're going to set up cluster quorum, using **Windows Admin Center**. + +1. If you're not already, ensure you're logged into your **Windows Admin Center** instance, and click on your **azshciclus** cluster that you created earlier + +![Connect to your cluster with Windows Admin Center](/deployment/media/wac_azshciclus_ga.png "Connect to your cluster with Windows Admin Center") + +2. You may be prompted for credentials, so log in with your **azshci\azureuser** credentials and tick the **Use these credentials for all connections** box. You should then be connected to your **azshciclus cluster** +3. After a few moments of verification, the **cluster dashboard** will open. +4. On the **cluster dashboard**, at the very bottom-left of the window, click on **Settings** +5. In the **Settings** window, click on **Witness** and under **Witness type**, use the drop-down to select **Cloud witness** + +![Set up cloud witness in Windows Admin Center](/deployment/media/wac_cloud_witness_new_ga.png "Set up cloud witness in Windows Admin Center") + +6. Open a new tab in your browser, and navigate to **https://portal.azure.com** and login with your Azure credentials +7. You should already have a subscription from an earlier step, but if not, you should [review those steps and create one, then come back here](/deployment/steps/1_DeployAzureVM.md#get-an-azure-subscription) +8. Once logged into the Azure portal, click on **Create a Resource**, click **Storage**, then **Storage account** +9. For the **Create storage account** blade, ensure the **correct subscription** is selected, then enter the following: + + * Resource Group: **Create new**, then enter **azshcicloudwitness**, and click **OK** + * Storage account name: **azshcicloudwitness** + * Region: **Select your preferred region** + * Performance: **Only standard is supported** + * Redundancy: **Locally-redundant storage (LRS)** - Failover Clustering uses the blob file as the arbitration point, which requires some consistency guarantees when reading the data. Therefore you must select Locally-redundant storage for Replication type. + +![Set up storage account in Azure](/deployment/media/azure_cloud_witness_ga.png "Set up storage account in Azure") + +1. On the **Advanced** page, ensure that **Enable blob public access** is **unchecked**, and **Minimum TLS version** is set to **Version 1.2** +2. On the **Networking**, **Data protection** and **Tags** pages, accept the defaults and press **Next** +3. When complete, click **Create** and your deployment will begin. This should take a few moments. +4. Once complete, in the **notification**, click on **Go to resource** +5. On the left-hand navigation, under Settings, click **Access Keys**. When you create a Microsoft Azure Storage Account, it is associated with two Access Keys that are automatically generated - Primary Access key and Secondary Access key. For a first-time creation of Cloud Witness, use the **Primary Access Key**. There is no restriction regarding which key to use for Cloud Witness. +6. Click on **Show keys** and take a copy of the **Storage account name** and **key1** + +![Configure Primary Access key in Azure](/deployment/media/azure_keys_ga.png "Configure Primary Access key in Azure") + +16. On the left-hand navigation, under Settings, click **Properties** and make a note of your **blob service endpoint**. + +![Blob Service endpoint in Azure](/deployment/media/azure_blob_ga.png "Blob Service endpoint in Azure") + +**NOTE** - The required service endpoint is the section of the Blob service URL **after blob.**, i.e. for our configuration, **core.windows.net** + +17. With all the information gathered, return to the **Windows Admin Center** and complete the form with your values, then click **Save** + +![Providing storage account info in Windows Admin Center](/deployment/media/wac_azure_key_ga.png "Providing storage account info in Windows Admin Center") + +18. Within a few moments, your witness settings should be successfully applied and you have now completed configuring the quorum settings for the **azshciclus** cluster. + +### Congratulations! ### +You've now successfully deployed and configured your Azure Stack HCI 20H2 cluster! + +Next Steps +----------- +In this step, you've successfully created a nested Azure Stack HCI 20H2 cluster using Windows Admin Center. With this complete, you can now [Integrate Azure Stack HCI 20H2 with Azure](/deployment/steps/3_AzSHCIIntegration.md "Integrate Azure Stack HCI 20H2 with Azure") + +Product improvements +----------- +If, while you work through this guide, you have an idea to make the product better, whether it's something in Azure Stack HCI, Windows Admin Center, or the Azure Arc integration and experience, let us know! We want to hear from you! + +For **Azure Stack HCI**, [Head on over to the Azure Stack HCI 20H2 Q&A forum](https://docs.microsoft.com/en-us/answers/topics/azure-stack-hci.html "Azure Stack HCI 20H2 Q&A"), where you can share your thoughts and ideas about making the technologies better and raise an issue if you're having trouble with the technology. + +Raising issues +----------- +If you notice something is wrong with this guide, such as a step isn't working, or something just doesn't make sense - help us to make this guide better! Raise an issue in GitHub, and we'll be sure to fix this as quickly as possible! + +If you're having an issue with Azure Stack HCI 20H2 **outside** of this guide, [head on over to the Azure Stack HCI 20H2 Q&A forum](https://docs.microsoft.com/en-us/answers/topics/azure-stack-hci.html "Azure Stack HCI 20H2 Q&A"), where Microsoft experts and valuable members of the community will do their best to help you. \ No newline at end of file diff --git a/deployment/steps/3_AzSHCIIntegration.md b/deployment/steps/3_AzSHCIIntegration.md new file mode 100644 index 0000000..d20f683 --- /dev/null +++ b/deployment/steps/3_AzSHCIIntegration.md @@ -0,0 +1,348 @@ +Integrate Azure Stack HCI 20H2 with Azure +============== +Overview +----------- + +With your Azure Stack HCI 20H2 cluster deployed successfully, you need to register this cluster to unlock full functionality. + +Contents +----------- +- [Overview](#overview) +- [Contents](#contents) +- [Prerequisites for cluster registration](#prerequisites-for-cluster-registration) +- [Complete Registration](#complete-registration) +- [Next Steps](#next-steps) +- [Product improvements](#product-improvements) +- [Raising issues](#raising-issues) + +Azure Stack HCI 20H2 is delivered as an Azure service and needs to register within 30 days of installation per the Azure Online Services Terms. With our cluster configured, we'll now register your Azure Stack HCI 20H2 cluster with **Azure Arc** for monitoring, support, billing, and hybrid services. Upon registration, an Azure Resource Manager resource is created to represent each on-premises Azure Stack HCI 20H2 cluster, effectively extending the Azure management plane to Azure Stack HCI 20H2. Information is periodically synced between the Azure resource and the on-premises cluster. One great aspect of Azure Stack HCI 20H2, is that the Azure Arc registration is a native capability of Azure Stack HCI 20H2, so there is no agent required. + +**NOTE** - After registering your Azure Stack HCI 20H2 cluster, the **first 60 days usage will be free**. + +Prerequisites for cluster registration +----------- + +Firstly, **you need an Azure Stack HCI 20H2 cluster**, which we've just created, so you're good there. + +Your nodes need to have **internet connectivity** in order to register and communicate with Azure. If you've been running nested in Azure, you should have this already set up correctly, but if you're running nested on a local physical machine, make any necessary adjustments to your InternalNAT switch to allow internet connections through to your nested Azure Stack HCI 20H2 nodes. + +You'll need an **Azure subscription**, along with appropriate **Azure Active Directory permissions** to complete the registration process. If you don't already have them, you'll need to ask your Azure AD administrator to grant permissions or delegate them to you. You can learn more about this below. + +For the simplest registration experience, have an **Azure AD admin** (Owner or User Access Administrator with Contributor role) complete the registration process using either Windows Admin Center or PowerShell. + +### Understanding Azure subscription permissions + +If you don’t already have an Azure account, [create one](https://azure.microsoft.com/). + +You can use an existing subscription of any type: +- Free account with Azure credits [for students](https://azure.microsoft.com/free/students/) or [Visual Studio subscribers](https://azure.microsoft.com/pricing/member-offers/credit-for-visual-studio-subscribers/) +- [Pay-as-you-go](https://azure.microsoft.com/pricing/purchase-options/pay-as-you-go/) subscription with credit card +- Subscription obtained through an Enterprise Agreement (EA) +- Subscription obtained through the Cloud Solution Provider (CSP) program + +The user registering the cluster must have Azure subscription permissions to: + +- Register a resource provider +- Create/Get/Delete Azure resources and resource groups + +If your Azure subscription is through an EA or CSP, the easiest way is to ask your Azure subscription admin to assign a built-in "Owner" role to your subscription, or a "User Access Administrator" role along with a "Contributor" role. + +#### Optional - Create a Custom Azure Role #### + +**Your admins may prefer a more restrictive option than using Owner, or Contributor**. In this case, it's possible to create a custom Azure role specific for Azure Stack HCI registration by following these steps: + +1. Create a json file called **CustomHCIRole.json** with following content. Make sure to change <subscriptionID> to your Azure subscription ID. To get your subscription ID, visit [portal.azure.com](https://portal.azure.com), navigate to Subscriptions, and copy/paste your ID from the list. + + ```json + { + "Name": "Azure Stack HCI registration role", + "Id": null, + "IsCustom": true, + "Description": "Custom Azure role to allow subscription-level access to register Azure Stack HCI", + "Actions": [ + "Microsoft.Resources/subscriptions/resourceGroups/write", + "Microsoft.Resources/subscriptions/resourceGroups/read", + "Microsoft.Resources/subscriptions/resourceGroups/delete", + "Microsoft.AzureStackHCI/register/action", + "Microsoft.AzureStackHCI/Unregister/Action", + "Microsoft.AzureStackHCI/clusters/*", + "Microsoft.Authorization/roleAssignments/write", + "Microsoft.HybridCompute/register/action", + "Microsoft.GuestConfiguration/register/action" + ], + "NotActions": [ + ], + "AssignableScopes": [ + "/subscriptions/<subscriptionId>" + ] + } + ``` + +2. Create the custom role: + + ```powershell + New-AzRoleDefinition -InputFile <path to CustomHCIRole.json> + ``` + +3. Assign the custom role to the user: + + ```powershell + $user = Get-AzAdUser -DisplayName <userdisplayname> + $role = Get-AzRoleDefinition -Name "Azure Stack HCI registration role" + New-AzRoleAssignment -ObjectId $user.Id -RoleDefinitionId $role.Id -Scope /subscriptions/<subscriptionid> + ``` + +### Understanding required Azure Active Directory permissions ### +In addition to creating an Azure resource in your subscription, registering Azure Stack HCI creates an app identity in your Azure AD tenant. This identity is conceptually similar to a user. The app identity inherits the cluster name. This identity acts on behalf on the Azure Stack HCI cloud service, as appropriate, within your subscription. + +If the user who registers the cluster is an Azure AD administrator or has sufficient permissions, this all happens automatically. No additional action is required. Otherwise, you might need approval from your Azure AD administrator to complete registration. Your administrator can either explicitly grant consent to the app, or they can delegate permissions so that you can grant consent to the app: + +![Azure Active Directory Permissions](/deployment/media/aad_permissions.png "Azure Active Directory Permissions") + +The user who runs Register-AzStackHCI needs Azure AD permissions to: + +The user who runs `Register-AzStackHCI` needs Azure AD permissions to: + +- Create (`New-Remove-AzureADApplication`), get (`Get-Remove-AzureADApplication`), set (`Set-Remove-AzureADApplication`), or remove (`Remove-AzureADApplication`) Azure AD applications. +- Create (`New-Get-AzureADServicePrincipal`) or get (`Get-AzureADServicePrincipal`) the Azure AD service principal. +- Manage Active Directory application secrets (`New-Remove-AzureADApplicationKeyCredential`, `Get-Remove-AzureADApplicationKeyCredential`, or `Remove-AzureADApplicationKeyCredential`). +- Grant consent to use specific application permissions (`New-AzureADApplicationKeyCredential`, `Get-AzureADApplicationKeyCredential`, or `Remove-AzureADServiceAppRoleAssignments`). + +There are three ways in which this can be accomplished. + +#### Option 1: Allow any user to register applications #### + +In Azure Active Directory, navigate to User settings > **App registrations**. Under **Users can register applications**, select **Yes**. + +This will allow any user to register applications. However, the user will still require the Azure AD admin to grant consent during cluster registration. Note that this is a tenant level setting, so it may not be suitable for large enterprise customers. + +#### Option 2: Assign Cloud Application Administration role #### + +Assign the built-in "Cloud Application Administration" Azure AD role to the user. This will allow the user to register clusters without the need for additional AD admin consent. + +#### Option 3: Create a custom AD role and consent policy #### + +The most restrictive option is to create a custom AD role with a custom consent policy that delegates tenant-wide admin consent for required permissions to the Azure Stack HCI Service. When assigned this custom role, users are able to both register and grant consent without the need for additional AD admin consent. + +**NOTE** - This option requires an Azure AD Premium license and uses custom AD roles and custom consent policy features which are currently in public preview. + +If you choose to perform Option 3, you'll need to follow these steps on **AzSHCIHost001**, which we'll demonstrate mainly through PowerShell. + +1. Firstly, configure the appropriate AzureAD modules, then **Connect to Azure AD**, and when prompted, **log in with your appropriate credentials**. + +```powershell +Remove-Module AzureAD -ErrorAction SilentlyContinue -Force +Install-Module AzureAD -AllowClobber -Force +Connect-AzureAD +``` + +2. Create a **custom consent policy**: + +```powershell +New-AzureADMSPermissionGrantPolicy -Id "AzSHCI-registration-consent-policy" ` + -DisplayName "Azure Stack HCI registration admin app consent policy" ` + -Description "Azure Stack HCI registration admin app consent policy" +``` + +3. Add a condition that includes required app permissions for Azure Stack HCI service, which carries the app ID 1322e676-dee7-41ee-a874-ac923822781c. Note that the following permissions are for the GA release of Azure Stack HCI, and will not work with Public Preview unless you have applied the [November 23, 2020 Preview Update (KB4586852)](https://docs.microsoft.com/en-us/azure-stack/hci/release-notes "November 23, 2020 Preview Update (KB4586852)") to every server in your cluster and have downloaded the Az.StackHCI module version 0.4.1 or later. + +```powershell +New-AzureADMSPermissionGrantConditionSet -PolicyId "AzSHCI-registration-consent-policy" ` + -ConditionSetType "includes" -PermissionType "application" -ResourceApplication "1322e676-dee7-41ee-a874-ac923822781c" ` + -Permissions "bbe8afc9-f3ba-4955-bb5f-1cfb6960b242", "8fa5445e-80fb-4c71-a3b1-9a16a81a1966", ` + "493bd689-9082-40db-a506-11f40b68128f", "2344a320-6a09-4530-bed7-c90485b5e5e2" +``` + +4. Grant permissions to allow registering Azure Stack HCI, noting the custom consent policy created in Step 2: + +```powershell +$displayName = "Azure Stack HCI Registration Administrator " +$description = "Custom AD role to allow registering Azure Stack HCI " +$templateId = (New-Guid).Guid +$allowedResourceAction = +@( + "microsoft.directory/applications/createAsOwner", + "microsoft.directory/applications/delete", + "microsoft.directory/applications/standard/read", + "microsoft.directory/applications/credentials/update", + "microsoft.directory/applications/permissions/update", + "microsoft.directory/servicePrincipals/appRoleAssignedTo/update", + "microsoft.directory/servicePrincipals/appRoleAssignedTo/read", + "microsoft.directory/servicePrincipals/appRoleAssignments/read", + "microsoft.directory/servicePrincipals/createAsOwner", + "microsoft.directory/servicePrincipals/credentials/update", + "microsoft.directory/servicePrincipals/permissions/update", + "microsoft.directory/servicePrincipals/standard/read", + "microsoft.directory/servicePrincipals/managePermissionGrantsForAll.AzSHCI-registration-consent-policy" +) +$rolePermissions = @{'allowedResourceActions' = $allowedResourceAction } +``` + +5. Create the new custom AD role: + +```powershell +$customADRole = New-AzureADMSRoleDefinition -RolePermissions $rolePermissions ` + -DisplayName $displayName -Description $description -TemplateId $templateId -IsEnabled $true +``` + +6. Assign the new custom AD role to the user who will register the Azure Stack HCI cluster with Azure by following [these instructions](https://docs.microsoft.com/en-us/azure/active-directory/fundamentals/active-directory-users-assign-role-azure-portal "Guidance on creating a custom Azure AD role"). + +Complete Registration +----------- + +To complete registration, you have 2 options - you can use **Windows Admin Center**, or you can use **PowerShell**. For this lab, it's recommended to use the PowerShell approach, due to a few unpredictible erros in the lab environment, likely due to WAC installed on the domain controller. + +### Option 1 - Register using PowerShell ### +We're going to perform the registration from the **AzSHCIHost001** machine, which we've been using with the Windows Admin Center. + +1. On **AzSHCIHost001**, open **PowerShell ISE as administrator** +2. In the file menu, click **Open** and navigate to **V:\Source** and open **Register-AzSHCI** +3. When the script file opens, select and run the following code to install the PowerShell Module for Azure Stack HCI 20H2 on that machine. + +```powershell +Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Force +Install-Module Az.StackHCI +``` + +**NOTE** - You may recieve a message that **PowerShellGet requires NuGet Provider...** - read the full message, and then click **Yes** to allow the appropriate dependencies to be installed. You may receive a second prompt to **install the modules from the PSGallery** - click **Yes to All** to proceed. + +In addition, in future releases, installing the Azure PowerShell **Az** modules will include **StackHCI**, however today, you have to install this module specifically, using the command **Install-Module Az.StackHCI** + +4. With the Az.StackHCI modules installed, it's now time to register your Azure Stack HCI 20H2 cluster to Azure, however first, it's worth exploring how to check existing registration status. The following code assumes you are still in the remote PowerShell session open from the previous commands. + +```powershell +Invoke-Command -ComputerName AZSHCINODE01 -ScriptBlock { + Get-AzureStackHCI +} +``` + +![Check the registration status of the Azure Stack HCI 20H2 cluster](/deployment/media/reg_check.png "Check the registration status of the Azure Stack HCI 20H2 cluster") + +As you can see from the result, the cluster is yet to be registered, and the cluster status identifies as **Clustered**. Azure Stack HCI 20H2 needs to register within 30 days of installation per the Azure Online Services Terms. If not clustered after 30 days, the **ClusterStatus** will show **OutOfPolicy**, and if not registered after 30 days, the **RegistrationStatus** will show **OutOfPolicy**. + +5. To register the cluster, you'll first need to get your **Azure subscription ID**. An easy way to do this is to quickly **log into https://portal.azure.com**, and in the **search box** at the top of the screen, search for **subscriptions** and then click on **Subscriptions** + +![Azure Subscriptions](/deployment/media/azure_subscriptions_ga.png "Azure Subscriptions") + +6. Your **subscription** should be shown in the main window. If you have more than one subscription listed here, click the correct one, and in the new blade, copy the **Subscription ID**. + +**NOTE** - If you don't see your desired subscription, in the top right-corner of the Azure portal, click on your user account, and click **Switch directory**, then select an alternative directory. Once in the chosen directory, repeat the search for your **Subscription ID** and copy it down. + +7. With your **Subscription ID** in hand, you can **register using the following Powershell commands**, from your open PowerShell window. + +```powershell +$azshciNodeCreds = Get-Credential -UserName "azshci\azureuser" -Message "Enter the azshci\azureuser password" +Register-AzStackHCI ` + -SubscriptionId "your-subscription-ID-here" ` + -ResourceName "azshciclus" ` + -ResourceGroupName "AZSHCICLUS_RG" ` + -Region "EastUS" ` + -EnvironmentName "AzureCloud" ` + -ComputerName "AZSHCINODE01.azshci.local" ` + –Credential $azshciNodeCreds ` +``` + +Of these commands, many are optional: + +* **-ResourceName** - If not declared, the Azure Stack HCI 20H2 cluster name is used +* **-ResourceGroupName** - If not declared, the Azure Stack HCI 20H2 cluster plus the suffix "-rg" is used +* **-Region** - If not declared, "EastUS" will be used. Additional regions are supported, with the longer term goal to integrate with Azure Arc in all Azure regions. +* **-EnvironmentName** - If not declared, "AzureCloud" will be used, but allowed values will include additional environments in the future +* **-ComputerName** - This is used when running the commands remotely against a cluster. Just make sure you're using a domain account that has admin privilege on the nodes and cluster +* **-Credential** - This is also used for running the commands remotely against a cluster. + +**Register-AzureStackHCI** runs syncronously, with progress reporting, and typically takes 1-2 minutes. The first time you run it, it may take slightly longer, because it needs to install some dependencies, including additional Azure PowerShell modules. + +8. Once dependencies have been installed, you'll receive a popup on **AzSHCIHost001** to authenticate to Azure. Provide your **Azure credentials**. + +![Login to Azure](/deployment/media/azure_login_reg.png "Login to Azure") + +9. Once successfully authenticated, the registration process will begin, and will take a few moments. Once complete, you should see a message indicating success, as per below: + +![Register Azure Stack HCI 20H2 with PowerShell](/deployment/media/register_azshci_ga.png "Register Azure Stack HCI 20H2 with PowerShell") + +**NOTE** - if upon registering, you receive an error similar to that below, please **try a different region**. You can still proceed to [Step 5](#next-steps) and continue with your evaluation, and it won't affect any functionality. Just make sure you come back and register later! + +``` +Register-AzStackHCI : Azure Stack HCI 20H2 is not yet available in region <regionName> +``` + +**NOTE** - if upon registering, you receive an error stating "Azure Arc integration isn't available for the version of Azure Stack HCI installed on node(s)", this can be safely ignored + +10. Once the cluster is registered, run the following command on **AzSHCIHost001** to check the updated status: + +```powershell +Invoke-Command -ComputerName AZSHCINODE01 -ScriptBlock { + Get-AzureStackHCI +} +``` +![Check updated registration status with PowerShell](/deployment/media/registration_status.png "Check updated registration status with PowerShell") + +You can see the **ConnectionStatus** and **LastConnected** time, which is usually within the last day unless the cluster is temporarily disconnected from the Internet. An Azure Stack HCI 20H2 cluster can operate fully offline for up to 30 consecutive days. + +### Option 2 - Register using Windows Admin Center ### + +1. On **AzSHCIHost001**, logged in as **azshci\azureuser**, open the Windows Admin Center, and on the **All connections** page, select your azshciclus +2. When the cluster dashboard has loaded, in the top-right corner, you'll see the **status of the Azure registration/connection** + +![Azure registration status in Windows Admin Center](/deployment/media/wac_azure_reg_dashboard_2.png "Azure registration status in Windows Admin Center") + +3. You can begin the registration process by clicking **Register this cluster** +4. If you haven't already, you'll be prompted to register Windows Admin Center with an Azure tenant. Follow the instructions to **Copy the code** and then click on the link to configure device login. +5. When prompted for credentials, **enter your Azure credentials** for a tenant you'd like to register the Windows Admin Center +6. Back in Windows Admin Center, you'll notice your tenant information has been added. You can now click **Connect** to connect Windows Admin Center to Azure + +![Connecting Windows Admin Center to Azure](/deployment/media/wac_azure_connect.png "Connecting Windows Admin Center to Azure") + +7. Click on **Sign in** and when prompted for credentials, **enter your Azure credentials** and you should see a popup that asks for you to accept the permissions, so click **Accept** + +![Permissions for Windows Admin Center](/deployment/media/wac_azure_permissions.png "Permissions for Windows Admin Center") + +8. Back in Windows Admin Center, you may need to refresh the page if your 'Register this cluster' link is not active. Once active, click **Register this cluster** and you should be presented with a window requesting more information. +9. Choose your **Azure subscription** that you'd like to use to register, along with an **Azure resource group** and **region**, then click **Register**. This will take a few moments. + +![Final step for registering Azure Stack HCI with Windows Admin Center](/deployment/media/wac_azure_register.png "Final step for registering Azure Stack HCI with Windows Admin Center") + +10. Once completed, you should see updated status on the Windows Admin Center dashboard, showing that the cluster has been correctly registered. + +![Azure registration status in Windows Admin Center](/deployment/media/wac_azure_reg_dashboard_3.png "Azure registration status in Windows Admin Center") + +**NOTE** - If you receive an error message like the one below, this can be ignored, your cluster will still be registered successfully. + +![Azure Arc Registration issue in Windows Admin Center](/deployment/media/wac_azure_arc_register_error.png "Azure Arc Registration issue in Windows Admin Center") + +You can now proceed on to [Viewing registration details in the Azure portal](#View-registration-details-in-the-Azure-portal) + +### View registration details in the Azure portal ### +With registration complete, either through Windows Admin Center, or through PowerShell, you should take some time to explore the artifacts that are created in Azure, once registration successfully completes. + +1. On **AzSHCIHost001**, open the Edge browser and **log into https://portal.azure.com** to check the resources created there. In the **search box** at the top of the screen, search for **Resource groups** and then click on **Resource groups** +2. You should see a new **Resource group** listed, with the name you specified earlier, which in our case, is **AZSHCICLUS_RG** + +![Registration resource group in Azure](/deployment/media/registration_rg_ga.png "Registration resource group in Azure") + +12. Click on the **AZSHCICLUS_RG** resource group, and in the central pane, you'll see that a record with the name **azshciclus** has been created inside the resource group +13. Click on the **azihciclus** record, and you'll be taken to the new Azure Stack HCI Resource Provider, which shows information about all of your clusters, including details on the currently selected cluster + +![Overview of the recently registered cluster in the Azure portal](/deployment/media/azure_portal_hcicluster.png "Overview of the recently registered cluster in the Azure portal") + +**NOTE** - If when you ran **Register-AzureStackHCI**, you don't have appropriate permissions in Azure Active Directory, to grant admin consent, you will need to work with your Azure Active Directory administrator to complete registration later. You can exit and leave the registration in status "**pending admin consent**," i.e. partially completed. Once consent has been granted, **simply re-run Register-AzureStackHCI** to complete registration. + +### Congratulations! ### +You've now successfully registered your Azure Stack HCI 20H2 cluster! + +Next Steps +----------- +In this step, you've successfully registered your Azure Stack HCI 20H2 cluster. With this complete, you can now move on to [Explore the management of your Azure Stack HCI 20H2 environment](/deployment/steps/4_ExploreAzSHCI.md "Explore the management of your Azure Stack HCI 20H2 environment") + +Product improvements +----------- +If, while you work through this guide, you have an idea to make the product better, whether it's something in Azure Stack HCI, Windows Admin Center, or the Azure Arc integration and experience, let us know! We want to hear from you! + +For **Azure Stack HCI**, [Head on over to the Azure Stack HCI 20H2 Q&A forum](https://docs.microsoft.com/en-us/answers/topics/azure-stack-hci.html "Azure Stack HCI 20H2 Q&A"), where you can share your thoughts and ideas about making the technologies better and raise an issue if you're having trouble with the technology. + +Raising issues +----------- +If you notice something is wrong with this guide, such as a step isn't working, or something just doesn't make sense - help us to make this guide better! Raise an issue in GitHub, and we'll be sure to fix this as quickly as possible! + +If you're having an issue with Azure Stack HCI 20H2 **outside** of this guide, [head on over to the Azure Stack HCI 20H2 Q&A forum](https://docs.microsoft.com/en-us/answers/topics/azure-stack-hci.html "Azure Stack HCI 20H2 Q&A"), where Microsoft experts and valuable members of the community will do their best to help you. \ No newline at end of file diff --git a/deployment/steps/4_ExploreAzSHCI.md b/deployment/steps/4_ExploreAzSHCI.md new file mode 100644 index 0000000..147fbc8 --- /dev/null +++ b/deployment/steps/4_ExploreAzSHCI.md @@ -0,0 +1,178 @@ + +Explore the management of your Azure Stack HCI 20H2 environment +============== +Overview +----------- +With all key components deployed, including a management infrastructure, along with clustered Azure Stack HCI 20H2 nodes, you can now begin to explore some of the additional capabilities within Azure Stack HCI 20H2 and the Windows Admin Center. We'll cover a few recommended activities below, to expose you to some of the key elements of the Windows Admin Center, but for the rest, we'll [direct you over to the official documentation](https://docs.microsoft.com/en-us/azure-stack/hci/ "Azure Stack HCI 20H2 documentation"). + +Contents +----------- +- [Overview](#overview) +- [Contents](#contents) +- [Create volumes for VMs](#create-volumes-for-vms) +- [Deploy a virtual machine](#deploy-a-virtual-machine) +- [Shutting down the environment](#shutting-down-the-environment) +- [Congratulations!](#congratulations) +- [Next steps](#next-steps) +- [Product improvements](#product-improvements) +- [Raising issues](#raising-issues) + +Create volumes for VMs +----------- +In this step, you'll create some volumes on an Azure Stack HCI 20H2 cluster by using Windows Admin Center, and enable data deduplication and compression on volumes. + +### Create a two-way mirror volume ### +If you're not already there, open the **Windows Admin Center**. You'll spend your time here for the remainder of the steps documented below. + +1. Once logged into **Windows Admin Center**, click on your previously deployed cluster, **azshciclus.azshci.local** +2. On the left hand navigation, under **Storage** select **Volumes**. The central **Volumes** page shows you should have a single volume currently +3. On the Volumes page, select the **Inventory** tab, and then select **Create** +4. In the **Create volume** pane, enter **Volume01** for the volume name, and leave **Resiliency** as **Two-way mirror** +5. In Size on HDD, specify **20GB** for the size of the volume, then click **Create**. + +![Create a volume on Azure Stack HCI 20H2](/deployment/media/wac_vm_storage_ga.png "Create a volume on Azure Stack HCI 20H2") + +6. Creating the volume can take a few minutes. Notifications in the upper-right will let you know when the volume is created. The new volume appears in the Inventory list + +![Volume created on Azure Stack HCI 20H2](/deployment/media/wac_vm_storage_deployed_ga.png "Volume created on Azure Stack HCI 20H2") + +### Optional - Create a mirror-accelerated parity volume ### + +**NOTE** - This can only be perfomed on clusters with **4 or more nodes**. If you just deployed a 2 node cluster, skip this optional step. + +Mirror-accelerated parity reduces the footprint of the volume on the HDD. For example, a three-way mirror volume would mean that for every 10 terabytes of size, you will need 30 terabytes as footprint. To reduce the overhead in footprint, create a volume with mirror-accelerated parity. This reduces the footprint from 30 terabytes to just 22 terabytes, even with only 4 servers, by mirroring the most active 20 percent of data, and using parity, which is more space efficient, to store the rest. You can adjust this ratio of parity and mirror to make the performance versus capacity tradeoff that's right for your workload. For example, 90 percent parity and 10 percent mirror yields less performance but streamlines the footprint even further. + +1. Still in **Windows Admin Center**, on the Volumes page, select the **Inventory** tab, and then select **Create** +2. In the **Create volume** pane, enter **Volume02_PAR** for the volume name, and set **Resiliency** as **Mirror-accelerated parity** +3. In **Parity percentage**, set the percentage of parity to **80% parity, 20% mirror** +4. In Size on HDD, specify **20GB** for the size of the volume, then click **Create**. + +For more information on planning volumes with Azure Stack HCI 20H2, you should [refer to the official docs](https://docs.microsoft.com/en-us/azure-stack/hci/concepts/plan-volumes "Planning volumes for Azure Stack HCI 20H2"). + +### Turn on deduplication and compression ### +You may have seen, during the **Create volume** wizard, you could have enabled deduplication and compression at creation time, however we wanted to make sure you were fully aware of how to enable it for existing volumes. + +1. Still in **Windows Admin Center**, on the Volumes page, select the **Inventory** tab, and then select your **Volume01** volume +2. On the Volume Volume01 pane, you'll see a simple rocker switch to enable **Deduplication and compression**. Click to enable it, and click **Start** + +![Enable deduplication on volume](/deployment/media/wac_enable_dedup_ga.png "Enable deduplication on volume") + +3. In the **Enable deduplication** pane, use the drop-down to select **Hyper-V** then click **Enable Deduplication**. This should be enabled quickly, as there's no files on the volume. + +**NOTE** - You'll notice there there are 3 options; default, Hyper-V and Backup. If you're interested in learning more about Deduplication in Azure Stack HCI 20H2, you should [refer to our documentation](https://docs.microsoft.com/en-us/windows-server/storage/data-deduplication/overview "Deduplication overview") + +You now have a couple of volumes created and ready to accept workloads. Whilst we deployed the volumes using the Windows Admin Center, you can also do the same through PowerShell. If you're interested in taking that approach, [check out the official docs that walk you through that process](https://docs.microsoft.com/en-us/azure-stack/hci/manage/create-volumes "Official documentation for creating volumes") + +Deploy a virtual machine +----------- +In this step, you'll deploy a VM onto your new volume, using Windows Admin Center. + +### Create the virtual machine ### +You should still be in **Windows Admin Center** for the next steps. + +1. Once logged into the **Windows Admin Center**, click on your previously deployed cluster, **azshciclus.azshci.local** +2. On the left hand navigation, under **Compute** select **Virtual machines**. The central **Virtual machines** page shows you no virtual machines deployed currently +3. On the **Virtual machines** page, select the **Inventory** tab, and then select **New** +4. In the **New virtual machine** pane, enter **VM001** for the name, and enter the following pieces of information, then click **Create** + + * Generation: **Generation 2 (Recommended)** + * Host: **Leave as recommended** + * Path: **C:\ClusterStorage\Volume01** + * Virtual processors: **1** + * Startup memory (GB): **0.5** + * Network: **ComputeSwitch** + * Storage: **Add, then Create an empty virtual hard disk** and set size to **5GB** + * Operating System: **Install an operating system later** + +5. The creation process will take a few moments, and once complete, **VM001** should show within the **Virtual machines view** +6. Click on the **VM** and then click **Start** - within moments, the VM should be running + +![VM001 up and running](/deployment/media/wac_vm001_ga.png "VM001 up and running") + +7. Click on **VM001** to view the properties and status for this running VM +8. Click on **Connect** - you may get a **VM Connect** prompt: + +![Connect to VM001](/deployment/media/vm_connect_ga.png "Connect to VM001") + +9. Click on **Go to Settings** and in the **Remote Desktop** pane, click on **Allow remote connections to this computer**, then **Save** +10. Click the **Back** button in your browser to return to the VM001 view, then click **Connect**, and when prompted with the certificate prompt, click **Connect** and enter appropriate credentials +11. There's no operating system installed here, so it should show a UEFI boot summary, but the VM is running successfully +12. Click **Disconnect** + +You've successfully create a VM using the Windows Admin Center! + +### Live migrate the virtual machine ### +The final step we'll cover is using Windows Admin Center to live migrate VM001 from it's current node, to an alternate node in the cluster. + +1. Still within the **Windows Admin Center** , under **Compute**, click on **Virtual machines** +2. On the **Virtual machines** page, select the **Inventory** tab +3. Under **Host server**, make a note of the node that VM001 is currently running on. You may need to expand the column width to see the name +4. Next to **VM001**, click the tick box next to VM001, then click **More**. You'll notice you can Clone, Domain Join and also Move the VM. Click **Move** + +![Start Live Migration using Windows Admin Center](/deployment/media/wac_move_ga.png "Start Live Migration using Windows Admin Center") + +5. In the **Move Virtual Machine** pane, ensure **Failover Cluster** is selected, and leave the default **Best available cluster node** to allow Windows Admin Center to pick where to migrate the VM to, then click **Move** +6. The live migration will then begin, and within a few seconds, the VM should be running on a different node. +7. On the left hand navigation, under **Compute** select **Virtual machines** to return to the VM dashboard view, which aggregates information across your cluster, for all of your VMs. + +Shutting down the environment +----------- +When running the environment in Azure, to save costs, you may wish to shut down your nested VMs, and Hyper-V host. In order to do so, it's advisable to run the following commands, from the Hyper-V host, to cleanly power down the different components, before powering down the Azure VM itself. + +1. On your Hyper-V host, open **PowerShell as administrator** +2. First, using PowerShell Direct, you'll log into one of the Azure Stack HCI 20H2 nodes to shutdown the cluster, then you'll power down the VMs running on your Hyper-V host + +```powershell +$domainName = "azshci.local" +$domainAdmin = "$domainName\azureuser" +$domainCreds = Get-Credential -UserName "$domainAdmin" -Message "Enter the password for the Admin account" +# Define node name +$nodeName = "AZSHCINODE01" +Invoke-Command -VMName $nodeName -Credential $domainCreds -ScriptBlock { + # Get any running VMs and turn them off + Get-ClusterResource | Where-Object {$_.ResourceType -eq "Virtual Machine"} | Stop-ClusterResource + # Stop the cluster + Stop-Cluster -Force +} +# Power down VMs on your Hyper-V host +Get-VM | Stop-VM -Force +``` + +3. Once all the VMs are switched off, you can then shut down your Hyper-V host. If you're running this environment on physical gear on-prem, you're all done, but if you deployed in Azure, visit https://portal.azure.com/, and login with your Azure credentials. Once logged in, using the search box on the dashboard, enter "azshci" and once the results are returned, click on your AzSHCIHost virtual machine. + +![Virtual machine located in Azure](/deployment/media/azure_vm_search_ga.png "Virtual machine located in Azure") + +4. Once on the overview blade for your VM, along the **top navigation**, click **Stop**, and then click **OK**. Your VM will then be deallocated and **compute charges** will cease. + +Congratulations! +----------- +You've reached the end of the evaluation guide. In this guide you have: + +* Deployed/Configured a Hyper-V host in Azure fully configured with all necessary roles, features and Windows Admin Center. +* Created an Azure Stack HCI 20H2 cluster, integrated with a cloud witness in Azure, and registered with Azure for billing +* Used the Windows Admin Center to create and modify volumes, then deploy and migrate a virtual machine. + +Great work! + +Next steps +----------- +This part of the guide covers only a handful of key topics and capabilities that Azure Stack HCI 20H2 can provide. We'll be adding more shortly, but in the meantime, we'd strongly advise you to check out some of the key areas below: + +* [Explore Windows Admin Center](https://docs.microsoft.com/en-us/azure-stack/hci/get-started "Explore Windows Admin Center") +* [Manage virtual machines](https://docs.microsoft.com/en-us/azure-stack/hci/manage/vm "Manage virtual machines") +* [Add additional servers for management](https://docs.microsoft.com/en-us/azure-stack/hci/manage/add-cluster "Add additional servers for management") +* [Manage clusters](https://docs.microsoft.com/en-us/azure-stack/hci/manage/cluster "Manage clusters") +* [Create and manage storage volumes](https://docs.microsoft.com/en-us/azure-stack/hci/manage/create-volumes "Create and manage storage volumes") +* [Integrate Windows Admin Center with Azure](https://docs.microsoft.com/en-us/azure-stack/hci/manage/register-windows-admin-center "Integrate Windows Admin Center with Azure") +* [Monitor with with Azure Monitor](https://docs.microsoft.com/en-us/azure-stack/hci/manage/azure-monitor "Monitor with with Azure Monitor") +* [Integrate with Azure Site Recovery](https://docs.microsoft.com/en-us/azure-stack/hci/manage/azure-site-recovery "Integrate with Azure Site Recovery") + +Product improvements +----------- +If, while you've worked through this guide, you have an idea to make the product better, whether it's something in Azure Stack HCI 20H2, Windows Admin Center, or the Azure Arc integration and experience, let us know! We want to hear from you! [Head on over to our Azure Stack HCI 20H2 UserVoice page](https://feedback.azure.com/forums/929833-azure-stack-hci "Azure Stack HCI 20H2 UserVoice"), where you can share your thoughts and ideas about making the technologies better. If however, you have an issue that you'd like some help with, read on... + +Raising issues +----------- +If you notice something is wrong with the evaluation guide, such as a step isn't working, or something just doesn't make sense - help us to make this guide better! Raise an issue in GitHub, and we'll be sure to fix this as quickly as possible! + +If however, you're having a problem with Azure Stack HCI 20H2 **outside** of this evaluation guide, make sure you post to [our Microsoft Q&A forum](https://docs.microsoft.com/en-us/answers/topics/azure-stack-hci.html "Microsoft Q&A Forum"), where Microsoft experts and valuable members of the community will do their best to help you. \ No newline at end of file diff --git a/nested/steps/1b_NestedInAzure.md b/nested/steps/1b_NestedInAzure.md deleted file mode 100644 index a2f8f8d..0000000 --- a/nested/steps/1b_NestedInAzure.md +++ /dev/null @@ -1,412 +0,0 @@ -Evaluate Azure Stack HCI 20H2 using Nested Virtualization in Azure -============== -Overview ------------ -With the introduction of [nested virtualization support in Azure](https://azure.microsoft.com/en-us/blog/nested-virtualization-in-azure/ "Nested virtualization announcement blog post") back in 2017, Microsoft opened the door to a number of new and interesting scenarios. Nested virtualization in Azure is particularly useful for validating configurations that would require additional hardware in your environment, such as running Hyper-V hosts and clusters. - -In this guide, you'll walk through the steps to stand up an Azure Stack HCI 20H2 configuration, and key dependencies. At a high level, this will consist of the following: - -* Deploy an Azure VM, running Windows Server 2019, to act as your main Hyper-V host -* Inside the Windows Server 2019 VM, enable the Hyper-V role and accompanying management tools -* On the Windows Server 2019 VM, deploy a Windows Server 2019 domain controller, and a Windows 10 management VM, running the Windows Admin Center -* On the Windows Server 2019 VM, deploy 2 nested Azure Stack HCI 20H2 nodes -* On the Windows 10 management VM, configure your Azure Stack HCI 20H2 cluster - -Contents ------------ -- [Overview](#overview) -- [Contents](#contents) -- [Architecture](#architecture) -- [Get an Azure subscription](#get-an-azure-subscription) -- [Azure VM Size Considerations](#azure-vm-size-considerations) -- [Deploying the Azure VM](#deploying-the-azure-vm) -- [Prepare your Azure VM](#prepare-your-azure-vm) -- [Next Steps](#next-steps) -- [Product improvements](#product-improvements) -- [Raising issues](#raising-issues) -- [Full Script - Prepare your VM](#full-script---prepare-your-vm) - -Architecture ------------ - -From an architecture perspective, the following graphic showcases the different layers and interconnections between the different components: - -![Architecture diagram for Azure Stack HCI 20H2 nested in Azure](/media/nested_virt_arch_ga.png "Architecture diagram for Azure Stack HCI 20H2 nested in Azure") - -Get an Azure subscription ------------ -To evaluate Azure Stack HCI 20H2, you'll need an Azure subscription. If you already have one provided by your company, you can skip this step, but if not, you have a couple of options. - -The first option would apply to Visual Studio subscribers, where you can use Azure at no extra charge. With your monthly Azure DevTest individual credit, Azure is your personal sandbox for dev/test. You can provision virtual machines, cloud services, and other Azure resources. Credit amounts vary by subscription level, but if you manage your Azure Stack HCI 20H2 Host VM run-time efficiently, you can test the scenario well within your subscription limits. - -The second option would be to sign up for a [free trial](https://azure.microsoft.com/en-us/free/ "Azure free trial link"), which gives you $200 credit for the first 30 days, and 12 months of popular services for free. The credit for the first 30 days will give you plenty of headroom to validate Azure Stack HCI 20H2. - -You can also use this same Azure subscription to register your Azure Stack HCI 20H2 cluster, once the deployment is completed. - -Azure VM Size Considerations ------------ - -Now, before you deploy the VM in Azure, it's important to choose a **size** that's appropriate for your needs for this evaluation, along with a preferred region. This deployment, by default, recommends using a **Standard_D16s_v4**, which is a general purpose VM size, with 16 vCPUs, 64 GiB memory, and no temporary SSD storage. The OS drive is 127 GiB in size. Realistically, with this size of host VM, you could nest the following: - -* Windows Server 2019 Domain Controller - 2 vCPU, 2 GB memory -* Windows 10 management VM - 2 vCPU, 4 GB memory -* Azure Stack HCI 20H2 nodes, each with 16 vCPUs and 24 GB memory depending on the number of nodes you choose - -These are just example numbers, and you can adjust accordingly to suit your needs, even after deployment. The point here is, think about how many Azure Stack HCI 20H2 nodes you'd like to nest inside this Azure VM, and select an Azure VM size from there. Some good examples would be: - -**D-series VMs (General purpose)** - -| Size | vCPU | Memory: GiB | Temp storage (SSD): GiB | Premium Storage | -|:--|---|---|---|---| -| Standard_D8_v3 | 8 | 32 | 200 | No | -| Standard_D8s_v3 | 8 | 32 | 64 | Yes | -| Standard_D8_v4 | 8 | 32 | 0 | No | -| **Standard_D8s_v4** | **8** | **32** | **0** | **Yes** | -| Standard_D8d_v4 | 8 | 32 | 300 | No | -| Standard_D8ds_v4 | 8 | 32 | 300 | Yes | -| Standard_D16_v3 | 16 | 64 | 400 | No | -| Standard_D16s_v3 | 16 | 64 | 128 | Yes | -| Standard_D16_v4 | 16 | 64 | 0 | No | -| **Standard_D16s_v4** | **16** | **64** | **0** | **Yes** | -| Standard_D16d_v4 | 16 | 64 | 600 | No | -| Standard_D16ds_v4 | 16 | 64 | 600 | Yes | - -For reference, the Standard_D8s_v3 VM size costs approximately US $0.38 per hour, and the Standard_D8ds_v4 VM size costs approximately US $0.45 per hour, based on East US region, under a Visual Studio subscription. - -**E-series VMs (Memory optimized)** - -| Size | vCPU | Memory: GiB | Temp storage (SSD): GiB | Premium Storage | -|:--|---|---|---|---| -| Standard_E8_v3 | 8 | 64 | 200 | No | -| Standard_E8s_v3 | 8 | 64 | 128 | Yes | -| Standard_E8_v4 | 8 | 64 | 0 | No | -| **Standard_E8s_v4** | **8** | **64** | **0** | **Yes** | -| Standard_E8d_v4 | 8 | 64 | 300 | No | -| Standard_E8ds_v4 | 8 | 64 | 300 | Yes | -| Standard_E16_v3 | 16 | 128 | 400 | No | -| Standard_E16s_v3 | 16 | 128 | 256 | Yes | -| Standard_E16_v4 | 16 | 128 | 0 | No | -| **Standard_E16s_v4** | **16** | **128** | **0** | **Yes** | -| Standard_E16d_v4 | 16 | 128 | 600 | No | -| Standard_E16ds_v4 | 16 | 128 | 600 | Yes | - -For reference, the Standard_E8s_v3 VM size costs approximately US $0.50 per hour, and the Standard_E8ds_v4 VM size costs approximately US $0.58 per hour, based on East US region, under a Visual Studio subscription. - -**NOTE 1** - Many of these VM sizes include temp storage, which offers high performance, but is not persistent through reboots, Azure host migrations and more. It's therefore advisable, that if you are going to be running the Azure VM for a period of time, but shutting down frequently, that you choose a VM size with no temp storage, and store your nested VMs on the local storage of the OS disk (128 GiB) or, ensure you don't store important files on the temp drive inside the VM. - -**NOTE 2** - It's strongly recommended that you choose a VM size that supports **premium storage** - when running nested virtual machines, increasing the number of available IOPS can have a significant impact on performance, hence choosing **premium storage** over Standard HDD or Standard SSD, is strongly advised. Refer to the table above to make the most appropriate selection. - -Ensure that whichever VM size you choose, it [supports nested virtualization](https://docs.microsoft.com/en-us/azure/virtual-machines/acu "Nested virtualization support") and is [available in your chosen region](https://azure.microsoft.com/en-us/global-infrastructure/services/?products=virtual-machines "Virtual machines available by region"). - -Deploying the Azure VM ------------ -The guidance below provides 2 main options for deploying the Azure VM. - -1. The first option, is to perform a deployment via a [custom Azure Resource Manager template](#option-1---creating-the-vm-with-an-azure-resource-manager-json-template). This option can be launched quickly, directly from the button within the documentation, and after completing a simple form, your VM will be deployed. -2. The second option, is a [deployment directly from PowerShell](#option-2---creating-the-azure-vm-with-powershell), which is fast, but still requires some additional steps if you wish to enable auto-shutdown of the VM. - -### Option 1 - Creating the VM with an Azure Resource Manager JSON Template ### -To keep things simple, and graphical to begin with, we'll show you how to deploy your VM via an Azure Resource Manager template. To simplify things further, we'll use the following buttons. - -Firstly, the **Visualize** button will launch the ARMVIZ designer view, where you will see a graphic representing the core components of the deployment, including the VM, NIC, disk and more. If you want to open this in a new tab, **hold CTRL** when you click the button. - -[![Visualize your template deployment](https://raw.githubusercontent.com/Azure/azure-quickstart-templates/master/1-CONTRIBUTION-GUIDE/images/visualizebutton.png)](http://armviz.io/#/?load=https%3A%2F%2Fraw.githubusercontent.com%2FAzure%2FAzureStackHCI-EvalGuide%2Fmain%2Fnested%2Fjson%2Fazshcilabvm.json "Visualize your template deployment") - -Secondly, the **Deploy to Azure** button, when clicked, will take you directly to the Azure portal, and upon login, provide you with a form to complete. If you want to open this in a new tab, **hold CTRL** when you click the button. - -[![Deploy to Azure](https://aka.ms/deploytoazurebutton)](https://portal.azure.com/#create/Microsoft.Template/uri/https%3A%2F%2Fraw.githubusercontent.com%2FAzure%2FAzureStackHCI-EvalGuide%2Fmain%2Fnested%2Fjson%2Fazshcilabvm.json "Deploy to Azure") - -Upon clicking the **Deploy to Azure** button, enter the details, which should look something similar to those shown below, and click **Purchase**. - -![Custom template deployment in Azure](/media/azure_vm_custom_template.png "Custom template deployment in Azure") - -**NOTE** - For customers with Software Assurance, Azure Hybrid Benefit for Windows Server allows you to use your on-premises Windows Server licenses and run Windows virtual machines on Azure at a reduced cost. By selecting **Yes** for the "Already have a Windows Server License", **you confirm I have an eligible Windows Server license with Software Assurance or Windows Server subscription to apply this Azure Hybrid Benefit** and have reviewed the [Azure hybrid benefit compliance](http://go.microsoft.com/fwlink/?LinkId=859786 "Azure hybrid benefit compliance document") - -The custom template will be validated, and if all of your entries are correct, you can click **Create**. Within a few minutes, your VM will be created. - -![Custom template deployment in Azure completed](/media/azure_vm_custom_template_complete.png "Custom template deployment in Azure completed") - -If you chose to **enable** the auto-shutdown for the VM, and supplied a time, and time zone, but want to also add a notification alert, simply click on the **Go to resource group** button and then perform the following steps: - -1. In the **Resource group** overview blade, click the **AzSHCIHost001** virtual machine -2. Once on the overview blade for your VM, **scroll down on the left-hand navigation**, and click on **Auto-shutdown** -3. Ensure the Enabled slider is still set to **On** and that your **time** and **time zone** information is correct -4. Click **Yes** to enable notifications, and enter a Webhook URL, or Email address -5. Click **Save** - -You'll now be notified when the VM has been successfully shut down as the requested time. - -### Option 2 - Creating the Azure VM with PowerShell ### -For simplicity and speed, can also use PowerShell on our local workstation to deploy the Windows Server 2019 VM to Azure. As an alternative, you can take the following commands, edit them, and run them directly in [PowerShell in Azure Cloud Shell](https://docs.microsoft.com/en-us/azure/cloud-shell/quickstart-powershell "PowerShell in Azure Cloud Shell"). For the purpose of this guide, we'll assume you're using the PowerShell console/ISE or Windows Terminal locally on your workstation. - -#### Update the Execution Policy #### -In this step, you'll update your PowerShell execution policy to RemoteSigned - -```powershell -# Get the Execution Policy on the system, and make note of it before making changes -Get-ExecutionPolicy -# Set the Execution Policy for this process only -if ((Get-ExecutionPolicy) -ne "RemoteSigned") { Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope Process -Force } -``` - -#### Download the Azure PowerShell modules #### -In order for us to create a new VM in Azure, we'll need to ensure we have the latest Azure PowerShell modules - -> [!WARNING] -> We do not support having both the AzureRM and Az modules installed for PowerShell 5.1 on Windows at the same time. If you need to keep AzureRM available on your system, install the Az module for > PowerShell 6.2.4 or later. - -```powershell -# Install latest NuGet provider -Install-PackageProvider -Name NuGet -Force - -# Check if the AzureRM PowerShell modules are installed - if so, present a warning -if ($PSVersionTable.PSEdition -eq 'Desktop' -and (Get-Module -Name AzureRM -ListAvailable)) { - Write-Warning -Message ('Az module not installed. Having both the AzureRM and ' + - 'Az modules installed at the same time is not supported.') -} else { - # If no AzureRM PowerShell modules are detected, install the Azure PowerShell modules - Install-Module -Name Az -AllowClobber -Scope CurrentUser -} -``` -By default, the PowerShell gallery isn't configured as a trusted repository for PowerShellGet so you may be prompted to allow installation from this source, and trust the repository. Answer **(Y) Yes** or **(A) Yes to All** to continue with the installation. The installation will take a few moments to complete, depending on your download speeds. - -#### Sign into Azure #### -With the modules installed, you can sign into Azure. By using the Login-AzAccount, you'll be presented with a login screen for you to authenticate with Azure. Use the credentials that have access to the subscription where you'd like to deploy this VM. - -```powershell -# Login to Azure -Login-AzAccount -``` - -When you've successfully logged in, you will be presented with the default subscription and tenant associated with those credentials. - -![Result of Login-AzAccount](/media/Login-AzAccount.png "Result of Login-AzAccount") - -If this is the subscription and tenant you wish to use for this evaluation, you can move on to the next step, however if you wish to deploy the VM to an alternative subscription, you will need to run the following commands: - -```powershell -# Optional - if you wish to switch to a different subscription -# First, get all available subscriptions as the currently logged in user -$context = Get-AzContext -ListAvailable -# Display those in a grid, select the chosen subscription, then press OK. -if (($context).count -gt 1) { - $context | Out-GridView -OutputMode Single | Set-AzContext -} -``` - -With login successful, and the target subscription confirmed, you can move on to deploy the VM. - -#### Deploy the VM with PowerShell #### -In order to keep things as streamlined and quick as possible, we're going to be deploying the VM that will host Azure Stack HCI 20H2, using PowerShell. As an alternative option, we will provide an alternative method using the Azure Portal and an Azure Resource Manager Template, in JSON format. - -In the below script, feel free to change the VM Name, along with other parameters. The public DNS name for this VM will be generated by combining your VM name, with a random guid, to ensure it is unique, and the deployment completes without conflicts. - -```powershell -# Define basic information & specify credentials for VM -$vmName = "AzSHCIHost001" -$resourceGroupName = "AzSHCILabRG" -$credential = Get-Credential -Message "Enter VM credentials" -UserName "AzureUser" - -# To display a list of all Azure locations, run (Get-AzLocation).Location -$locationName = "eastus" - -# See table above for recommended sizes for your VM -$vmSize = "Standard_D16s_v4" - -# Enter either StandardSSD_LRS or Premium_LRS and for diskSize, enter 128, 256, 512 or 1024. -$managedDiskType = "StandardSSD_LRS" -$diskSize = "128" - -# Define network parameters -$networkName = "AzSHCILabvNet" -$nicName = "AzSHCILabvNIC" -$subnetName = "AzSHCILabSubnet" -$subnetPrefix = "10.0.0.0/24" -$vNetPrefix = "10.0.0.0/24" -$randomGuid = ((New-Guid).ToString()).Substring(0, 6) -$dnsName = ("$vmName" + "$randomGuid").ToLower() - -# Create resource group & network resources -New-AzResourceGroup -Name "$resourceGroupName" -Location $locationName -Force -$publicIp = New-AzPublicIpAddress -Name "AzSHCILabPubIP" -ResourceGroupName $resourceGroupName ` - -AllocationMethod Dynamic -DomainNameLabel "$dnsName" -Location $locationName -$rdpRule = New-AzNetworkSecurityRuleConfig -Name "RDP" -Description "Allow RDP" -Access Allow ` - -Protocol Tcp -Direction Inbound -Priority 100 -SourceAddressPrefix Internet -SourcePortRange * ` - -DestinationAddressPrefix * -DestinationPortRange 3389 -$NSG = New-AzNetworkSecurityGroup -ResourceGroupName $resourceGroupName ` - -Location $locationName -Name "AzSHCILabNSG" -SecurityRules $rdpRule -$subnet = New-AzVirtualNetworkSubnetConfig -Name $subnetName -AddressPrefix $subnetPrefix ` - -NetworkSecurityGroup $NSG -$vNet = New-AzVirtualNetwork -Name $networkName -ResourceGroupName $resourceGroupName ` - -Location $locationName -AddressPrefix $vNetPrefix -Subnet $subnet -$vNic = New-AzNetworkInterface -Name $nicName -ResourceGroupName $resourceGroupName ` - -Location $locationName -SubnetId $vNet.Subnets[0].Id -PublicIpAddressId $publicIp.Id - -# Finalize the VM configuration, including size, image and storage -$vm = New-AzVMConfig -VMName $vmName -VMSize $vmSize -$vm = Set-AzVMOperatingSystem -VM $vm -Windows -ComputerName $vmName -Credential $credential -ProvisionVMAgent -$vm = Add-AzVMNetworkInterface -VM $vm -Id $vNic.Id -$vm = Set-AzVMSourceImage -VM $vm -PublisherName 'MicrosoftWindowsServer' -Offer 'WindowsServer' ` - -Skus '2019-Datacenter' -Version latest -$vm = Set-AzVMOSDisk -VM $vm -StorageAccountType $managedDiskType -Windows -DiskSizeInGB $diskSize ` - -CreateOption FromImage -$vm = Set-AzVMBootDiagnostic -VM $vm -Disable - -# Create the Virtual Machine -New-AzVM -ResourceGroupName $resourceGroupName -Location $locationName -VM $vm -Verbose - -# Optional Parameter -> -LicenseType "Windows_Server" -# Copy and paste the parameter on the end of the New-AzVM command. -# Only use this if you have existing Windows Server licenses with Software Assurance (See NOTE 3 below) - -# Get connection details of the newly created VM -Get-AzVM -ResourceGroupName $resourceGroupName -Name $vm.Name -$getIp = Get-AzPublicIpAddress -Name "AzSHCILabPubIP" -ResourceGroupName $resourceGroupName -$getIp | Select-Object Name,IpAddress,@{label='FQDN';expression={$_.DnsSettings.Fqdn}} -``` - -**NOTE 1** - You'll be prompted to supply a credential for the VM - simply enter a username of your choice, and strong password. - -**NOTE 2** - When running the above script, if your VM size contains an 's', such as 'Standard_E16**s**_v4' it will use **Premium LRS storage**. If it does not contain an 's', it will deploy with a Standard HDD, which will impact performance. Refer to the [table earlier](#azure-vm-size-considerations) to determine the appropriate size for your deployment. - -**NOTE 3** - For customers with Software Assurance, Azure Hybrid Benefit for Windows Server allows you to use your on-premises Windows Server licenses and run Windows virtual machines on Azure at a reduced cost. By removing the comment in the script above, for the -LicenseType parameter, **you confirm you have an eligible Windows Server license with Software Assurance or Windows Server subscription to apply this Azure Hybrid Benefit** and have reviewed the [Azure hybrid benefit compliance document](http://go.microsoft.com/fwlink/?LinkId=859786 "Azure hybrid benefit compliance document") - -Once you've made your size and region selection, based on the information provided earlier, run the PowerShell script and wait a few moments for your VM deployment to complete. - -![Virtual machine successfully deployed with PowerShell](/media/powershell_vm_deployed.png "Virtual machine successfully deployed with PowerShell") - -With the VM successfully deployed, make a note of the fully qualified domain name, as you'll use that to connect to the VM shortly. - -#### OPTIONAL - Enable Auto-Shutdown for your VM #### -One way to control costs, is to ensure your VM automatically shuts down at the end of each day. Enabling this feature requires you to log into the Azure portal, and perform a few steps: - -Firstly, visit https://portal.azure.com/, and login with the same credentials used earlier. Once logged in, using the search box on the dashboard, enter "azshci" and once the results are returned, click on your AzSHCIHost virtual machine. - -![Virtual machine located in Azure](/media/azure_vm_search.png "Virtual machine located in Azure") - -1. Once on the overview blade for your VM, **scroll down on the left-hand navigation**, and click on **Auto-shutdown** -2. Click the Enabled slider to **On** -3. Enter your **scheduled shutdown time**, **time zone** and **notification information** -4. Click **Save** - -![Enable VM auto-shutdown in Azure](/media/auto_shutdown.png "Enable VM auto-shutdown in Azure") - -Prepare your Azure VM ------------ - -With your Azure VM (AzSHCIHost001) successfully deployed, you're ready to configure the VM to allow creation of the the Windows Server 2019 domain controller, the Windows 10 management VM, and the Azure Stack HCI 20H2 nodes. - -### Update your Azure VM ### -Firstly, you'll need to connect into the VM, with the easiest approach being via Remote Desktop. If you're not already logged into the Azure portal, visit https://portal.azure.com/, and login with the same credentials used earlier. Once logged in, using the search box on the dashboard, enter "**azshci**" and once the results are returned, **click on your AzSHCIHost001 virtual machine**. - -![Virtual machine located in Azure](/media/azure_vm_search.png "Virtual machine located in Azure") - -Once you're on the Overview blade for your VM, along the top of the blade, click on **Connect** and from the drop-down options. - -![Connect to a virtual machine in Azure](/media/connect_to_vm.png "Connect to a virtual machine in Azure") - -Select **RDP**. On the newly opened Connect blade, ensure the **Public IP** is selected, and the port is **3389**, click **Download RDP File** and select a suitable folder to store the .rdp file. - -![Configure RDP settings for Azure VM](/media/connect_to_vm_properties.png "Configure RDP settings for Azure VM") - -Once downloaded, locate the .rdp file on your local machine, and double-click to open it. Click **connect** and when prompted, enter the credentials you supplied when creating the VM earlier. Accept any certificate prompts, and within a few moments, you should be successfully logged into the Windows Server 2019 VM. - -Now that you're successfully connected to the VM, it's a good idea to ensure your OS is running the latest security updates and patches. VMs deployed from marketplace images in Azure, should already contain most of the latest updates, however it's worthwhile checking for any additional updates, and applying them as necessary. - -1. Open the **Start Menu** and search for **Update** -2. In the results, select **Check for Updates** -3. In the Updates window, click **Check for updates**. If any are required, ensure they are downloaded and installed. -4. Restart if required, and once completed, re-connect your RDP session using the steps earlier. - -With the OS updated, and back online after any required reboot, it's now time to enable the Hyper-V role and accompanying PowerShell management modules. - -### OPTIONAL - Resize your primary partition ### -If you deployed your VM with a disk size larger than the standard 127GB, run the following PowerShell command to extend your existing C: to fill the remaining space on the drive. - -```powershell -$size = (Get-PartitionSupportedSize -DriveLetter "C") -Resize-Partition -DriveLetter "C" -Size $size.SizeMax -``` - -### Configure the Hyper-V host ### -In order to run our nested workloads, you first need to enable the Hyper-V role within Windows Server 2019, and the accompanying PowerShell modules. In addition, you'll create a special NAT switch, to ensure that your nested workloads can access the internet, using the Windows Server 2019 host as the NAT gateway. - -The quickest, and easiest way to enable the required Hyper-V role and accompanying management tools, is using PowerShell. Firstly, open PowerShell **as an administrator** and run the following command: - -```powershell -# Install the Hyper-V role and management tools, including PowerShell -Install-WindowsFeature -Name Hyper-V -IncludeManagementTools -Restart -``` - ->[!WARNING] -> ->This command restarts the Azure VM. You will lose your RDP connection during the restart process. - -Once the Azure VM has fully restarted, which may take a few minutes, reconnect to your VM using the previously downloaded .rdp file. Once connected, the next step is to configure the NAT virtual switch on the VM, to enable your VMs to access the internet. - -#### Configure Internal NAT vSwitch #### -Both Windows 10 Hyper-V, and Windows Server 2019 Hyper-V allow native network address translation (NAT) for a virtual network. NAT gives a virtual machine access to network resources using the host computer's IP address and a port through an internal Hyper-V Virtual Switch. It doesn't require you to expose the sandbox VMs directly onto your physical network, or in this case, your Azure vNET. - -If you're not familiar, Network Address Translation (NAT) is a networking mode designed to conserve IP addresses by mapping an external IP address and port to a much larger set of internal IP addresses. Basically, a NAT uses a flow table to route traffic from an external (host) IP Address and port number to the correct internal IP address associated with an endpoint on the network (virtual machine, computer, container, etc.) - -To configure the network switch, open PowerShell **as an administrator** and run the following command: - -```powershell -# Create a new internal virtual switch on the host -New-VMSwitch -Name "InternalNAT" -SwitchType Internal -# Create an IP address for the NAT Gateway -New-NetIPAddress -IPAddress 192.168.0.1 -PrefixLength 24 -InterfaceAlias "vEthernet (InternalNAT)" -# Create the new NAT network -New-NetNat -Name "AzSHCINAT" -InternalIPInterfaceAddressPrefix 192.168.0.0/24 -# Check the NAT configuration -Get-NetNat -``` -The **Get-NetNat** cmdlet gets Network Address Translation (NAT) objects configured on a computer. NAT modifies IP address and port information in packet headers. Your configuration should look similar to the configuration below: - -![Result of Get-NetNat PowerShell command](/media/get_net_nat.png "Result of Get-NetNat PowerShell command") - -The final part of the process is to enable Enhanced Session mode. Enhanced Session mode can be useful to enhance the user experience, particularly when using the Windows 10 VM later, when connecting to a VM over VMConnect. To enable Enhanced Session Mode with PowerShell, run the following on AzSHCIHost001: - -```powershell -Set-VMhost -EnableEnhancedSessionMode $True -``` - -Next Steps ------------ -In this step, you've successfully created your Azure VM, and configured Windows Server 2019 with the Hyper-V role, and core networking to support the nested scenario. You're now ready to start creating your virtual machines as part of deploying your management infrastructure. You have 2 choices on how to proceed, either a more graphical way, using a GUI (Graphical User Interface, such as Hyper-V Manager, Server Manager etc), or via PowerShell. Make your choice below: - -* [**Part 2a** - Deploy your management infrastructure with the GUI](/nested/steps/2a_ManagementInfraGUI.md "Deploy your management infrastructure with the GUI") -* [**Part 2b** - Deploy your management infrastructure with PowerShell](/nested/steps/2b_ManagementInfraPS.md "Deploy your management infrastructure with PowerShell") - -Product improvements ------------ -If, while you work through this guide, you have an idea to make the product better, whether it's something in Azure Stack HCI 20H2, Windows Admin Center, or the Azure Arc integration and experience, let us know! We want to hear from you! [Head on over to our Azure Stack HCI 20H2 UserVoice page](https://feedback.azure.com/forums/929833-azure-stack-hci "Azure Stack HCI 20H2 UserVoice"), where you can share your thoughts and ideas about making the technologies better. If however, you have an issue that you'd like some help with, read on... - -Raising issues ------------ -If you notice something is wrong with the evaluation guide, such as a step isn't working, or something just doesn't make sense - help us to make this guide better! Raise an issue in GitHub, and we'll be sure to fix this as quickly as possible! - -If however, you're having a problem with Azure Stack HCI 20H2 **outside** of this evaluation guide, make sure you post to [our Microsoft Q&A forum](https://docs.microsoft.com/en-us/answers/topics/azure-stack-hci.html "Microsoft Q&A Forum"), where Microsoft experts and valuable members of the community will do their best to help you. - -Full Script - Prepare your VM ------------ - -```powershell -# Resize primary partition -$size = (Get-PartitionSupportedSize -DriveLetter "C") -Resize-Partition -DriveLetter "C" -Size $size.SizeMax - -# Install the Hyper-V role and management tools, including PowerShell -Install-WindowsFeature -Name Hyper-V -IncludeManagementTools -Restart - -# Create a new internal virtual switch on the host -New-VMSwitch -Name "InternalNAT" -SwitchType Internal -# Create an IP address for the NAT Gateway -New-NetIPAddress -IPAddress 192.168.0.1 -PrefixLength 24 -InterfaceAlias "vEthernet (InternalNAT)" -# Create the new NAT network -New-NetNat -Name "AzSHCINAT" -InternalIPInterfaceAddressPrefix 192.168.0.0/24 -# Check the NAT configuration -Get-NetNat - -Set-VMhost -EnableEnhancedSessionMode $True -``` \ No newline at end of file