diff --git a/.changelog.yml b/.changelog.yml new file mode 100644 index 0000000..b5c87bb --- /dev/null +++ b/.changelog.yml @@ -0,0 +1,106 @@ +# This generates CHANGELOG.md and changelog entries in the box description +changelog: +- version: '1.0.0' + date: 2021-06-19 + changes: + - Removed Packer as part of the build process, this runs using Ansible only. + - Removed the Server 2008 and 2008 R2 builds as they are end of life. + - Disabled shutdown tracker UI by default. + - Added Server 2022 based on the latest preview ISO on the evaluation centre. + - Added `pwsh` to the image and configured PSRemoting of `pwsh` for both SSH and WinRM. + - Recreate RDP certificate to use SHA256 as SHA1 is being deprecated. + - Enable a few Hyper-V features for the default QEMU/Libvirt Vagrantfile. + - Updated OpenSSH version to [v8.6.0.0p1-Beta](https://github.com/PowerShell/Win32-OpenSSH/releases/tag/V8.6.0.0p1-Beta). + - Updated VirtIO driver version to `0.1.185`. + - Raised minimum Ansible version to `2.9.0`. + host_specific_changes: + '2022': + - New build added in this version + +- version: '0.7.0' + date: 2019-12-20 + changes: + - Added `qemu/libvirt` boxes and default template to use VirtIO drivers for better performance + - Pin the VirtIO driver version to a specific version that can be manually updated across version. Currently at the latest stable version of `0.1.171`. + - Updated OpenSSH version to [v8.0.0.0p1-Beta](https://github.com/PowerShell/Win32-OpenSSH/releases/tag/v8.0.0.0p1-Beta) + - Raised minimum Ansible version to `2.7.0`. + - Swapped the connection plugin from `winrm` to `psrp` for faster builds. The [pypsrp](https://pypi.org/project/pypsrp/) Python library needs to be installed. + host_specific_changes: + 2008-x86: + - Enabled TLSv1.2 on the SChannel server now the patch is not faulty. + 2008-x64: + - Enabled TLSv1.2 on the SChannel server now the patch is not faulty. + +- version: '0.6.0' + date: 2019-01-20 + changes: + - Fix logic when setting the `LocalAccountTokenFilterPolicy` value when setting up the WinRM listener + - Added ability to override the base Chocolatey packages that are installed with the image, use the `opt_package_setup_packages` variable with `-e` when generating the template to configure + - Moved away from custom role to install the Win32-OpenSSH components, now using the [jborean93.win_openssh](https://galaxy.ansible.com/jborean93/win_openssh) role + - Updated OpenSSH version [7.9.0.0p1-Beta](https://github.com/PowerShell/Win32-OpenSSH/releases/tag/v7.9.0.0p1-Beta) + - Installed the [virtio-network](https://stg.fedoraproject.org/wiki/Windows_Virtio_Drivers) driver on VirtualBox images + host_specific_changes: + '2016': + - Changed the default Windows Explorer window to show `This PC` instead of `Quick access` + +- version: '0.5.0' + date: 2018-08-08 + changes: + - Disabled automatic Windows Update to eliminate post-startup thrash on older images - https://github.com/jborean93/packer-windoze/issues/10 + - Updated Win32-OpenSSH to the latest release [v7.7.2.0p1-Beta](https://github.com/PowerShell/Win32-OpenSSH/releases/tag/v7.7.2.0p1-Beta) + - Ensure WinRM HTTPS listener and firewall is configured before allowing Vagrant to detect the host is up - https://github.com/jborean93/packer-windoze/issues/11 + - Run ngen before sysprep process to try and speed up the Vagrant init time + - Clean up `C:\Windows\SoftwareDistribution\Download` and `C:\Recovery` as part of the cleanup process + +- version: '0.4.0' + date: 2018-05-16 + changes: + - Create a PS Module called `PackerWindoze` that stores the `Reset-WinRMConfig` cmdlet that recreates the WinRM configuration and keep that post sysprep for downstream users to call at any time + - Added support for the Server 1803 image + - Install the full sysinternals suite instead of just PsTools, ProcMon, and ProcExp + - Fixed issue where the WinRM HTTPS firewall rule was not created after sysprep + - Fixed issue where WinRM still allowed unencrypted data after sysprep + +- version: '0.3.0' + date: 2018-05-10 + changes: + - Updated OpenSSH version to [v7.6.1.0p1-Beta](https://github.com/PowerShell/Win32-OpenSSH/releases/tag/v7.6.1.0p1-Beta) + - Set the builtin `vagrant` account password to never expire + - Stop using the Ansible ConfigureRemotingForAnsible.ps1 script, swap over to custom script to support SHA256 and simplify steps + - Added Hyper-V builder support by specifying `-e opt_packer_setup_builder=hyperv` - This will only run on a Windows with WSL host and doesn't work for Server 2008 + host_specific_changes: + 2008-x64: + - Enabled TLSv1.2 client support, server is still disabled by default + 2008-x86: + - Enabled TLSv1.2 client support, server is still disabled by default + +- version: '0.2.0' + date: 2017-12-01 + changes: + - Create a custom Vagrantfile template for the final image that includes the username and other required settings + - Moved sysprep process before the image is created + - Added `slmgr.vbs /rearm` to run just after Vagrant starts the image to get the full evaluation period possible + - Removed SSL certificates that were created during the packer build process + - Installed [Win32-OpenSSH](https://github.com/PowerShell/Win32-OpenSSH) v0.0.23.0 on all images eacept Server 2008 + - Added .travis-ci file to run [ansible-lint](https://github.com/willthames/ansible-lint) on the Ansible files for some testing sanity + - Decided to install the VirtualBox guest additions tools as part fo the build process + - Added vim to the list of chocolatey packages to help with Core OS installs or connecting via SSH + host_specific_changes: + '1709': + - Added support for Windows Server 1709 + - This won't be available in Vagrant Cloud as it is not avaible as a public evaluation ISO + '2016': + - Will not remove Features on Demand until [this](https://social.msdn.microsoft.com/Forums/en-US/2ad1c1d9-09ba-407e-ba03-951c6f2baa34/features-on-demand-server-2016-source-not-found?forum=ws2016) is resolved + 2008r2: + - Enabled TLSv1.2 cipher support for both the client and server components + 2008-x64: + - Disabled screensaver to stop auto logoff by default + - Ensure TLSv1.2 cipher support KB is installed but not enabled due to bug in the server implementation + 2008-x86: + - Disabled screensaver to stop auto logoff by default + - Ensure TLSv1.2 cipher support KB is installed but not enabled due to bug in the server implementation + +- version: '0.0.1' + date: 2017-10-29 + changes: + - First images built by this process diff --git a/.gitignore b/.gitignore index e1ccd58..0d8dd30 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ # Packer Vagrant Cache output-virtualbox-iso/* output-hyperv-iso/* +output-qemu/* packer_cache/* .vagrant/* *.box @@ -13,22 +14,20 @@ hosts-* # Galaxy files roles/jborean93.* +collections/* +!collections/ansible_collections/jborean93/windoze/* # Temp Packer files -2008-x86/ -2008-x64/ -2008r2/ +output/ 2012 2012r2/ 2016/ 2019/ -1709/ -1803/ -10-x86/ -10-x64/ +2022/ # MacOS files .DS_Store # Various IDEs .idea/ +.vscode/ diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 0dbb3b1..0000000 --- a/.travis.yml +++ /dev/null @@ -1,16 +0,0 @@ -language: python - -python: -- "2.7" - -install: -- pip install pip setuptools -- pip install -U pip setuptools -- pip install ansible-lint - -script: -# show the ansible-lint version -- ansible-lint --version -# TODO: fix the line length ignore, also item != '' in roles/personalise/tasks/main.yml -- ansible-lint packer-setup.yml -x 204 -- ansible-lint main.yml -x 204 -x 602 diff --git a/CHANGELOG.md b/CHANGELOG.md index a27f35c..b4d9e04 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,21 @@ changelog entries to `roles/packer-setup/vars/main.yml` to modify this file_ This is the changelog of each image version uploaded to the Vagrant Cloud. It contains a list of changes that each incorporate. +### v1.0.0 - 2021-06-19 + +* Removed Packer as part of the build process, this runs using Ansible only. +* Removed the Server 2008 and 2008 R2 builds as they are end of life. +* Disabled shutdown tracker UI by default. +* Added Server 2022 based on the latest preview ISO on the evaluation centre. +* Added `pwsh` to the image and configured PSRemoting of `pwsh` for both SSH and WinRM. +* Recreate RDP certificate to use SHA256 as SHA1 is being deprecated. +* Enable a few Hyper-V features for the default QEMU/Libvirt Vagrantfile. +* Updated OpenSSH version to [v8.6.0.0p1-Beta](https://github.com/PowerShell/Win32-OpenSSH/releases/tag/V8.6.0.0p1-Beta). +* Updated VirtIO driver version to `0.1.185`. +* Raised minimum Ansible version to `2.9.0`. +* 2022 + * New build added in this version + ### v0.7.0 - 2019-12-20 * Added `qemu/libvirt` boxes and default template to use VirtIO drivers for better performance diff --git a/README.md b/README.md index 764eae1..42c2cb7 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,7 @@ # packer-windoze -This repo contains code that can generate Packer templates designed to build -and package Windows templates for use with Vagrant boxes. The overall goal is -to cover all supported Windows OS' included Windows Server, Server Core, -Server Nano and the Desktop OS' but the main focus is on the Windows Server -images. +This repo contains code that can generate Vagrant boxes. +The overall goal is to cover supported Windows Server OS'. Each image is designed to be; @@ -12,211 +9,106 @@ Each image is designed to be; * As small as can be possible for a Windows image * Contain minimal tools useful for Windows development such as the sysinternals suite * Enable WinRM (HTTP and HTTPS) and RDP on creation in Vagrant allowing other tools to interact with a new image without manual interaction +* Incldues `pwsh` (formally known as PowerShell Core) on all host types except for Server 2012 * Also include the latest [Win32-OpenSSH](https://github.com/PowerShell/Win32-OpenSSH) in the image that starts up automatically * Each image contain the maximum amount of time available on a Windows evaluation image (usually 180 days) without prompting for a key -The blog post [Using Packer to Create Windows Images](http://www.bloggingforlogging.com/2017/11/23/using-packer-to-create-windows-images/) -contain a more detailed guide on this process and how it all works. Feel free -to read through it if you want to understand each component and how they fit -together more. +The blog post [Using Packer to Create Windows Images](http://www.bloggingforlogging.com/2017/11/23/using-packer-to-create-windows-images/) contain a more detailed guide on this process and how it all works. +The contents there are outdated as `Packer` is no longer used but the generic concepts still apply here. +Feel free to read through it if you want to understand each component and how they fit together more. + +_Note: This repo used to use Packer to build the Vagrant images (hence the name) but no longer does._ ## Requirements To use the scripts in this repo you will need the following; +* [Ansible](https://github.com/ansible/ansible) >= 2.9.0 +* `mkisofs` +* `pigz` + +The following Python libraries are also used: + +* [httpx](https://pypi.org/project/httpx/) * [pypsrp](https://pypi.org/project/pypsrp/) -* [Packer](https://www.packer.io/docs/install/index.html) >= 1.0.0, 1.2.4 is required for Hyper-V with Server 2008 R2 support -* [Ansible](https://github.com/ansible/ansible) >= 2.7.0 -* `mkisofs` for Windows this needs to be installed in WSL where Ansible is located * [BeautifulSoup4](https://www.crummy.com/software/BeautifulSoup/) to retrieve the latest Windows Updates for the build -One of the following hypervisers as defined by `opt_packer_setup_builder`: +One of the following hypervisers as defined by `platform`: * [VirtualBox](https://www.virtualbox.org/wiki/Downloads) >= 5.1.12 -* [Hyper-V](https://docs.microsoft.com/en-us/windows-server/virtualization/hyper-v/hyper-v-on-windows-server) - Server 2008 is not supported with Hyper-V +* [Hyper-V](https://docs.microsoft.com/en-us/windows-server/virtualization/hyper-v/hyper-v-on-windows-server) * [QEMU](https://www.qemu.org/) -When setting `man_packer_setup_host_type: 2008-x64`, Ansible will extract the -evaluation ISO from a self extracting archive. This requires the `unrar` -package to be installed. If you don't want to install this package, manually -extract the ISO on another box and specify the path under -`opt_packer_setup_iso_path`. - -To install `mkisofs` and `unrar`, you can run one of the commands below -depending on your distribution; +To install `mkisofs` and `pigz`, you can run one of the commands below depending on your distribution; ```bash # for Debian/Ubuntu -sudo apt-get install mkisofs unrar +sudo apt-get install mkisofs pigz # for RHEL/CentOS -sudo yum install mkisofs - -sudo yum localinstall --nogpgcheck https://download1.rpmfusion.org/free/el/rpmfusion-free-release-7.noarch.rpm https://download1.rpmfusion.org/nonfree/el/rpmfusion-nonfree-release-7.noarch.rpm -sudo yum install unrar +sudo yum install mkisofs pigz # for Fedora -sudo dnf install mkisofs - -sudo dnf install https://download1.rpmfusion.org/free/fedora/rpmfusion-free-release-$(rpm -E %fedora).noarch.rpm https://download1.rpmfusion.org/nonfree/fedora/rpmfusion-nonfree-release-$(rpm -E %fedora).noarch.rpm -sudo dnf install unrar +sudo dnf install mkisofs pigz # for MacOS (requires Homebrew) -brew install cdrtools unrar +brew install cdrtools pigz ``` -## How to Run - -To create an image, the process is split up into 2 phases -1. Create the files required for Packer to build and provision an image -2. Run Packer based on the files created above - -### Create Packer Files - -For Packer to provision a Windows host it first needs an `Autounattend.xml` -file and bootstrapping script to configure the base host requirements needed by -Ansible. Instead of having them already stored in the repo, they are -dynamically created by Ansible based on the configuration provided. +## How to Run -To create the files for a particular host type run; +The imaging process uses Ansible from start to finish and in most cases can be rerun for it to take off where it started. +To start the process run the following script: ```bash -ansible-playbook packer-setup.yml -e man_packer_setup_host_type= - -# see below what can be used for but to create the Packer files for a Server 2012 R2 image run -ansible-playbook packer-setup.yml -e man_packer_setup_host_type=2012r2 - -# specify custom Chocolatey packages to install instead of vim and sysinternals on the image -ansible-playbook packer-setup.yml -e opt_packer_setup_packages='["pstools", "notepadplusplus"]' - -# when running on Windows, you can run this from PowerShell like -bash.exe -ic "ansible-playbook packer-setup.yml -e man_packer_setup_host_type=2012r2 -e opt_packer_setup_builder=hyperv" +ansible-playbook main.yml --limit '*2022'` ``` -After running the playbook, a folder which is named after the value of -`man_packer_setup_host_type` will be created and it will contain the following -files; - -* `iso/Autounattend.xml`: The answer file used by Windows during the initial install -* `iso/bootstrap.ps1`: A PowerShell script that is run after the initial install to configure the host required by Ansible -* `iso/*`: Other files used in the bootstrapping process like hotfixes and updates required to configure WinRM -* `configure-hyperv-network.ps1`: Used in the Hyper-V builder to set the correct IP for Ansible's inventory file -* `description.md`: The changelog description of that current build -* `hosts.ini`: An Ansible inventory file containing the info required by Ansible during the provisioning phase -* `packer.json`: The Packer definition file the contains all the info that Packer needs to build the image -* `secondary.iso`: The secondary ISO file used to store the `Autounattend.xml`, `bootstrap.ps1`, and other files used in that process -* `vagrantfile.template`: The templated Vagrantfile that is embedded in the Vagrant box produced - -When `opt_packer_setup_builder=hyperv`, this process will also create the -Hyper-V switch defined by `opt_packer_setup_hyperv_switch` if it does not -exist. This switch is created as an External Network type with the host OS -allowed to share with the guest. This type of switch is required for 2 reasons; +This will build the Windows Server 2022 image for QEMU. +You can change `*2022` to the Windows version as defined in inventory.yml that you wish to build (the `*` is important). +The following options can also be specified with `-e` to change the build behaviour: -1. Allows the Windows host to access the guest -2. Allows the guest to access the internet for things like updates and downloading packages +* `platform`: The Hypervisor to build for - can be `qemu`, `virtualbox`, or `hyperv` (default: `qemu`). +* `output_dir`: The base directory to store the output/build files (default: `{{ playbook_dir }}/output`). +* `setup_username`: The name of the user to create on the base image +* `setup_password`: The password to apply to the username that is created. +* `iso_src_`: The URL or path to use for the install ISO, change `` to the inventory hostname, e.g. `2022`, or `2019`. +* `iso_checksum_`: The checksum for `iso_src`, change `` to the inventory hostname, e.g. `2022`, or `2019`. +* `iso_wim_label_`: The Windows install WIM label to install, change `` to the inventory hostname, e.g. `2022`, or `2019`. -An Internal Network type covers the first point but you need an External -Network type to access the internet. +It is technically possible to build more than 1 image at a time by specifying multiple hosts with `--limit` but it is recommended to kick off the runs in parallel to keep better track. -This switch is NOT cleaned up afterwards automatically. +After running the image process will have created a few files in `{{ output_dir }}/{{ host }}`: -#### Mandatory Variables +* `description.md`: A markdown description of the box created. +* `{{ platform }}.box`: The box for the specific platform hypervisor. -The following parameters must be set using the `-e` arguments; - -* `man_packer_setup_host_type`: The host type string that tells packer what to build, see options below - -You can set the host type to the following values - -* `2008-x86`: Windows Server 2008 Standard 32-bit -* `2008-x64`: Windows Server 2008 Standard 64-bit -* `2008r2`: Windows Server 2008 R2 Standard -* `2012`: Windows Server 2012 Standard -* `2012r2`: Windows Server 2012 R2 Standard -* `2016`: Windows Server 2016 Standard -* `2019`: Windows Server 2019 Standard - -The following host types can also be used but it requires the ISO to be -manually downloaded and set with `opt_packager_setup_iso_path`. Microsoft does -not offer evaluation ISOs for these builds so it won't be part of the public -facing images - -* `1709`: Windows Server Build 1709 Standard -* `1803`: Windows Server Build 1803 Standard - -#### Optional Variables - -The following are optional parameters set using the `-e` argument and can -change the way Packer builds the images in the next step; - -* `opt_packer_setup_builder`: The Packer builder to use, defaults to `virtualbox` but can `qemu`, or `hyperv` when running on Windows. -* `opt_packer_setup_disk_size_mib`: The size in mebibytes' (`MiB`) of the OS disk to create (default: `40960`). -* `opt_packer_setup_headless`: Used for debugging, will display the Windows console during the build if set to `False` (default: `True`) -* `opt_packer_setup_iso_path`: The local path to the install Windows ISO, this means packer will use this instead of downloading the pre-set evaluation ISO from the internet. -* `opt_packer_setup_iso_wim_label`: The WIM image name to use when installing Windows from an ISO, the process defaults to the Standard edition if not set. -* `opt_packer_setup_username`: (Default: `vagrant`) The name of the user to create in the provisioning process, this is the only user that will available in the image created as the builtin Administrator account is disabled. -* `opt_packer_setup_password`: (Default: `vagrant`) The password for `opt_packer_setup_username`, this password is also set for the builtin Administrator account even though it is disabled in the image. -* `opt_packer_setup_product_key`: The product key to use when installing Windows, do not set this unless you know what you are doing. -* `opt_packer_setup_hyperv_switch`: (Default: `packer-windoze`) The name of the Hyper-V switch to create. There shouldn't be a need to change this unless you know what you're doing. -* `opt_packer_setup_packages`: (Default: `vim`, `sysinternals`) Override the default Chocolatey packages that are installed on each image. This should be a list of valid Chocolatey package names that are packes to the `win_chocolatey` module, see the examples for more details. - -To add a post-processor to upload to Vagrant Cloud, add in the following 3 -variables; - -* `opt_packer_setup_access_token`: The acces token for the Vagrant Cloud API, this is set to the `access_token` key in the packer build file. -* `opt_packer_setup_box_tag`: The shorthand tag for the map that maps to Vagrant Cloud, this is set to the `box_tag` key in the packer build file. -* `opt_packer_setup_version`: The version number for the box which is validated based on semantic versioning, this is set to the `version` key in the packer build file and if ommitted then the latest version in the changelog is used. - -### Create Images with Packer - -Once the packer files have been created under it's own folder, Packer can now -be used to create the image. Run the following to start the process - -```bash -packer build -force /packer.json - -# replace with the type to build, e.g. for Server 2012 R2 -packer build -force 2012r2/packer.json -``` +### Hyper-V and WSL -This process takes a looong time to finish as Packer will download the ISO, -install Windows and finally configure Windows. The best thing to do is to run -this overnight or as a background process. +Because Ansible cannot run natively on Windows the Hyper-V builder must be run on WSL. +The current process has been tested on WSL2 and will probably not work for WSL1. +Before kicking off the run on WSL you must ensure that you've started the WSL process as an administrator so it has access to manage Hyper-V VMs. +You also need to either run this repo from a Windows path or specify `-e output_dir=/mnt/c/some/path` so that Hyper-V can access the build artifacts. -Once complete a file with the `.box` extension will be created in the same -folder the `packer.json` file is located in. This file can be added to Vagrant -using `vagrant box add .box` or can be shared with others using your own -methods. The filename is dependent on the builder that was used. When -`opt_packer_setup_builder` is `virtualbox` then it will be `virtualbox.box`, -otherwise `hyperv` will be `hyperv.box`. ## What It Does Here is a brief step by step overview of what actually happens with the images -1. Packer start a VM under the hypervisor and attaches the ISO that contains the `Autounattend.xml`, `bootstrap.ps1`, and other bootstrap files -2. Windows starts the install process and configures it according to the `Autounattend.xml` file -3. After the install is complete Windows will auto login to the `vagrant` user and run the `bootstrap.ps1` script -4. The bootstrap script will ensure that PowerShell v3 or greater is installed, WinRM is setup and other things -5. Packer detects that WinRM is up and running and starts the provision process which is the Ansible playbook -6. Ansible will then install all available updates and reboot accordingly (this step can take hours so be prepared to wait) -7. Some personalisation tweaks occur such as showing hidden files and folders, file extensions and installing the sysinternals tools -8. Will try to cleanup as much of the WinSXS folder as possible (older hosts are limited in how much it can do) -9. Will remove all non enabled Features if Features on Demand is supported (Server 2012 and newer) -10. Remove pagefile, temp files, log files that are not needed. Defrags the disk and 0's out empty space for the compression to work properly -11. Setup the sysprep template files -12. Remove the WinRM listeners and run the sysprep process to shutdown the host - -From this point Packer will create an image of the OS which can be used by -Vagrant. When Vagrant first starts up the image, it will automatically log on -and, rearm the activation key and recreate the WinRM listeners. - -## Backlog/Future Work - -* Windows Server Core images -* Windows Server Nano images -* Windows 10 32 and 64 bit -* Look at supporting parallel builds -* Look at slipstreaming Windows updates into the evaluation ISO's instead of running the updates from scratch each time -* Look at supporting local WSUS servers during the Update phase to save time and bandwidth +1. Ansible prepares the unattended install of Windows including the latest available updates and install ISOs +1. Ansible kicks off the Hypervisor to create and run the VM +1. Windows starts the install process and configures it according to the `Autounattend.xml` file generated by Ansible +1. After the install process is complete, Windows will auto login the `vagrant` user and run the `bootstrap.ps1` script +1. The bootstrap script will ensure that the base updates are applied and WinRM is set up for Ansible to talk to +1. Ansible will then run the provisioning steps against that host over the newly set up WinRM connection +1. Ansible will then install all available updates and reboot accordingly (this step can take hours so be prepared to wait) +1. Some personalisation tweaks occur such as showing hidden files and folders, file extensions and installing the sysinternals tools +1. Will try to cleanup as much of the WinSXS folder as possible (older hosts are limited in how much it can do) +1. Will remove all non enabled Features if Features on Demand is supported (Server 2012 and newer) +1. Remove pagefile, temp files, log files that are not needed. Defrags the disk and 0's out empty space for the compression to work properly +1. Setup the sysprep template files +1. Remove the WinRM listeners and run the sysprep process to shutdown the host + +From this point Ansible will create an image of the OS which can be used by Vagrant. +When Vagrant first starts up the image, it will automatically log on and, rearm the activation key and recreate the WinRM listeners. diff --git a/ansible.cfg b/ansible.cfg new file mode 100644 index 0000000..78b5e94 --- /dev/null +++ b/ansible.cfg @@ -0,0 +1,5 @@ +[defaults] +inventory = inventory.yml +retry_files_enabled = False +stdout_callback = yaml +jinja2_native = True diff --git a/collections/ansible_collections/jborean93/windoze/plugins/action/psremoting.py b/collections/ansible_collections/jborean93/windoze/plugins/action/psremoting.py new file mode 100644 index 0000000..6c0b674 --- /dev/null +++ b/collections/ansible_collections/jborean93/windoze/plugins/action/psremoting.py @@ -0,0 +1,123 @@ +# Copyright: (c) 2021, Jordan Borean (@jborean93) +# MIT License (see LICENSE or https://opensource.org/licenses/MIT) + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +import time +import traceback + +from ansible.plugins.action import ActionBase +from ansible.utils.display import Display + +from typing import ( + Dict, +) + +display = Display() + + +class ActionModule(ActionBase): + + def run(self, tmp=None, task_vars=None): + self._supports_check_mode = True + self._supports_async = False + + result = self._execute_module( + module_name='ansible.windows.win_shell', + module_args={ + '_raw_params': 'Get-PSSessionConfiguration -Name PowerShell.* -ErrorAction SilentlyContinue', + 'executable': 'pwsh', + }, + task_vars=task_vars, + ) + if result['stdout'].strip(): + return {'changed': False} + + res = {'changed': True} + + if self._play_context.check_mode: + return res + + # We run with async as it allows the process to outlive the WinRM connection which is bounced with + # Enable-PSRemoting. Inspired from community.windows.win_pssession_configuration. + self._task.async_val = 60 + self._task.poll = 5 + async_result = self._execute_module( + module_name='ansible.windows.win_shell', + module_args={ + '_raw_params': '$ErrorActionPrefence = "Stop"; Enable-PSRemoting -Force', + 'executable': 'pwsh', + }, + task_vars=task_vars, + ) + jid = async_result['ansible_job_id'] + + # Turn off async so we don't run the following actions as async + self._task.async_val = 0 + wait_for_action = self._get_action_task('wait_for_connection', { + 'timeout': 60, + 'sleep': 5, + }) + status_action = self._get_action_task('async_status', { + 'jid': jid, + 'mode': 'status', + }) + + tries = 0 + while True: + try: + # check up on the async job + job_status = status_action.run(task_vars=task_vars) + + if job_status.get('failed', False): + res.update(job_status) # Includes the failure information + break + + if job_status.get('finished', False): + break + + time.sleep(self._task.poll) + + except Exception as e: + tries += 1 + if tries == 5: + return { + 'msg': f'Unknown failure while waiting for task to complete: {e!s}', + 'exception': traceback.format_exc(), + } + + display.vvvv(f'Failure while waiting for task to complete (running wait_for_connection): {e!s}') + wait_for_action.run(task_vars=task_vars) + + cleanup_action = self._get_action_task('async_status', { + 'jid': jid, + 'mode': 'cleanup', + }) + try: + cleanup_res = cleanup_action.run(task_vars=task_vars) + if cleanup_res.get('failed', False): + display.warning(f"Clean up of async status failed on the remote host: {cleanup_res.get('msg', cleanup_res)}") + + except Exception as e: + display.warning(f"Clean up of async status failed on the remote host: {e}") + + return res + + def _get_action_task( + self, + action: str, + action_args: Dict, + ): + action_task = self._task.copy() + action_task.args = action_args + + return self._shared_loader_obj.action_loader.get( + action, + task=action_task, + connection=self._connection, + play_context=self._play_context, + loader=self._loader, + templar=self._templar, + shared_loader_obj=self._shared_loader_obj + ) diff --git a/collections/ansible_collections/jborean93/windoze/plugins/filter/ip_addr.py b/collections/ansible_collections/jborean93/windoze/plugins/filter/ip_addr.py new file mode 100644 index 0000000..88b7f0f --- /dev/null +++ b/collections/ansible_collections/jborean93/windoze/plugins/filter/ip_addr.py @@ -0,0 +1,36 @@ +# Copyright: (c) 2021, Jordan Borean (@jborean93) +# MIT License (see LICENSE or https://opensource.org/licenses/MIT) + +import ipaddress + +from typing import ( + List, +) + +def ip_addr( + value: str, + reserved: List[str], + idx: int +) -> str: + """Gets the next IP address available.""" + interface = ipaddress.ip_interface(value) + reserved_ips = set(reserved) + reserved_ips.add(str(interface.ip)) + + for next_ip in interface.network.hosts(): + if str(next_ip) in reserved: + continue + + if idx == 0: + return str(next_ip) + + else: + idx -= 1 + + +class FilterModule: + + def filters(self): + return { + 'ip_addr': ip_addr, + } diff --git a/collections/ansible_collections/jborean93/windoze/plugins/lookup/ip_info.py b/collections/ansible_collections/jborean93/windoze/plugins/lookup/ip_info.py new file mode 100644 index 0000000..dbfcb6d --- /dev/null +++ b/collections/ansible_collections/jborean93/windoze/plugins/lookup/ip_info.py @@ -0,0 +1,101 @@ +# Copyright: (c) 2021, Jordan Borean (@jborean93) +# MIT License (see LICENSE or https://opensource.org/licenses/MIT) + +DOCUMENTATION = ''' +lookup: ip_info +author: Jordan Borean (@jborean93) +short_description: Retrieves the IP address and default gateway of a WSL host +description: +- This lookup returns the IP address and the default gatewya of the WSL2 host. +options: {} +''' + +EXAMPLES = """ +- set_fact: + ip_info: '{{ lookup("jborean93.windoze.ip_info") }}" +""" + +RETURN = """ +_raw: + description: + - IP address and default gateway. + type: dict +""" + +import fcntl +import ipaddress +import socket +import struct + +from ansible.errors import AnsibleLookupError +from ansible.plugins.lookup import LookupBase + +from typing import ( + Tuple, + Union, +) + + +def get_gateway( + interface: str +) -> str: + with open("/proc/net/route") as fh: + for line in fh: + fields = line.strip().split() + if ( + fields[0] != interface or + fields[1] != '00000000' or + not int(fields[3], 16) & 2 + ): + continue + + return socket.inet_ntoa(struct.pack(" Tuple[Union[ipaddress.IPv4Interface, ipaddress.IPv6Interface], str]: + with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as s: + # SIOCGIFADDR + b_ip = fcntl.ioctl(s, 0x8915, struct.pack('256s', interface.encode()))[20:24] + ip = socket.inet_ntoa(b_ip) + + # SIOCGIFNETMASK + b_mask = fcntl.ioctl(s, 0x0891b, struct.pack('256s', interface.encode()))[20:24] + mask = socket.inet_ntoa(b_mask) + + gateway = get_gateway(interface) + + return ipaddress.ip_interface(f'{ip}/{mask}'), gateway + + +class LookupModule(LookupBase): + + def run(self, terms, variables=None, **kwargs): + #self.set_options(var_options=variables, direct=kwargs) + + for _, name in socket.if_nameindex(): + interface = gateway = None + try: + interface, gateway = get_interfaceinfo(name) + if ( + interface.is_link_local or + interface.is_loopback or + interface.is_multicast or + interface.is_reserved + ): + continue + + break + + except: + continue + + if interface is None: + raise AnsibleLookupError('Failed to find WSL interface details') + + return [{ + 'ip': str(interface), + 'prefixlen': interface.network.prefixlen, + 'gateway': gateway, + }] diff --git a/collections/ansible_collections/jborean93/windoze/plugins/module_utils/update_catalog.py b/collections/ansible_collections/jborean93/windoze/plugins/module_utils/update_catalog.py new file mode 100644 index 0000000..266ac78 --- /dev/null +++ b/collections/ansible_collections/jborean93/windoze/plugins/module_utils/update_catalog.py @@ -0,0 +1,277 @@ +# Copyright: (c) 2021, Jordan Borean (@jborean93) +# MIT License (see LICENSE or https://opensource.org/licenses/MIT) + +import asyncio +import collections +import datetime +import json +import re +import traceback +import uuid + +from typing import ( + Any, + AsyncIterable, + Awaitable, + Callable, + Dict, + List, + Optional, + Tuple, +) + +HTTPX_IMP_ERR = None +try: + import httpx + HAS_HTTPX = True +except ImportError: + HTTPX_IMP_ERR = traceback.format_exc() + HAS_HTTPX = False + + +BS_IMP_ERR = None +try: + import bs4 + HAS_BS = True +except ImportError: + BS_IMP_ERR = traceback.format_exc() + HAS_BS = False + + +CATALOG_URL = 'https://www.catalog.update.microsoft.com/' +DOWNLOAD_PATTERN = re.compile(r'\[(\d*)\]\.url = [\"\'](http[s]?://w{0,3}.?download\.windowsupdate\.com/[^\'\"]*)') +PRODUCT_SPLIT_PATTERN = re.compile(r',(?=[^\s])') + + +class WindowsUpdate(collections.namedtuple('WindowsUpdate', [ + 'title', 'products', 'classification', 'last_update', 'version', + 'size', 'update_id', 'architecture', 'description', 'download_urls', + 'kb_numbers', 'more_information', 'msrc_number', 'msrc_severity', + 'support_url', + ])): + + def __str__(self): + return self.title + + +class WUDownloadInfo(collections.namedtuple('WUDownloadInfo', [ + 'url', 'digest', 'architectures', 'languages', 'long_languages', + 'file_name', + ])): + + def __str__(self): + return f'{self.file_name or "unknown"} - {self.long_languages or "unknown language"}' + + +async def _invoke_request( + proc_func: Callable[[str], Any], + func: Callable[[Any], Awaitable[Any]], + *args, + **kwargs +) -> Any: + # The update catalog is crazy unstable and can comsetimes return an error, no response, something else. Instead we + # just try the request multiple times until it works. Not great but I don't have time to find out a better way. + failed_once = False + + while True: + try: + resp = await func(*args, **kwargs) + resp_text = resp.content.decode().strip() + return proc_func(resp_text) + + except: + if failed_once: + # I found that if it fails at least once adding a sleep between attempts helps. + await asyncio.sleep(1) + + failed_once = True + + +async def _get_update_details( + client: httpx.AsyncClient, + update_id: str, +) -> bs4.BeautifulSoup: + + def proc_func(text: str) -> bs4.BeautifulSoup: + details = bs4.BeautifulSoup(text, 'html.parser') + body_class_list = details.body['class'] + if 'error' in body_class_list: + raise Exception("Invalid response - try again") + + return details + + return await _invoke_request(proc_func, client.get, f'{CATALOG_URL}ScopedViewInline.aspx', + params={'updateId': update_id}) + + +async def _get_update_download_urls( + client: httpx.AsyncClient, + update_id: str, +) -> List[WUDownloadInfo]: + + def proc_func(text: str) -> Tuple[str, List]: + link_matches = re.findall(DOWNLOAD_PATTERN, text) + if len(link_matches): + return text, link_matches + + else: + raise Exception("Invalid response - try again") + + update_ids = json.dumps({ + 'size': 0, + 'updateID': update_id, + 'uidInfo': update_id, + }) + resp_text, link_matches = await _invoke_request(proc_func, client.post, f'{CATALOG_URL}DownloadDialog.aspx', + data={'updateIDs': f'[{update_ids}]'}) + + urls = [] + for download_id, url in link_matches: + attribute_map = { + 'digest': 'digest', + 'architectures': 'architectures', + 'languages': 'languages', + 'long_languages': 'longLanguages', + 'file_name': 'fileName', + } + for attrib_name, raw_name in attribute_map.items(): + regex_pattern = r"\[%s]\.%s = ['\"]([\w\-\.=+\/\(\) ]*)['\"];" % ( + re.escape(download_id), re.escape(raw_name)) + + regex_match = re.search(regex_pattern, resp_text) + if regex_match: + attribute_map[attrib_name] = regex_match.group(1) + + else: + attribute_map[attrib_name] = None + + urls.append(WUDownloadInfo(url, **attribute_map)) + + return urls + + +async def _parse_raw_update( + client: httpx.AsyncClient, + raw_element: bs4.Tag, +) -> WindowsUpdate: + cells = raw_element.find_all('td') + + update_id = cells[7].find('input').attrs['id'] + details, download_urls = await asyncio.gather( + _get_update_details(client, update_id), + _get_update_download_urls(client, update_id), + ) + + raw_kb = details.find(id='ScopedViewHandler_labelKBArticle_Separator') + # If no KB's apply then the value will be n/a. Technically an update can have multiple KBs but I have + # not been able to find an example of this so cannot test that scenario. + kb_numbers = [int(n.strip()) for n in list(raw_kb.next_siblings) if n.strip().lower() != 'n/a'] + + raw_info = details.find(id='ScopedViewHandler_labelMoreInfo_Separator') + raw_msrc_number = details.find(id='ScopedViewHandler_labelSecurityBulliten_Separator') + raw_support_url = details.find(id='ScopedViewHandler_labelSupportUrl_Separator') + + return WindowsUpdate( + title=cells[1].get_text().strip(), + products=list(filter(None, re.split(PRODUCT_SPLIT_PATTERN, cells[2].get_text().strip()))), + classification=cells[3].get_text().strip(), + last_update=datetime.datetime.strptime(cells[4].get_text().strip(), '%m/%d/%Y'), + version=cells[5].get_text().strip(), + size=int(cells[6].find_all('span')[1].get_text().strip()), + update_id=uuid.UUID(update_id), + architecture=details.find(id='ScopedViewHandler_labelArchitecture_Separator').next_sibling.strip(), + description=details.find(id='ScopedViewHandler_desc').get_text(), + download_urls=download_urls, + kb_numbers=kb_numbers, + more_information=list(raw_info.next_siblings)[1].get_text().strip(), + msrc_number=list(raw_msrc_number.next_siblings)[0].strip(), + msrc_severity=details.find(id='ScopedViewHandler_msrcSeverity').get_text().strip(), + support_url=list(raw_support_url.next_siblings)[1].get_text().strip(), + ) + + +async def get_updates( + client: httpx.AsyncClient, + search: str, + all_updates: bool = False, + sort: Optional[str] = None, + sort_reverse: bool = False, + data: Dict = None, +) -> AsyncIterable[WindowsUpdate]: + """Gets all updates based on the search criteria. + + Async generator function that outputs WindowsUpdate objects for each + update found on the Microsoft Update catalog. + + Args: + search: The search string to use when searching the update catalog. + all_updates: Set to True to yield all updates and not just the first + 25. This can increase the runtime quite dramatically so use with + caution. + sort: The field name as seen in the update catalog GUI to sort by. + sort_reverse: Reverse the sort order if sort is set. + data: Data to post to the request, used internally. + + Returns: + (AsyncIterable[WindowsUpdate]): An async iterable that yields a + WindowsUpdate object for each update found. + """ + resp = await client.post(f'{CATALOG_URL}Search.aspx', data=data, params={'q': search}) + resp_text = resp.content.decode().lstrip() + catalog = bs4.BeautifulSoup(resp_text, 'html.parser') + + # If we need to perform an action (like sorting or next page) we need to add these 4 fields that are based on the + # original response received. + def build_action_data(action): + data = { + '__EVENTTARGET': action, + } + for field in ['__EVENTARGUMENT', '__EVENTVALIDATION', '__VIEWSTATE', '__VIEWSTATEGENERATOR']: + element = catalog.find(id=field) + if element: + data[field] = element.attrs['value'] + + return data + + matches = catalog.find(id='ctl00_catalogBody_updateMatches') + if not matches: + return + raw_updates = matches.find_all('tr') + + if sort: + # Lookup the header click JS targets based on the header name to sort. + headers = raw_updates[0] # The first entry in the table are the headers which we may use for sorting. + header_links = headers.find_all('a') + event_targets = dict((l.find('span').get_text(), l.attrs['id'].replace('_', '$')) for l in header_links) + data = build_action_data(event_targets[sort]) + + sort = sort if sort_reverse else None # If we want to sort descending we need to sort it again. + async for update in get_updates(client, search, all_updates=all_updates, sort=sort, data=data): + yield update + + return + + # Would like to use asyncio.as_completed but we do care about the order here + coros = [_parse_raw_update(client, u) for u in raw_updates[1:]] + for update in await asyncio.gather(*coros): + yield update + + # ctl00_catalogBody_nextPage is set when there are no more updates to retrieve. + last_page = catalog.find(id='ctl00_catalogBody_nextPage') + if not last_page and all_updates: + data = build_action_data('ctl00$catalogBody$nextPageLinkText') + async for update in get_updates(client, search, all_updates=True, data=data): + yield update + + +def get_client() -> httpx.AsyncClient: + """Returns a httpx client that can be used with get_updates().""" + transport = httpx.AsyncHTTPTransport( + limits=httpx.Limits(max_keepalive_connections=5), + retries=5, + ) + headers = { + 'Content-Type': 'application/x-www-form-urlencoded', + 'User-Agent': 'packet-windoze', + } + return httpx.AsyncClient(headers=headers, timeout=30, transport=transport) diff --git a/collections/ansible_collections/jborean93/windoze/plugins/modules/psremoting.py b/collections/ansible_collections/jborean93/windoze/plugins/modules/psremoting.py new file mode 100644 index 0000000..b626e12 --- /dev/null +++ b/collections/ansible_collections/jborean93/windoze/plugins/modules/psremoting.py @@ -0,0 +1,26 @@ +#!/usr/bin/python + +# Copyright: (c) 2021, Jordan Borean (@jborean93) +# MIT License (see LICENSE or https://opensource.org/licenses/MIT) + +DOCUMENTATION = r''' +module: psremoting: +short_description: Enable pwsh PSRemoting endpoint +description: +- Enables the PSRemoting endpoint for pwsh. +notes: +- This module uses async internally to survive the WinRM service being restarted when enabling the remoting endpoint. + Do not run with async explictly. +options: {} +author: +- Jordan Borean (@jborean93) +''' + +EXAMPLES = r''' +- name: enable PSRemoting for pwsh + jborean93.windoze.psremoting: +''' + +RETURN = r''' +# Nothing +''' diff --git a/collections/ansible_collections/jborean93/windoze/plugins/modules/win_update_info.py b/collections/ansible_collections/jborean93/windoze/plugins/modules/win_update_info.py new file mode 100644 index 0000000..69680dc --- /dev/null +++ b/collections/ansible_collections/jborean93/windoze/plugins/modules/win_update_info.py @@ -0,0 +1,166 @@ +#!/usr/bin/python + +# Copyright: (c) 2021, Jordan Borean (@jborean93) +# MIT License (see LICENSE or https://opensource.org/licenses/MIT) + +DOCUMENTATION = r''' +module: win_update_info: +short_description: Get Windows Update information +description: +- Gets Windows Update information from the Microsoft Update catalog. +options: + name: + description: + - A list of search terms to search in the update catalog. + - The results of each search term are a list in the C(updates) return result. + type: list + elements: str + required: True + architecture: + description: + - The architecture each update should match. + - If the update arch does not match this value then it is not returned. + type: str + product: + description: + - The update product to filter the update results by. + type: str + ignore_terms: + description: + - Filter the found update titles with thse regex terms. + - If matched the update is skipped and not returned. + type: list + elements: str + sort: + description: + - Whether to sort the results using these categories. + - The sorting rules are based on the Microsoft Update Catalog site and is filtered server side. + type: str + choices: + - title + - products + - classification + - last_updated + - version + - size +requirements: +- beautifulsoup4 +- httpx +author: +- Jordan Borean (@jborean93) +''' + +EXAMPLES = r''' +- name: get update information + jborean93.windoze.win_update_info: + name: + - Servicing Stack Update for Windows Server 2019 + - Cumulative Update for Windows Server 2019 + product: Windows Server 2019 + architecture: amd64 + sort: latest_updated +''' + +RETURN = r''' +updates: + description: + - A list of lists containing the found updates. + - The list entries correlate to the C(name) terms + type: list + elements: list +''' + +import asyncio +import re + +from ansible.module_utils.basic import AnsibleModule, missing_required_lib + +from ..module_utils import update_catalog + + +async def run_module(): + module = AnsibleModule( + argument_spec=dict( + name=dict(type='list', elements='str', required=True), + architecture=dict(type='str'), + product=dict(type='str'), + ignore_terms=dict(type='list', elements='str'), + sort=dict(type='str', choices=['title', 'products', 'classification', 'last_updated', 'version', 'size']), + ), + supports_check_mode=True, + ) + + result = dict( + changed=False, + updates=[], + ) + + arch = (module.params['architecture'] or '').lower() + product = module.params['product'] + sort = module.params['sort'] + ignore_terms = module.params['ignore_terms'] or [] + if sort: + sort = { + 'title': 'Title', + 'products': 'Products', + 'classification': 'Classification', + 'last_updated': 'Last Updated', + 'version': 'Version', + 'size': 'Size', + }[sort] + + if not update_catalog.HAS_BS: + msg = missing_required_lib("beautifulsoup4", url="https://pypi.org/project/beautifulsoup4/") + module.fail_json(msg=msg, exception=update_catalog.BS_IMP_ERR, **result) + + if not update_catalog.HAS_HTTPX: + msg = missing_required_lib("httpx", url="https://pypi.org/project/httpx/") + module.fail_json(msg=msg, exception=update_catalog.BS_IMP_ERR, **result) + + async with update_catalog.get_client() as client: + raw_updates = await asyncio.gather(*[search_update(client, n, sort) for n in module.params['name']]) + + for updates in raw_updates: + name_updates = [] + for update in updates: + if product and product not in update.products: + continue + + if arch and arch != update.architecture.lower(): + continue + + matched = False + for term in ignore_terms: + if re.search(term, update.title): + matched = True + break + if matched: + continue + + name_updates.append({ + 'id': str(update.update_id), + 'title': update.title, + 'kb': f'KB{update.kb_numbers[0]}', + 'url': update.download_urls[0].url, + 'filename': update.download_urls[0].file_name, + }) + + result['updates'].append(name_updates) + + module.exit_json(**result) + + +async def search_update(client, search, sort): + updates = [] + async for update in update_catalog.get_updates(client, search, sort=sort): + updates.append(update) + + return updates + + +def main(): + asyncio.run(run_module()) + + +if __name__ == '__main__': + main() diff --git a/inventory.yml b/inventory.yml new file mode 100644 index 0000000..db62066 --- /dev/null +++ b/inventory.yml @@ -0,0 +1,116 @@ +all: + children: + setup: + hosts: + '2012': + box_tag: jborean93/WindowsServer2012 + + iso_src: http://download.microsoft.com/download/6/D/A/6DAB58BA-F939-451D-9101-7DE07DC09C03/9200.16384.WIN8_RTM.120725-1247_X64FRE_SERVER_EVAL_EN-US-HRM_SSS_X64FREE_EN-US_DV5.ISO + iso_checksum: sha256:19c627b6a24554bce45a3b03fc913a0d791d117088c7e953b9c07e866fd88b67 + iso_wim_label: Windows Server 2012 SERVERSTANDARD + architecture: amd64 + driver_host_string: 2k12 + + updates: + product: Windows Server 2012 + names: + - Servicing Stack Update for Windows Server 2012 + - Security Monthly Quality Rollup for Windows Server 2012 + + virtualbox: + os_type: Windows2012_64 + + 2012r2: + box_tag: jborean93/WindowsServer2012R2 + + iso_src: 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 + iso_checksum: sha256:6612b5b1f53e845aacdf96e974bb119a3d9b4dcb5b82e65804ab7e534dc7b4d5 + iso_wim_label: Windows Server 2012 R2 SERVERSTANDARD + architecture: amd64 + driver_host_string: 2k12R2 + + updates: + product: Windows Server 2012 R2 + names: + - Servicing Stack Update for Windows Server 2012 R2 + - Security Monthly Quality Rollup for Windows Server 2012 R2 + + virtualbox: + os_type: Windows2012_64 + + '2016': + box_tag: jborean93/WindowsServer2016 + + iso_src: https://software-download.microsoft.com/download/pr/Windows_Server_2016_Datacenter_EVAL_en-us_14393_refresh.ISO + iso_checksum: sha256:1ce702a578a3cb1ac3d14873980838590f06d5b7101c5daaccbac9d73f1fb50f + iso_wim_label: Windows Server 2016 SERVERSTANDARD + architecture: amd64 + driver_host_string: 2k16 + + updates: + product: Windows Server 2016 + names: + - Servicing Stack Update for Windows Server 2016 + - Cumulative Update for Windows Server 2016 + + virtualbox: + os_type: Windows2016_64 + + '2019': + box_tag: jborean93/WindowsServer2019 + + iso_src: https://software-download.microsoft.com/download/pr/17763.737.190906-2324.rs5_release_svc_refresh_SERVER_EVAL_x64FRE_en-us_1.iso + iso_checksum: sha256:549bca46c055157291be6c22a3aaaed8330e78ef4382c99ee82c896426a1cee1 + iso_wim_label: Windows Server 2019 SERVERSTANDARD + architecture: amd64 + driver_host_string: 2k19 + + updates: + product: Windows Server 2019 + names: + - Servicing Stack Update for Windows Server 2019 + - Cumulative Update for Windows Server 2019 + + virtualbox: + os_type: Windows2019_64 + + '2022': + box_tag: jborean93/WindowsServer2022 + + iso_src: https://software-download.microsoft.com/download/sg/20348.1.210507-1500.fe_release_SERVER_EVAL_x64FRE_en-us.iso + iso_checksum: sha256:2ee3a0325f7230b1ff68bd8db2695f4102eae4ff32118382b1ab2e2b98a71aaa + iso_wim_label: Windows Server 2022 SERVERSTANDARD + architecture: amd64 + driver_host_string: 2k19 # FUTURE: update once virtio does + + updates: # FUTURE: This might have changed, look into once more updates are available + product: Windows Server 2022 + names: + - Servicing Stack Update for Windows Server 2022 + - Cumulative Update for Windows Server 2022 + + virtualbox: + os_type: Windows2019_64 # FUTURE: update once virtualbox does + + vars: + ansible_connection: local + ansible_python_interpreter: '{{ ansible_playbook_python }}' + + windows: + vars: + ansible_connection: psrp + ansible_user: '{{ setup_username }}' + ansible_password: '{{ setup_password }}' + ansible_psrp_protocol: http + ansible_psrp_auth: basic + ansible_psrp_message_encryption: never + ansible_become_method: runas + ansible_become_user: SYSTEM + + vars: + output_dir: '{{ playbook_dir }}/output' + platform: qemu + openssh_version: V8.6.0.0p1-Beta + virtio_version: 0.1.185-2 + setup_username: vagrant + setup_password: vagrant diff --git a/main.yml b/main.yml index 6c68b32..11cb905 100644 --- a/main.yml +++ b/main.yml @@ -1,27 +1,39 @@ ---- -- name: setup new Vagrant box with the latest updates and config - hosts: windows +- name: setup build files + hosts: setup gather_facts: no + vars: + force: False + headless: True pre_tasks: - - name: fail if mandatory vars are not set + - name: check that the Ansible version is at least 2.9 assert: that: - - ansible_become_method == "runas" - - ansible_become_user is defined - - man_provider is defined - - man_driver_host_string is defined - - man_host_type is defined - - man_host_architecture is defined - - man_is_longhorn is defined - - man_packer_windoze_version is defined - - man_skip_feature_removal is defined - - man_personalize_choco_packages is defined - - - name: make sure the WinRM service is set to auto - win_service: - name: winrm - start_mode: auto + - ansible_version.major >= 2 + - ansible_version.minor >= 9 + msg: packer-windoze requires Ansible 2.9 or newer to run, please upgrade or checkout devel before running + + - name: check that the platform is valid + assert: + that: + - platform in ['hyperv', 'qemu', 'virtualbox'] + msg: packer-windoze only supports the hyperv, qemu, and virtualbox provider + + - name: check that pypsrp is installed + command: python -c "import pypsrp" + changed_when: False + + roles: + - setup + +- name: setup windows host + hosts: windows + gather_facts: no + + pre_tasks: + - name: wait for Windows host WinRM to come online + wait_for_connection: + timeout: 14400 # The bootstrapping process can take some time roles: - update @@ -29,9 +41,29 @@ - role: jborean93.win_openssh opt_openssh_firewall_profiles: domain,private,public opt_openssh_skip_start: True - opt_openssh_version: v8.0.0.0p1-Beta - when: not man_is_longhorn + opt_openssh_version: '{{ openssh_version }}' + opt_openssh_powershell_subsystem: '{{ out_personalize_pwsh_path | default(omit) }}' - cleanup-winsxs - cleanup-features - cleanup - sysprep + + post_tasks: + - name: shutdown host for sysprep + raw: schtasks.exe /Run /TN "packer-shutdown" + + - name: wait for Windows host to go offline + wait_for: + host: '{{ ansible_host }}' + port: '{{ ansible_port }}' + state: stopped + delegate_to: localhost + +- name: package Vagrant box + hosts: setup + gather_facts: no + tags: + - box + + roles: + - box diff --git a/packer-setup.yml b/packer-setup.yml deleted file mode 100644 index 6bbca9a..0000000 --- a/packer-setup.yml +++ /dev/null @@ -1,30 +0,0 @@ ---- -- name: set up local dir with packer json and build files - hosts: localhost - gather_facts: no - vars: - opt_packer_setup_username: vagrant - opt_packer_setup_password: vagrant - opt_packer_setup_headless: true - opt_packer_setup_builder: virtualbox - - pre_tasks: - - name: check that the Ansible version is at least 2.7 - assert: - that: - - ansible_version.major >= 2 - - ansible_version.minor >= 7 - msg: packer-windoze requires Ansible 2.7 or newer to run, please upgrade or checkout devel before running - - - name: check that the provider is valid - assert: - that: - - opt_packer_setup_builder in ['hyperv', 'qemu', 'virtualbox'] - msg: packer-windoze only supports the hyperv, qemu, and virtualbox provider - - - name: check that pypsrp is installed - command: python -c "import pypsrp" - changed_when: False - - roles: - - packer-setup diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..f6272ba --- /dev/null +++ b/requirements.txt @@ -0,0 +1,3 @@ +beautifulsoup4 +httpx +pypsrp diff --git a/requirements.yml b/requirements.yml index 41b908a..ec20b0f 100644 --- a/requirements.yml +++ b/requirements.yml @@ -1,2 +1,14 @@ +roles: - src: jborean93.win_openssh - version: v0.2.0 + version: v0.3.0 + +collections: +- name: ansible.windows + version: '>=1.7.0' + +- name: chocolatey.chocolatey + +- name: community.general + version: '>=3.0.0' + +- name: community.windows diff --git a/roles/box/tasks/hyperv.yml b/roles/box/tasks/hyperv.yml new file mode 100644 index 0000000..611bd69 --- /dev/null +++ b/roles/box/tasks/hyperv.yml @@ -0,0 +1,52 @@ +- name: wait until VM is powered off + shell: | + while ((Get-VM -Name 'windoze-{{ inventory_hostname }}').State -ne 'Off') { + Start-Sleep -Seconds 1 + } + args: + executable: powershell.exe + changed_when: False + +- name: build box metadata + set_fact: + box_metadata: + provider: hyperv + +- name: get Windows path for the Hyper-V box path + command: wslpath -w {{ (output_dir ~ '/' ~ inventory_hostname ~ '/box') | quote }} + changed_when: False + register: hyperv_output_path + +- name: export VM + shell: | + $outPath = '{{ hyperv_output_path.stdout | trim }}' + $vmName = 'windoze-{{ inventory_hostname }}' + $exportPath = Join-Path $outPath $vmName + + $vm = Get-VM -Name $vmName + $vm | Get-VMDvdDrive | Remove-VMDvdDrive + $vm | Set-VMFirmware -BootOrder $vm.HardDrives[0] + $vm | Export-VM -Path $outPath + + Remove-Item -Path (Join-Path $exportPath 'Snapshots') -Force -Recurse + Move-Item -Path (Join-Path $exportPath '*') -Destination $outPath -Force + Remove-Item -Path $exportPath -Force -Recurse + register: box_img + args: + creates: '{{ output_dir }}/{{ inventory_hostname }}/box/Virtual Hard Disks' + executable: powershell.exe + +- name: remove VM + shell: | + $vm = Get-VM -Name 'windoze-{{ inventory_hostname }}' -ErrorAction SilentlyContinue + if ($vm) { + $vm | Remove-VM -Force + $true + } + else { + $false + } + register: vm_remove + changed_when: vm_remove.stdout | trim | bool + args: + executable: powershell.exe diff --git a/roles/box/tasks/main.yml b/roles/box/tasks/main.yml new file mode 100644 index 0000000..bda43bd --- /dev/null +++ b/roles/box/tasks/main.yml @@ -0,0 +1,46 @@ +- set_fact: + box_path: '{{ output_dir }}/{{ inventory_hostname }}/box' + +- name: create box folder + file: + path: '{{ box_path }}' + state: directory + +- name: template Vagrantfile + template: + src: Vagrantfile.tmpl + dest: '{{ box_path }}/Vagrantfile' + register: box_template + +- name: prepare provider specific box contents + include_tasks: '{{ platform }}.yml' + +- name: remove build artifacts + file: + path: '{{ output_dir }}/{{ inventory_hostname }}/{{ item }}' + state: absent + loop: + - hyperv + - qemu + - vbox + +- name: create metadata.json + copy: + content: '{{ box_metadata | to_json }}' + dest: '{{ box_path }}/metadata.json' + register: box_metadata + +- name: create box + shell: >- + tar + --create + --verbose + --sparse + ./* + | pigz --best -c > ../{{ platform }}.box + args: + chdir: '{{ box_path }}' + when: >- + box_template is changed or + box_img is changed or + box_metadata is changed diff --git a/roles/box/tasks/qemu.yml b/roles/box/tasks/qemu.yml new file mode 100644 index 0000000..bffcae3 --- /dev/null +++ b/roles/box/tasks/qemu.yml @@ -0,0 +1,33 @@ +- name: get VM PID + community.general.pids: + pattern: '{{ ("qemu-system-x86_64 -name windoze-" ~ inventory_hostname ~ " -machine") | regex_escape("posix_basic") }}*' + register: qemu_pid + +- name: wait for VM process to complete + wait_for: + path: /proc/{{ item }}/status + state: absent + loop: '{{ qemu_pid.pids }}' + +- name: compress VM image + command: >- + qemu-img convert + -c + -O qcow2 + {{ (output_dir ~ '/' ~ inventory_hostname ~ '/qemu/' ~ inventory_hostname ~ '-vm.qcow2') | quote }} + {{ (box_path ~ '/box.img') | quote }} + register: box_img + args: + creates: '{{ box_path }}/box.img' + +- name: get image details + command: qemu-img info --output=json {{ (box_path ~ '/box.img') | quote }} + register: qemu_img_info + changed_when: False + +- name: build box metadata + set_fact: + box_metadata: + format: qcow2 + provider: libvirt + virtual_size: '{{ (((qemu_img_info.stdout | trim | from_json)["virtual-size"] | int) / 1073741824) | int }}' diff --git a/roles/box/tasks/virtualbox.yml b/roles/box/tasks/virtualbox.yml new file mode 100644 index 0000000..43d24f8 --- /dev/null +++ b/roles/box/tasks/virtualbox.yml @@ -0,0 +1,55 @@ +- name: wait until VM is powered off + shell: | + until $(VBoxManage showvminfo --machinereadable {{ ('windoze-' ~ inventory_hostname) | quote }} | grep -q ^VMState=.poweroff.) + do + sleep 1 + done + changed_when: False + +- name: detach all dvd drives + command: >- + VBoxManage storageattach + {{ ('windoze-' ~ inventory_hostname) | quote }} + --storagectl SATA + --device 0 + --port {{ item }} + --medium none + register: detach_disk + changed_when: detach_disk.rc == 0 + failed_when: + - detach_disk.rc == 1 + - '"VBOX_E_OBJECT_NOT_FOUND" not in detach_disk.stderr' + loop: '{{ range(1, 5) | list }}' + +- name: change port count of storage controller + command: >- + VBoxManage storagectl + {{ ('windoze-' ~ inventory_hostname) | quote }} + --name SATA + --portcount 1 + +- name: export VM ovf + command: >- + VBoxManage export + {{ ('windoze-' ~ inventory_hostname) | quote }} + --output {{ (output_dir ~ '/' ~ inventory_hostname ~ '/vbox/box.ovf' )}} + args: + creates: '{{ output_dir }}/{{ inventory_hostname }}/vbox/box.ovf' + +- name: remove VM + command: VBoxManage unregistervm {{ ('windoze-' ~ inventory_hostname) | quote }} --delete + +- name: copy box files + copy: + src: '{{ output_dir }}/{{ inventory_hostname }}/vbox/{{ item }}' + dest: '{{ output_dir }}/{{ inventory_hostname }}/box/{{ item }}' + remote_src: + register: box_img + loop: + - box-disk001.vmdk + - box.ovf + +- name: build box metadata + set_fact: + box_metadata: + provider: virtualbox diff --git a/roles/packer-setup/templates/vagrantfile.template.j2 b/roles/box/templates/Vagrantfile.tmpl similarity index 54% rename from roles/packer-setup/templates/vagrantfile.template.j2 rename to roles/box/templates/Vagrantfile.tmpl index bc9dd62..95095d6 100644 --- a/roles/packer-setup/templates/vagrantfile.template.j2 +++ b/roles/box/templates/Vagrantfile.tmpl @@ -1,36 +1,44 @@ # -*- mode: ruby -*- # vi: set ft=ruby : Vagrant.configure(2) do |config| - config.vm.box = "{{opt_packer_setup_box_tag}}" + config.vm.box = "{{ box_tag }}" config.vm.guest = :windows config.vm.communicator = "winrm" - config.winrm.username = "{{opt_packer_setup_username}}" - config.winrm.password = "{{opt_packer_setup_password}}" + config.winrm.username = "{{ setup_username }}" + config.winrm.password = "{{ setup_password }}" config.vm.boot_timeout = 600 config.vm.network :forwarded_port, guest: 3389, host: 3389, id: 'rdp', auto_correct: true config.vm.network :forwarded_port, guest: 22, host: 2222, id: 'ssh', auto_correct: true -{% if opt_packer_setup_builder == 'qemu' %} - # Disable rsync with libvirt as it's not compatible with Windows +{% if platform in ['hyperv', 'qemu'] %} +{# Not compatible with libvirt/qemu and Hyper-V always prompts for creds #} config.vm.synced_folder ".", "/vagrant", disabled: true {% endif %} - config.vm.provider "virtualbox" do |vb| vb.cpus = 2 vb.default_nic_type = "virtio" vb.gui = false + vb.linked_clone = true vb.memory = 2048 end config.vm.provider "hyperv" do |h| h.cpus = 2 + h.linked_clone = true h.memory = 2048 end config.vm.provider "libvirt" do |l| + l.clock_offset = 'localtime' + l.clock_timer :name => 'hypervclock', :present => 'yes' l.cpus = 2 l.disk_bus = "virtio" + l.hyperv_feature :name => 'relaxed', :state => 'on' + l.hyperv_feature :name => 'spinlocks', :state => 'on', :retries => 8191 + l.hyperv_feature :name => 'vapic', :state => 'on' + l.input :type => "tablet", :bus => "usb" l.memory = 2048 l.nic_model_type = "virtio" + l.video_type = 'qxl' end end diff --git a/roles/cleanup-features/tasks/main.yml b/roles/cleanup-features/tasks/main.yml index fb9b43b..4ec0571 100644 --- a/roles/cleanup-features/tasks/main.yml +++ b/roles/cleanup-features/tasks/main.yml @@ -1,12 +1,5 @@ --- -- name: check if the Uninstall-WindowsFeature cmdlet is available - win_command: powershell.exe "Get-Command -Name Uninstall-WindowsFeature" - register: pri_cleanup_feature_uninstall_available - failed_when: False - +# Features on Demand won't reinstall on Server 2016+ so let's not do it for now - name: uninstall all unused Windows Features if function is available win_shell: Get-WindowsFeature | Where-Object { $_.InstallState -eq 'Available' } | Uninstall-WindowsFeature -Remove - when: - - pri_cleanup_feature_uninstall_available.rc == 0 - # Features on Demand won't reinstall on Server 2016+ so let's not do it for now - - not man_skip_feature_removal + when: inventory_hostname == 'win-2012r2' diff --git a/roles/cleanup-winsxs/tasks/cleanmgr.yml b/roles/cleanup-winsxs/tasks/cleanmgr.yml index eee1edf..6516c81 100644 --- a/roles/cleanup-winsxs/tasks/cleanmgr.yml +++ b/roles/cleanup-winsxs/tasks/cleanmgr.yml @@ -1,62 +1,18 @@ -# Server 2008, 2008 R2 and 2012 don't have cleanmgr setup by default, this +# Server 2012 doesn't have cleanmgr setup by default, this # needs to be copied from the relevant winsxs folder to it's intended location # before running. --- -- name: copy cleanmgr from winsxs Server 2008 32-bit - win_copy: - src: C:\Windows\WinSxS\x86_microsoft-windows-cleanmgr_31bf3856ad364e35_6.0.6001.18000_none_6d4436615d8bd133\cleanmgr.exe - dest: C:\Windows\System32\cleanmgr.exe - remote_src: True - when: man_host_type == "2008-x86" - -- name: copy cleanmgr mui from winsxs Server 2008 32-bit - win_copy: - src: C:\Windows\WinSxS\x86_microsoft-windows-cleanmgr.resources_31bf3856ad364e35_6.0.6001.18000_en-us_5dd66fed98a6c5bc\cleanmgr.exe.mui - dest: C:\Windows\System32\en-US\cleanmgr.exe.mui - remote_src: True - when: man_host_type == "2008-x86" - -- name: copy cleanmgr from winsxs Server 2008 64-bit - win_copy: - src: C:\Windows\WinSxS\amd64_microsoft-windows-cleanmgr_31bf3856ad364e35_6.0.6001.18000_none_c962d1e515e94269\cleanmgr.exe - dest: C:\Windows\System32\cleanmgr.exe - remote_src: True - when: man_host_type == "2008-x64" - -- name: copy cleanmgr mui from winsxs Server 2008 64-bit - win_copy: - src: C:\Windows\WinSxS\amd64_microsoft-windows-cleanmgr.resources_31bf3856ad364e35_6.0.6001.18000_en-us_b9f50b71510436f2\cleanmgr.exe.mui - dest: C:\Windows\System32\en-US\cleanmgr.exe.mui - remote_src: True - when: man_host_type == "2008-x64" - -- name: copy cleanmgr from winsxs Server 2008 R2 - win_copy: - src: C:\Windows\WinSxS\amd64_microsoft-windows-cleanmgr_31bf3856ad364e35_6.1.7600.16385_none_c9392808773cd7da\cleanmgr.exe - dest: C:\Windows\System32\cleanmgr.exe - remote_src: True - when: man_host_type == "2008r2" - -- name: copy cleanmgr mui from winsxs Server 2008 R2 - win_copy: - src: C:\Windows\WinSxS\amd64_microsoft-windows-cleanmgr.resources_31bf3856ad364e35_6.1.7600.16385_en-us_b9cb6194b257cc63\cleanmgr.exe.mui - dest: C:\Windows\System32\en-US\cleanmgr.exe.mui - remote_src: True - when: man_host_type == "2008r2" - -- name: copy cleanmgr from winsxs Server 2012 +- name: copy cleanmgr from winsxs win_copy: src: C:\Windows\WinSxS\amd64_microsoft-windows-cleanmgr_31bf3856ad364e35_6.2.9200.16384_none_c60dddc5e750072a\cleanmgr.exe dest: C:\Windows\System32\cleanmgr.exe remote_src: True - when: man_host_type == "2012" -- name: copy cleanmgr mui from winsxs Server 2012 +- name: copy cleanmgr mui from winsxs win_copy: src: C:\Windows\WinSxS\amd64_microsoft-windows-cleanmgr.resources_31bf3856ad364e35_6.2.9200.16384_en-us_b6a01752226afbb3\cleanmgr.exe.mui dest: C:\Windows\System32\en-US\cleanmgr.exe.mui remote_src: True - when: man_host_type == "2012" - name: run cleanmgr with everything set block: diff --git a/roles/cleanup-winsxs/tasks/main.yml b/roles/cleanup-winsxs/tasks/main.yml index ba91142..0e6a959 100644 --- a/roles/cleanup-winsxs/tasks/main.yml +++ b/roles/cleanup-winsxs/tasks/main.yml @@ -19,7 +19,7 @@ # Server 2016 fails on the first few runs, retry up to 5 times - name: run DISM with reset base on hosts that support it - win_command: DISM.exe /Online /Cleanup-Image /StartComponentCleanup /ResetBase + win_command: DISM.exe /Online /Cleanup-Image /StartComponentCleanup /ResetBase /Quiet register: pri_cleanup_winsxs_cleanup_result until: pri_cleanup_winsxs_cleanup_result.rc == 0 retries: 5 @@ -27,19 +27,8 @@ ignore_errors: yes when: pri_cleanup_winsxs_dism_supported.stdout_lines[0] == "true" -- name: run DISM to clear service packs when not Server 2008 - win_command: DISM.exe /Online /Cleanup-Image /SPSuperseded - when: - - man_host_type != "2008-x86" - - man_host_type != "2008-x64" - -- name: run compcln to clear service packs for Server 2008 - win_command: compcln.exe /quiet - register: pri_cleanup_winsxs_compcln_result - when: man_host_type == "2008-x86" or man_host_type == "2008-x64" - failed_when: - - pri_cleanup_winsxs_compcln_result.rc != 0 - - pri_cleanup_winsxs_compcln_result.rc != -2147467259 # means it has already been run +- name: run DISM to clear service packs + win_command: DISM.exe /Online /Cleanup-Image /SPSuperseded /Quiet - name: check if cleanmgr.exe is natively available win_stat: diff --git a/roles/cleanup/tasks/main.yml b/roles/cleanup/tasks/main.yml index 4d70908..e2403ee 100644 --- a/roles/cleanup/tasks/main.yml +++ b/roles/cleanup/tasks/main.yml @@ -26,12 +26,18 @@ win_shell: Remove-Item -Path C:\Recovery -Force -Recurse ignore_errors: yes +- name: check if ManifestCache exists + win_stat: + path: C:\Windows\WinSxs\ManifestCache + register: pri_cleanup_manifest_cache_stat + # we want to clear the folder contents and not the folder itself - name: clear out the WinSXS ManifestCache folder win_shell: | &cmd.exe /c Takeown /f %windir%\winsxs\ManifestCache\* &cmd.exe /c Icacls %windir%\winsxs\ManifestCache\* /GRANT administrators:F &cmd.exe /c Del /q %windir%\winsxs\ManifestCache\* + when: pri_cleanup_manifest_cache_stat.stat.exists - name: see if Optimize-Volume cmdlet is available win_command: powershell.exe "Get-Command -Name Optimize-Volume" diff --git a/roles/packer-setup/files/configure-hyperv-network.ps1 b/roles/packer-setup/files/configure-hyperv-network.ps1 deleted file mode 100644 index 9b65917..0000000 --- a/roles/packer-setup/files/configure-hyperv-network.ps1 +++ /dev/null @@ -1,10 +0,0 @@ -$ErrorActionPreference = "Stop" - -$inventory_file = "$PSScriptRoot\hosts.ini" -$ip_file = "$PSScriptRoot\hyper-v-ip.txt" - -$ip_address = Get-Content -Path $ip_file -Remove-Item -Path $ip_file > $null -$contents = Get-Content -Path $inventory_file -$contents = $contents -replace "ansible_host=.*$", "ansible_host=$ip_address" -Set-Content -Path $inventory_file -Value $contents diff --git a/roles/packer-setup/filter_plugins/parse_update.py b/roles/packer-setup/filter_plugins/parse_update.py deleted file mode 100644 index 042d2a8..0000000 --- a/roles/packer-setup/filter_plugins/parse_update.py +++ /dev/null @@ -1,38 +0,0 @@ -# Copyright: (c) 2019, Jordan Borean (@jborean93) -# MIT License (see LICENSE or https://opensource.org/licenses/MIT) - -from __future__ import (absolute_import, division, print_function) -__metaclass__ = type - -import re - - -class FilterModule: - - def filters(self): - return { - 'parse_update': self.parse_update, - } - - def parse_update(self, update, filename_pattern=None): - """ Converts a WindowsUpdate object in the windows_update lookup to a simple dict with the KB and URL.""" - kb_numbers = update.kb_numbers - download_urls = update.get_download_urls() - - if filename_pattern: - matched_urls = [] - for download_info in download_urls: - if re.match(filename_pattern, download_info.file_name): - matched_urls.append(download_info.url) - else: - matched_urls = [d.url for d in download_urls] - - if len(matched_urls) != 1: - raise ValueError("Expecting only 1 download link for '%s' but found %d" % (str(update), len(matched_urls))) - - update_info = { - 'title': update.title, - 'name': 'KB%s' % kb_numbers[0] if kb_numbers else update.id, - 'url': matched_urls[0], - } - return update_info diff --git a/roles/packer-setup/filter_plugins/update_dict.py b/roles/packer-setup/filter_plugins/update_dict.py deleted file mode 100644 index c7b9f40..0000000 --- a/roles/packer-setup/filter_plugins/update_dict.py +++ /dev/null @@ -1,22 +0,0 @@ -# Copyright: (c) 2019, Jordan Borean (@jborean93) -# MIT License (see LICENSE or https://opensource.org/licenses/MIT) - -from __future__ import (absolute_import, division, print_function) -__metaclass__ = type - - -class FilterModule: - - def filters(self): - return { - 'update_dict': self.update_dict, - 'merge_dict': self.merge_dict, - } - - def update_dict(self, old_dict, key, value): - old_dict[key] = value - return old_dict - - def merge_dict(self, old_dict, new_dict): - old_dict.update(new_dict) - return old_dict diff --git a/roles/packer-setup/lookup_plugins/windows_update.py b/roles/packer-setup/lookup_plugins/windows_update.py deleted file mode 100644 index d0b1955..0000000 --- a/roles/packer-setup/lookup_plugins/windows_update.py +++ /dev/null @@ -1,410 +0,0 @@ -# Copyright: (c) 2019, Jordan Borean (@jborean93) -# MIT License (see LICENSE or https://opensource.org/licenses/MIT) - -from __future__ import (absolute_import, division, print_function) -__metaclass__ = type - -DOCUMENTATION = r""" -lookup: windows_update -author: Jordan Borean (@jborean93) -short_description: Search for updates on the Microsoft Update Catalog. -description: -- Searches for updates on the Microsoft Update Catalog. -- The search terms are fairly rudimentary due to a limitation of the server side. -requirements: -- beautifulsoup4 -options: - _terms: - description: - - The search string to search for on the Microsoft update catalog. - required: True - type: str - all: - description: - - Whether to retrieve all updates available or just the first 25 returning. - - Setting to C(True) can result in a lot more mores to the update catalog itself and will take some time for large - results to be returned. - type: bool - default: False - architecture: - description: - - Filter the updates returned by architecture they are for. - - Typical values are C(amd64) and C(x86). - type: str - ascending: - description: - - Whether to sort in ascending order if I(sort) is set. - - Setting C(False) will sort the field specified by I(sort) in descending order. This results in another call - request to the Microsoft Update Catalog. - type: bool - default: True - product: - description: - - Filter the updates returned by the product they are for. - type: str - sort: - description: - - Sort the results by the header specified. - - Sorting by any field will result in an extra request to the Microsoft Update Catalog. - - Control the sort order with I(ascending). - type: str - choices: - - title - - products - - classification - - last_updated - - version - - size -""" - -import contextlib -import datetime -import json -import re -import traceback -import uuid - -from ansible.errors import AnsibleLookupError -from ansible.module_utils._text import to_bytes, to_native, to_text -from ansible.module_utils.basic import missing_required_lib -from ansible.module_utils.urls import open_url -from ansible.plugins.lookup import LookupBase -from ansible.module_utils.six.moves import urllib - -BS_IMP_ERR = None -try: - from bs4 import BeautifulSoup - HAS_BS = True -except ImportError: - BS_IMP_ERR = traceback.format_exc() - HAS_BS = False - - -CATALOG_URL = 'https://www.catalog.update.microsoft.com/' -DOWNLOAD_PATTERN = re.compile(r'\[(\d*)\]\.url = [\"\'](http[s]?://w{0,3}.?download\.windowsupdate\.com/[^\'\"]*)') -PRODUCT_SPLIT_PATTERN = re.compile(r',(?=[^\s])') - - -@contextlib.contextmanager -def urlopen(*args, **kwargs): - resp = open_url(*args, http_agent='packer-windoze/%s' % __name__, **kwargs) - try: - yield resp - finally: - resp.close() - - -class WUDownloadInfo: - - def __init__(self, download_id, url, raw): - """ - Contains information about an individual download link for an update. An update might have multiple download - links available and this keeps track of the metadata for each of them. - - :param download_id: The ID that relates to the download URL. - :param url: The download URL for this entry. - :param raw: The raw response text of the downloads page. - """ - self.url = url - self.digest = None - self.architectures = None - self.languages = None - self.long_languages = None - self.file_name = None - - attribute_map = { - 'digest': 'digest', - 'architectures': 'architectures', - 'languages': 'languages', - 'long_languages': 'longLanguages', - 'file_name': 'fileName', - } - for attrib_name, raw_name in attribute_map.items(): - regex_pattern = r"\[%s]\.%s = ['\"]([\w\-\.=+\/\(\) ]*)['\"];" % ( - re.escape(download_id), re.escape(raw_name)) - regex_match = re.search(regex_pattern, raw) - if regex_match: - setattr(self, attrib_name, regex_match.group(1)) - - def __str__(self): - return to_native("%s - %s" % (self.file_name or "unknown", self.long_languages or "unknown language")) - - -class WindowsUpdate: - - def __init__(self, raw_element): - """ - Stores information about a Windows Update entry. - - :param raw_element: The raw XHTML element that has been parsed by BeautifulSoup4. - """ - cells = raw_element.find_all('td') - - self.title = cells[1].get_text().strip() - - # Split , if there is no space ahead. - products = cells[2].get_text().strip() - self.products = list(filter(None, re.split(PRODUCT_SPLIT_PATTERN, products))) - - self.classification = cells[3].get_text().strip() - self.last_updated = datetime.datetime.strptime(cells[4].get_text().strip(), '%m/%d/%Y') - self.version = cells[5].get_text().strip() - self.size = int(cells[6].find_all('span')[1].get_text().strip()) - self.id = uuid.UUID(cells[7].find('input').attrs['id']) - self._details = None - self._architecture = None - self._description = None - self._download_urls = None - self._kb_numbers = None - self._more_information = None - self._msrc_number = None - self._msrc_severity = None - self._support_url = None - - @property - def architecture(self): - """ The architecture of the update. """ - if not self._architecture: - details = self._get_details() - raw_arch = details.find(id='ScopedViewHandler_labelArchitecture_Separator') - self._architecture = raw_arch.next_sibling.strip() - - return self._architecture - - @property - def description(self): - """ The description of the update. """ - if not self._description: - details = self._get_details() - self._description = details.find(id='ScopedViewHandler_desc').get_text() - - return self._description - - @property - def download_url(self): - """ The download URL of the update, will fail if the update contains multiple packages. """ - download_urls = self.get_download_urls() - - if len(download_urls) != 1: - raise ValueError("Expecting only 1 download link for '%s', received %d. Use get_download_urls() and " - "filter it based on your criteria." % (str(self), len(download_urls))) - - return download_urls[0].url - - @property - def kb_numbers(self): - """ A list of KB article numbers that apply to the update. """ - if self._kb_numbers is None: - details = self._get_details() - raw_kb = details.find(id='ScopedViewHandler_labelKBArticle_Separator') - - # If no KB's apply then the value will be n/a. Technically an update can have multiple KBs but I have - # not been able to find an example of this so cannot test that scenario. - self._kb_numbers = [int(n.strip()) for n in list(raw_kb.next_siblings) if n.strip().lower() != 'n/a'] - - return self._kb_numbers - - @property - def more_information(self): - """ Typically the URL of the KB article for the update but it can be anything. """ - if self._more_information is None: - details = self._get_details() - raw_info = details.find(id='ScopedViewHandler_labelMoreInfo_Separator') - self._more_information = list(raw_info.next_siblings)[1].get_text().strip() - - return self._more_information - - @property - def msrc_number(self): - """ The MSRC Number for the update, set to n/a if not defined. """ - if self._msrc_number is None: - details = self._get_details() - raw_info = details.find(id='ScopedViewHandler_labelSecurityBulliten_Separator') - self._msrc_number = list(raw_info.next_siblings)[0].strip() - - return self._msrc_number - - @property - def msrc_severity(self): - """ THe MSRC severity level for the update, set to Unspecified if not defined. """ - if self._msrc_severity is None: - details = self._get_details() - self._msrc_severity = details.find(id='ScopedViewHandler_msrcSeverity').get_text().strip() - - return self._msrc_severity - - @property - def support_url(self): - """ The support URL for the update. """ - if self._support_url is None: - details = self._get_details() - raw_info = details.find(id='ScopedViewHandler_labelSupportUrl_Separator') - self._support_url = list(raw_info.next_siblings)[1].get_text().strip() - - return self._support_url - - def get_download_urls(self): - """ - Get a list of WUDownloadInfo objects for the current update. These objects contain the download URL for all the - packages inside the update. - """ - if self._download_urls is None: - update_ids = json.dumps({ - 'size': 0, - 'updateID': str(self.id), - 'uidInfo': str(self.id), - }) - data = to_bytes(urllib.parse.urlencode({'updateIDs': '[%s]' % update_ids})) - - headers = { - 'Content-Type': 'application/x-www-form-urlencoded', - } - linkFound = False - while not linkFound: - with urlopen('%s/DownloadDialog.aspx' % CATALOG_URL, data=data, - headers=headers, timeout=1200) as resp: - resp_text = to_text(resp.read()).strip() - - link_matches = re.findall(DOWNLOAD_PATTERN, resp_text) - if len(link_matches) == 0: - display.v("Download link not found - read it again") - linkFound = False - else: - linkFound = True - - download_urls = [] - for download_id, url in link_matches: - download_urls.append(WUDownloadInfo(download_id, url, resp_text)) - - self._download_urls = download_urls - - return self._download_urls - - def _get_details(self): - if not self._details: - headers = { - 'Content-Type': 'application/x-www-form-urlencoded', - } - bodyOK = False - while not bodyOK: - with urlopen('%s/ScopedViewInline.aspx?updateid=%s' % (CATALOG_URL, str(self.id)), - headers=headers, timeout=1200) as resp: - resp_text = to_text(resp.read()).lstrip() - self._details = BeautifulSoup(resp_text, 'html.parser') - - body_class_list = self._details.body['class'] - if "error" in body_class_list: - display.vv("Page error - read it again") - bodyOK = False - else: - bodyOK = True - - return self._details - - def __str__(self): - return self.title - - -def find_updates(search, all_updates=False, sort=None, sort_reverse=False, data=None): - """ - Generator function that yields WindowsUpdate objects for each update found on the Microsoft Update catalog. - Yields a list of updates from the Microsoft Update catalog. These updates can then be downloaded locally using the - .download(path) function. - - :param search: The search string used when searching the update catalog. - :param all_updates: Set to True to continue to search on all pages and not just the first 25. This can dramatically - increase the runtime of the script so use with caution. - :param sort: The field name as seen in the update catalog GUI to sort by. Setting this will result in 1 more call - to the catalog URL. - :param sort_reverse: Reverse the sort after initially sorting it. Setting this will result in 1 more call after - the sort call to the catalog URL. - :param data: Data to post to the request, used when getting all pages - :return: Yields the WindowsUpdate objects found. - """ - search_safe = urllib.parse.quote(search) - headers = { - 'Content-Type': 'application/x-www-form-urlencoded', - } - if data: - data = to_bytes(urllib.parse.urlencode(data)) - - url = '%s/Search.aspx?q=%s' % (CATALOG_URL, search_safe) - with urlopen(url, data=data, headers=headers) as resp: - resp_text = to_text(resp.read()).lstrip() - - catalog = BeautifulSoup(resp_text, 'html.parser') - - # If we need to perform an action (like sorting or next page) we need to add these 4 fields that are based on the - # original response received. - def build_action_data(action): - data = { - '__EVENTTARGET': action, - } - for field in ['__EVENTARGUMENT', '__EVENTVALIDATION', '__VIEWSTATE', '__VIEWSTATEGENERATOR']: - element = catalog.find(id=field) - if element: - data[field] = element.attrs['value'] - - return data - - raw_updates = catalog.find(id='ctl00_catalogBody_updateMatches').find_all('tr') - headers = raw_updates[0] # The first entry in the table are the headers which we may use for sorting. - - if sort: - # Lookup the header click JS targets based on the header name to sort. - header_links = headers.find_all('a') - event_targets = dict((l.find('span').get_text(), l.attrs['id'].replace('_', '$')) for l in header_links) - data = build_action_data(event_targets[sort]) - - sort = sort if sort_reverse else None # If we want to sort descending we need to sort it again. - for update in find_updates(search, all_updates, sort=sort, data=data): - yield update - return - - for u in raw_updates[1:]: - yield WindowsUpdate(u) - - # ctl00_catalogBody_nextPage is set when there are no more updates to retrieve. - last_page = catalog.find(id='ctl00_catalogBody_nextPage') - if not last_page and all_updates: - data = build_action_data('ctl00$catalogBody$nextPageLinkText') - for update in find_updates(search, True, data=data): - yield update - - -class LookupModule(LookupBase): - - def run(self, terms, variables=None, **kwargs): - if not HAS_BS: - msg = missing_required_lib("beautifulsoup4", url="https://pypi.org/project/beautifulsoup4/") - msg += ". Import Error: %s" % BS_IMP_ERR - raise AnsibleLookupError(msg) - - self.set_options(var_options=variables, direct=kwargs) - all_updates = self.get_option('all') - architecture = self.get_option('architecture') - ascending = self.get_option('ascending') - product = self.get_option('product') - sort = self.get_option('sort') - - if sort: - # Map the lookup plugin's option title choices to the actual titles as returned in the XML. - sort = { - 'title': 'Title', - 'products': 'Products', - 'classification': 'Classification', - 'last_updated': 'Last Updated', - 'version': 'Version', - 'size': 'Size', - }[sort] - - ret = [] - for search in terms: - for update in find_updates(search, all_updates, sort=sort, sort_reverse=not ascending): - if product and product not in update.products: - continue - if architecture and architecture.lower() != update.architecture.lower(): - continue - ret.append(update) - - return ret diff --git a/roles/packer-setup/tasks/download_extract_iso.yml b/roles/packer-setup/tasks/download_extract_iso.yml deleted file mode 100644 index 8751a7f..0000000 --- a/roles/packer-setup/tasks/download_extract_iso.yml +++ /dev/null @@ -1,32 +0,0 @@ -# used to download and extract any evaluation ISO's that are a self extracting -# exe, Server 2008 64 bit is the only one currently that is in this form ---- -- name: check if unrar is installed - command: command -v unrar - ignore_errors: yes - changed_when: false - register: pri_packer_setup_unrar_result - -- name: fail if unrar is not installed - fail: - msg: The package 'unrar' is required to extract the ISO at {{ pri_packer_setup_config.iso_url }} - when: pri_packer_setup_unrar_result.rc != 0 - -- name: download evaluation ISO - get_url: - url: '{{ pri_packer_setup_config.iso_url }}' - dest: '{{ man_packer_setup_host_type }}/tmp-iso.exe' - -- name: get name of the ISO inside the rar package - command: unrar lb {{ man_packer_setup_host_type }}/tmp-iso.exe - changed_when: false - register: pri_packer_setup_unrar_file - -- name: extract ISO from self extracting exe file - command: unrar e {{ man_packer_setup_host_type }}/tmp-iso.exe {{ man_packer_setup_host_type }}/ - args: - creates: '{{ man_packer_setup_host_type }}/{{ pri_packer_setup_unrar_file.stdout }}' - -- name: override iso_url packer fact with newly extracted file - set_fact: - pri_packer_setup_config: '{{ pri_packer_setup_config|update_dict("iso_url", man_packer_setup_host_type + "/" + pri_packer_setup_unrar_file.stdout) }}' diff --git a/roles/packer-setup/tasks/main.yml b/roles/packer-setup/tasks/main.yml deleted file mode 100644 index 20a7dc2..0000000 --- a/roles/packer-setup/tasks/main.yml +++ /dev/null @@ -1,93 +0,0 @@ ---- -- name: check that the mandatory variables have been set - assert: - that: - - man_packer_setup_host_type is defined - -- name: ensure Galaxy requirements have been downloaded - command: ansible-galaxy install -r requirements.yml -p roles - args: - chdir: '{{ playbook_dir }}' - tags: - - skip_ansible_lint # should be idempotent but it's not the end of the world - -- name: check that the host type specified is valid - fail: - msg: man_packer_setup_host_type {{ man_packer_setup_host_typei }} is not a valid host type, expecting {{ pri_packer_setup_host_config.keys() }} - when: pri_packer_setup_host_config[man_packer_setup_host_type] is not defined - -- name: verify that a non supported Hyper-V configuration isn't set - fail: - msg: man_packer_setup_host_type {{ man_packer_setup_host_type }} is not supported with the Hyper-V builder - when: opt_packer_setup_builder == 'hyperv' and man_packer_setup_host_type in ["2008-x86", "2008-x64"] - -- name: create the packer build folder for the packer files - file: - path: '{{ man_packer_setup_host_type }}' - state: directory - -- name: setup Packer config and json file - include_tasks: packer_config.yml - -- name: create the secondary ISO file used in the bootstrapping process - include_tasks: secondary_iso.yml - -- name: download and extract ISO's that end with .exe - include_tasks: download_extract_iso.yml - when: pri_packer_setup_config.iso_url.endswith('.exe') and opt_packer_setup_iso_path is not defined - -- name: download Virtio ISO for VirtualBox build - get_url: - url: https://fedorapeople.org/groups/virt/virtio-win/direct-downloads/archive-virtio/virtio-win-0.1.171-1/virtio-win-0.1.171.iso - dest: '{{ man_packer_setup_host_type }}/virtio.iso' - when: opt_packer_setup_builder in ['virtualbox', 'qemu'] - -- name: create Hyper-V External Network switch - block: - - name: check if the swtch already exists - shell: if (Get-VMSwitch -Name "{{ pri_packer_setup_builders.switch_name }}" -ErrorAction SilentlyContinue) { $true } else { $false } - args: - executable: powershell.exe - changed_when: False - register: pri_packer_setup_hyper_switch_res - - - name: create Hyper-V switch - shell: $name = (Get-NetAdapter | ?{ $_.Name -like "Ethernet*" })[0].Name; New-VMSwitch -Name "{{ pri_packer_setup_builders.switch_name }}" -AllowManagementOS $true -NetAdapterName $name -Notes "Used by packer-windoze" - args: - executable: powershell.exe - when: not pri_packer_setup_hyper_switch_res.stdout_lines[0]|bool - when: opt_packer_setup_builder == 'hyperv' - -- name: template out the Ansible host file required during provisioning - template: - dest: '{{ man_packer_setup_host_type }}/hosts.ini' - src: hosts.ini.j2 - vars: - pri_packer_setup_ansible_port: '{{ (opt_packer_setup_builder == "virtualbox")|ternary(pri_packer_setup_config.vb_forwarded_port, 5985) }}' - -- name: template out the Vagrantfile template - template: - dest: '{{ man_packer_setup_host_type }}/vagrantfile.template' - src: vagrantfile.template.j2 - -- name: copy the Hyper-V get IP script - copy: - dest: '{{ man_packer_setup_host_type }}/configure-hyperv-network.ps1' - src: configure-hyperv-network.ps1 - when: opt_packer_setup_builder == 'hyperv' - -# this isn't strictly used in the Packer process but does test out the -# description templater when we don't want to upload the box -- name: template out the description for the box - template: - dest: '{{ man_packer_setup_host_type }}/description.md' - src: description.md.j2 - -- name: generate the main CHANGELOG.md - template: - dest: CHANGELOG.md - src: CHANGELOG.md.j2 - -- name: show user how to run packer build after this completes - debug: - msg: Now that the packer files have been set up, run "packer build -force {{ man_packer_setup_host_type }}/packer.json" to build the packer image diff --git a/roles/packer-setup/tasks/packer_config.yml b/roles/packer-setup/tasks/packer_config.yml deleted file mode 100644 index 26fe262..0000000 --- a/roles/packer-setup/tasks/packer_config.yml +++ /dev/null @@ -1,59 +0,0 @@ ---- -- name: set fact of packer host config based on host_type - set_fact: - pri_packer_setup_config: '{{ pri_packer_setup_host_config[man_packer_setup_host_type] }}' - -- name: override iso_url with opt_packer_setup_iso_path if defined - set_fact: - pri_packer_setup_config: '{{ pri_packer_setup_config|update_dict("iso_url", opt_packer_setup_iso_path) }}' - when: opt_packer_setup_iso_path is defined - -- name: override iso_wim_label with opt_packer_setup_iso_wim_label if defined - set_fact: - pri_packer_setup_config: '{{ pri_packer_setup_config|update_dict("iso_wim_label", opt_packer_setup_iso_wim_label) }}' - when: opt_packer_setup_iso_wim_label is defined - -- name: override product_key with opt_packer_setup_product_key if defined - set_fact: - pri_packer_setup_config: '{{ pri_packer_setup_config|update_dict("product_key", opt_packer_setup_product_key) }}' - when: opt_packer_setup_product_key is defined - -- name: assert that iso_url is set or overriden with opt_packer_setup_iso_path - assert: - that: - - pri_packer_setup_config.iso_url is defined - msg: opt_packer_setup_iso_path must be defined is the URL is not supplied in the default vars - -- name: create Packer builder info - set_fact: - pri_packer_setup_builders: '{{ pri_packer_setup_builders_info.common|merge_dict(pri_packer_setup_builders_info[opt_packer_setup_builder]) }}' - -- name: create Packer provisioner info - set_fact: - pri_packer_setup_provisioners: '{{ pri_packer_setup_provisioners_info.common + pri_packer_setup_provisioners_info[opt_packer_setup_builder] }}' - -- name: create Packer post-provisioners info - set_fact: - pri_packer_setup_post_processors: '{{ pri_packer_setup_post_processors_info.common|merge_dict(pri_packer_setup_post_processors_info[opt_packer_setup_builder]) }}' - -- name: set opt_packer_setup_box_tag if it isn't defined - set_fact: - opt_packer_setup_box_tag: '{{ pri_packer_setup_config.box_tag }}' - when: opt_packer_setup_box_tag is not defined - -- name: add vagrant-cloud post-processor if upload vars are set - set_fact: - pri_packer_setup_post_processors: - - '{{ pri_packer_setup_post_processors }}' - - type: vagrant-cloud - access_token: '{{ opt_packer_setup_access_token }}' - box_tag: '{{ opt_packer_setup_box_tag }}' - version: '{{ opt_packer_setup_version|default(pri_packer_setup_changelog[0].version) }}' - version_description: "{{ lookup('template', 'description.md.j2') }}" - when: - - opt_packer_setup_access_token is defined - -- name: create packer.json file based on vars set - copy: - content: '{{ pri_packer_setup_json|to_nice_json }}' - dest: '{{ man_packer_setup_host_type }}/packer.json' diff --git a/roles/packer-setup/tasks/secondary_iso.yml b/roles/packer-setup/tasks/secondary_iso.yml deleted file mode 100644 index 9641c78..0000000 --- a/roles/packer-setup/tasks/secondary_iso.yml +++ /dev/null @@ -1,59 +0,0 @@ -# Used to create the secondary ISO that contains the boostrapping script -# and files used in the bootstrapping process ---- -- name: create iso download folder - file: - path: '{{ man_packer_setup_host_type }}/iso' - state: directory - -- name: download latest Microsoft code verification root certificate for older hosts - get_url: - url: https://www.microsoft.com/pki/certs/MicrosoftCodeVerifRoot.crt - dest: '{{ man_packer_setup_host_type }}/iso/MicrosoftCodeVerifRoot.crt' - -# Due to how the hosts are setup the drive names differ per builder -- name: get a list of drive names for secondary.iso for each build type - set_fact: - pri_packer_setup_bootstrap_drives: - qemu: D - hyperv: E - virtualbox: F - -- name: set initial boostrap files fact - set_fact: - pri_packer_setup_bootstrap_files: [] - -# Process the bootstrap files here so the update lookups only run once. -- name: set fact of finalised bootstrap files to download/install - set_fact: - pri_packer_setup_bootstrap_files: '{{ pri_packer_setup_bootstrap_files + [ - item if item.type | default("") != "update" else - (lookup("windows_update", item.name, architecture=pri_packer_setup_config.architecture, product=item.product, - sort="last_updated", wantlist=True)[0] | - parse_update(filename_pattern=item.filename_pattern | default(None))) - ] }}' - with_items: '{{ pri_packer_setup_config.bootstrap_files }}' - -- name: template the required files - template: - dest: '{{ man_packer_setup_host_type }}/iso/{{ item }}' - src: '{{ item }}.j2' - vars: - pri_packer_setup_bootstrap_drive: '{{ pri_packer_setup_bootstrap_drives[opt_packer_setup_builder] }}' - register: pri_packer_setup_template_res - with_items: - - bootstrap.ps1 - - Autounattend.xml - -- name: download the required bootstrapping files - get_url: - dest: '{{ man_packer_setup_host_type }}/iso/{{ item.file|default() }}' - url: '{{ item.url }}' - force: no - checksum: '{{ item.checksum | default(omit) }}' - register: pri_packer_setup_bootstrap_download_res - with_items: '{{ pri_packer_setup_bootstrap_files }}' - -- name: create the secondary ISO file - command: mkisofs -joliet-long -lU -o {{ man_packer_setup_host_type }}/secondary.iso {{ man_packer_setup_host_type }}/iso - when: pri_packer_setup_template_res is changed or pri_packer_setup_bootstrap_download_res is changed diff --git a/roles/packer-setup/templates/bootstrap.ps1.j2 b/roles/packer-setup/templates/bootstrap.ps1.j2 deleted file mode 100644 index 678d0ff..0000000 --- a/roles/packer-setup/templates/bootstrap.ps1.j2 +++ /dev/null @@ -1,361 +0,0 @@ -$ErrorActionPreference = 'Stop' -$tmp_dir = $env:TEMP -$script_dir = Split-Path -Path $($script:MyInvocation.MyCommand.Path) -Parent - -trap { - $msg = "Unhandled exception`r`n$($_ | Out-String)" - Write-Log -message $msg -level "ERROR" - Write-Error -ErrorRecord $_ -ErrorAction Continue - Write-Host -NoNewLine "Press any key to continue..." - $null = $Host.UI.RawUI.ReadKey('NoEcho,IncludeKeyDown') -} - -Function Write-Log($message, $level="INFO") { - # Poor man's implementation of Log4Net - $date_stamp = Get-Date -Format s - $log_entry = "$date_stamp - $level - $message" - $log_file = "$tmp_dir\bootstrap.log" - Write-Host $log_entry - Add-Content -Path $log_file -Value $log_entry -} - -Function Reboot-AndResume($action) { - # need to reboot the server and rerun this script at the next action - $command = "$env:SystemDrive\Windows\System32\WindowsPowerShell\v1.0\powershell.exe $($script:MyInvocation.MyCommand.Path) '$action'" - $reg_key = "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\RunOnce" - $reg_property_name = "bootstrap" - Set-ItemProperty -Path $reg_key -Name $reg_property_name -Value $command - Write-Log -message "rebooting server and continuing bootstrap.ps1 with action '$action'" - if (Get-Command -Name Restart-Computer -ErrorAction SilentlyContinue) { - Restart-Computer -Force - Start-Sleep -Seconds 10 - } else { - # PS v1 (Server 2008) doesn't have the cmdlet Restart-Computer, use el-traditional - shutdown /r /t 0 - Start-Sleep -Seconds 10 - } -} - -Function Run-Process($executable, $arguments) { - $process = New-Object -TypeName System.Diagnostics.Process - $psi = $process.StartInfo - $psi.FileName = $executable - $psi.Arguments = $arguments - Write-Log -message "starting new process '$executable $arguments'" - $process.Start() | Out-Null - - $process.WaitForExit() | Out-Null - $exit_code = $process.ExitCode - Write-Log -message "process completed with exit code '$exit_code'" - - return $exit_code -} - -Function Extract-Zip($zip, $dest) { - Write-Log -message "extracting '$zip' to '$dest'" - try { - Add-Type -AssemblyName System.IO.Compression.FileSystem > $null - $legacy = $false - } catch { - $legacy = $true - } - - if ($legacy) { - try { - $shell = New-Object -ComObject Shell.Application - $zip_src = $shell.NameSpace($zip) - $zip_dest = $shell.NameSpace($dest) - $zip_dest.CopyHere($zip_src.Items(), 1044) - } catch { - Write-Log -message "failed to extract zip file: $($_.Exception.Message)" -level "ERROR" - throw $_ - } - } else { - try { - [System.IO.Compression.ZipFile]::ExtractToDirectory($zip, $dest) - } catch { - Write-Log -message "failed to extract zip file: $($_.Exception.Message)" -level "ERROR" - throw $_ - } - } -} - -Function Get-VirtIODriverPath($Name) { - # For PSv1 we just default to the standard drive, we don't actually install the drive on this version. - $drive = $env:SystemDrive + "\" - foreach ($file_system in (Get-PSDrive -PSProvider FileSystem)) { - if (Test-Path -LiteralPath "$($file_system.Root)$Name") { - $drive = $file_system.Root - break - } - } - - $host_key = "{{ pri_packer_setup_config.driver_host_string }}" - $architecture = $env:PROCESSOR_ARCHITECTURE - $inf_path = (Get-ChildItem -LiteralPath "$drive\$Name\$host_key\$architecture" -Filter "*.inf").FullName - return $inf_path -} - -Function Import-Certificate($cert, $store) { - $installed_thumbprints = (Get-ChildItem -LiteralPath "Cert:\LocalMachine\$store").Thumbprint - if ($null -eq $installed_thumbprints) { - # The 1st arg of IndexOf cannot be $null so this is a sanity check for that. - $installed_thumbprints = @() - } - - # Cannot use -in or -notin as PSv1 (Server 2008) will fail to parse the script - if (($null -ne $cert.Thumbprint) -and ([System.Array]::IndexOf($installed_thumbprints, $cert.Thumbprint) -eq -1)) { - Write-Log -message "Certificate $($cert.Thumbprint) not in $store store" - $store_name = [System.Security.Cryptography.X509Certificates.StoreName]"$store" - $store_location = [System.Security.Cryptography.X509Certificates.Storelocation]::LocalMachine - - $store = New-Object -TypeName System.Security.Cryptography.X509Certificates.X509Store -ArgumentList $store_name, $store_location - $store.Open([System.Security.Cryptography.X509Certificates.OpenFlags]::ReadWrite) - try { - $store.Add($cert) - } finally { - # Only .NET 4.6.2 has X509 as an IDisposable, use Close for backwards compatibility - $store.Close() - } - } -} - -$action = $args[0] -if (-not (Test-Path -Path $tmp_dir)) { - New-Item -Path $tmp_dir -ItemType Directory | Out-Null -} -Write-Log -message "starting bootstrap.ps1 with action '$action'" - -$bootstrap_actions = @( -{% for action in pri_packer_setup_bootstrap_files %} - @{ - name = "{{action.name}}" - action = "{{action.action|default("install")}}" - url = "{{action.url}}" - file = "{{action.file|default()}}" - zip_file_pattern = "{{action.zip_file_pattern|default()}}" - arguments = "{{action.arguments|default("/quiet /norestart")}}" - }, -{% endfor %} -{% if opt_packer_setup_builder in ['qemu', 'virtualbox'] %} - @{ - name = "Red Hat Virtio Network Driver" - path = (Get-VirtIODriverPath -Name NetKVM) - action = "driver" - }, -{% endif %} -{% if opt_packer_setup_builder == 'qemu' %} - @{ - name = "Red Hat Virtio SCSI driver" - path = (Get-VirtIODriverPath -Name vioscsi) - action = "driver" - }, - @{ - name = "Red Hat Virtio RNG driver" - path = (Get-VirtIODriverPath -Name viorng) - action = "driver" - }, - @{ - name = "Red Hat Virtio serial driver" - path = (Get-VirtIODriverPath -Name vioserial) - action = "driver" - }, - @{ - name = "Red Hat Virtio Memory Memory Balloon Driver" - path = (Get-VirtIODriverPath -Name Balloon) - action = "driver" - }, - @{ - name = "Red Hat Virtio pvpanic driver" - path = (Get-VirtIODriverPath -Name pvpanic) - action = "driver" - }, - {% if man_packer_setup_host_type not in ['2008-x86', '2008-x64'] %} -{# There is no relevant graphics driver for 2008 #} - @{ - name = "Red Hat Virtio Graphics Driver" - path = (Get-VirtIODriverPath -Name {{ (man_packer_setup_host_type == '2008r2') | ternary("qxl", "qxldod") }}) - action = "driver" - }, -{# There is no driver for vioinput for 2008 #} - @{ - name = "Red Hat Virtio VIOInput driver" - path = (Get-VirtIODriverPath -Name vioinput) - action = "driver" - }, -{# Server 2008 cannot install SHA256 signed drivers non-interactively #} - @{ - name = "Red Hat Virtio PCI serial" - path = (Get-VirtIODriverPath -Name qemupciserial) - action = "driver" - }, - {% if man_packer_setup_host_type not in ['2008r2', '2012', '2012r2'] %} -{# qemufwcfg only valid for 2016+ #} - @{ - name = "Red Hat Virtio Firmware Config Driver" - path = (Get-VirtIODriverPath -Name qemufwcfg) - action = "driver" - }, - {% endif %} - {% endif %} -{% endif %} - @{ - name = "Configure WinRM" - action = "winrm" - } -) - -$actions = @() -if ($action) { - $add_action = $false - foreach ($bootstrap_action in $bootstrap_actions) { - if ($bootstrap_action.name -eq $action) { - $add_action = $true - } - - if ($add_action) { - $actions += $bootstrap_action - } - } -} else { - $actions = $bootstrap_actions -} - -for ($i = 0; $i -lt $actions.Count; $i++) { - $current_action = $actions[$i] - $next_action = $null - if ($i -lt ($actions.Count - 1)) { - $next_action = $actions[$i + 1] - } - - switch($current_action.action) { - "install" { - Write-Log -message "Installing $($current_action.name)" - if ($current_action.file) { - $src = $current_action.file - } else { - $src = $current_action.url.Split("/")[-1] - } - $src = "$script_dir\$src" - $exit_code = Run-Process -executable $src -arguments $current_action.arguments - if ($exit_code -eq 3010) { - Reboot-AndResume -action $next_action.name - } elseif ($exit_code -ne 0) { - $error_message = "failed to install $($current_action.name): exit code $exit_code" - Write-Log -message $error_message -level "ERROR" - throw $error_message - } - } - "install-zip" { - Write-Log -message "Installing $($current_action.name)" - if ($current_action.file) { - $src = $current_action.file - } else { - $src = $current_action.url.Split("/")[-1] - } - $zip_src = "$script_dir\$src" - Extract-Zip -zip $zip_src -dest $tmp_dir - $install_file = Get-Item -Path "$tmp_dir\$($current_action.zip_file_pattern)" - if ($install_file -eq $null) { - $error_message = "unable to find extracted file of pattern $($current_action.zip_file_pattern) for installing $($current_action.name)" - Write-Log -message $error_message -level "ERROR" - throw $error_message - } - - $exit_code = Run-Process -executable $install_file -arguments $current_action.arguments - if ($exit_code -eq 3010) { - Reboot-AndResume -action $next_action.name - } elseif ($exit_code -ne 0) { - $error_message = "failed to install $($current_action.name): exit code $exit_code" - Write-Log -message $error_message -level "ERROR" - throw $error_message - } - } - "driver" { - Write-Log -message "Installing driver $($current_action.name)" - Add-Type -TypeDefinition @' -using System; -using System.Runtime.InteropServices; - -namespace PackerWindoze -{ - public class NativeMethods - { - [DllImport("Newdev.dll", SetLastError = true, CharSet = CharSet.Unicode)] - public static extern bool DiInstallDriverW( - IntPtr hwndParent, - string InfPath, - UInt32 Flags, - out bool NeedReboot); - } -} -'@ - - # Older hosts may not have the root Microsoft cert that has signed the VirtIO drivers installed. We - # manually install it so we can install the driver silently without user interaction. - $root_cert_path = "$script_dir\MicrosoftCodeVerifRoot.crt" - $root_cert = New-Object -TypeName System.Security.Cryptography.X509Certificates.X509Certificate2 -ArgumentList $root_cert_path - Import-Certificate -cert $root_cert -store "Root" - - $cat_path = Get-ChildItem -Path (Split-Path -Path $current_action.path -Parent) -Filter "*.cat" -File - $driver_cert = (Get-AuthenticodeSignature -LiteralPath $cat_path.FullName).SignerCertificate - Import-Certificate -cert $driver_cert -store "TrustedPublisher" - - $needs_reboot = $false - $res = [PackerWindoze.NativeMethods]::DiInstallDriverW([IntPtr]::Zero, $current_action.path, 0, [ref]$needs_reboot) - if (-not $res) { - $err = [System.Runtime.InteropServices.Marshal]::GetLastWin32Error() - try { - throw [System.ComponentModel.Win32Exception]$err - } catch [System.ComponentModel.Win32Exception] { - $error_msg = "failed to install driver $($current_action.name) - {0} (Win32 Error Code {1} - 0x{1:X8})" -f $_.Exception.Message, $err - } - Write-Log -message $error_message -level "ERROR" - throw $error_message - } - if ($needs_reboot) { - Reboot-AndResume -action $next_action.name - } - } - "winrm" { - Write-Log -message "configuring WinRM listener to work over 5985 with Basic auth" - &winrm.cmd quickconfig -q - Set-Item -Path WSMan:\localhost\Service\Auth\Basic -Value $true - Set-Item -Path WSMan:\localhost\Service\AllowUnencrypted -Value $true - $winrm_service = Get-Service -Name winrm - if ($winrm_service.Status -ne "Running") { - try { - Start-Service -Name winrm - } catch { - $error_message = "failed to start the WinRM service required by Ansible" - Write-Log -message $error_message -level "ERROR" - throw $error_message - } - } - - Write-Log -message "configuring PSRemoting endpoints" - Enable-PSRemoting -Force -SkipNetworkProfileCheck - - Write-Log -message "enabling RDP" - $rdp_wmi = Get-CimInstance -ClassName Win32_TerminalServiceSetting -Namespace root\CIMV2\TerminalServices - $rdp_enable = $rdp_wmi | Invoke-CimMethod -MethodName SetAllowTSConnections -Arguments @{ AllowTSConnections = 1; ModifyFirewallException = 1 } - if ($rdp_enable.ReturnValue -ne 0) { - $error_message = "failed to change RDP connection settings, error code: $($rdp_enable.ReturnValue)" - Write-Log -message $error_message -level "ERROR" - throw $error_message - } - - Write-Log -message "enabling NLA authentication for RDP" - $nla_wmi = Get-CimInstance -ClassName Win32_TSGeneralSetting -Namespace root\CIMV2\TerminalServices - $nla_wmi | Invoke-CimMethod -MethodName SetUserAuthenticationRequired -Arguments @{ UserAuthenticationRequired = 1 } | Out-Null - $nla_wmi = Get-CimInstance -ClassName Win32_TSGeneralSetting -Namespace root\CIMV2\TerminalServices - if ($nla_wmi.UserAuthenticationRequired -ne 1) { - $error_message = "failed to enable NLA" - Write-Log -message $error_message -level "ERROR" - throw $error_message - } - } - } -} - -Write-Log -message "bootstrap.ps1 complete" diff --git a/roles/packer-setup/templates/hosts.ini.j2 b/roles/packer-setup/templates/hosts.ini.j2 deleted file mode 100644 index 75548b6..0000000 --- a/roles/packer-setup/templates/hosts.ini.j2 +++ /dev/null @@ -1,22 +0,0 @@ -[windows] -packer-host ansible_host=127.0.0.1 - -[windows:vars] -ansible_user={{opt_packer_setup_username}} -ansible_password={{opt_packer_setup_password}} -ansible_port={{pri_packer_setup_ansible_port}} -ansible_connection=psrp -ansible_psrp_auth=basic -ansible_psrp_protocol=http -ansible_psrp_message_encryption=never -ansible_become_method=runas -ansible_become_user=SYSTEM - -man_provider={{opt_packer_setup_builder}} -man_driver_host_string={{pri_packer_setup_config.driver_host_string}} -man_host_type="{{man_packer_setup_host_type}}" -man_host_architecture={{pri_packer_setup_config.architecture}} -man_is_longhorn={{pri_packer_setup_config.answer_longhorn}} -man_packer_windoze_version={{opt_packer_setup_version|default(pri_packer_setup_changelog[0].version)}} -man_skip_feature_removal={{pri_packer_setup_config.skip_feature_removal|default(False)}} -man_personalize_choco_packages={{opt_packer_setup_packages|default('["vim", "sysinternals"]')}} diff --git a/roles/packer-setup/vars/main.yml b/roles/packer-setup/vars/main.yml deleted file mode 100644 index 0a6186a..0000000 --- a/roles/packer-setup/vars/main.yml +++ /dev/null @@ -1,542 +0,0 @@ -# this creates the changelog in the description for the Vagrant box -pri_packer_setup_changelog: -- version: '0.7.0' - date: 2019-12-20 - changes: - - Added `qemu/libvirt` boxes and default template to use VirtIO drivers for better performance - - Pin the VirtIO driver version to a specific version that can be manually updated across version. Currently at the latest stable version of `0.1.171`. - - Updated OpenSSH version to [v8.0.0.0p1-Beta](https://github.com/PowerShell/Win32-OpenSSH/releases/tag/v8.0.0.0p1-Beta) - - Raised minimum Ansible version to `2.7.0`. - - Swapped the connection plugin from `winrm` to `psrp` for faster builds. The [pypsrp](https://pypi.org/project/pypsrp/) Python library needs to be installed. - host_specific_changes: - 2008-x86: - - Enabled TLSv1.2 on the SChannel server now the patch is not faulty. - 2008-x64: - - Enabled TLSv1.2 on the SChannel server now the patch is not faulty. -- version: '0.6.0' - date: 2019-01-20 - changes: - - Fix logic when setting the `LocalAccountTokenFilterPolicy` value when setting up the WinRM listener - - Added ability to override the base Chocolatey packages that are installed with the image, use the `opt_package_setup_packages` variable with `-e` when generating the template to configure - - Moved away from custom role to install the Win32-OpenSSH components, now using the [jborean93.win_openssh](https://galaxy.ansible.com/jborean93/win_openssh) role - - Updated OpenSSH version [7.9.0.0p1-Beta](https://github.com/PowerShell/Win32-OpenSSH/releases/tag/v7.9.0.0p1-Beta) - - Installed the [virtio-network](https://stg.fedoraproject.org/wiki/Windows_Virtio_Drivers) driver on VirtualBox images - host_specific_changes: - 2016: - - Changed the default Windows Explorer window to show `This PC` instead of `Quick access` -- version: '0.5.0' - date: 2018-08-08 - changes: - - Disabled automatic Windows Update to eliminate post-startup thrash on older images - https://github.com/jborean93/packer-windoze/issues/10 - - Updated Win32-OpenSSH to the latest release [v7.7.2.0p1-Beta](https://github.com/PowerShell/Win32-OpenSSH/releases/tag/v7.7.2.0p1-Beta) - - Ensure WinRM HTTPS listener and firewall is configured before allowing Vagrant to detect the host is up - https://github.com/jborean93/packer-windoze/issues/11 - - Run ngen before sysprep process to try and speed up the Vagrant init time - - Clean up `C:\Windows\SoftwareDistribution\Download` and `C:\Recovery` as part of the cleanup process -- version: '0.4.0' - date: 2018-05-16 - changes: - - Create a PS Module called `PackerWindoze` that stores the `Reset-WinRMConfig` cmdlet that recreates the WinRM configuration and keep that post sysprep for downstream users to call at any time - - Added support for the Server 1803 image - - Install the full sysinternals suite instead of just PsTools, ProcMon, and ProcExp - - Fixed issue where the WinRM HTTPS firewall rule was not created after sysprep - - Fixed issue where WinRM still allowed unencrypted data after sysprep -- version: '0.3.0' - date: 2018-05-10 - changes: - - Updated OpenSSH version to [v7.6.1.0p1-Beta](https://github.com/PowerShell/Win32-OpenSSH/releases/tag/v7.6.1.0p1-Beta) - - Set the builtin `vagrant` account password to never expire - - Stop using the Ansible ConfigureRemotingForAnsible.ps1 script, swap over to custom script to support SHA256 and simplify steps - - Added Hyper-V builder support by specifying `-e opt_packer_setup_builder=hyperv` - This will only run on a Windows with WSL host and doesn't work for Server 2008 - host_specific_changes: - 2008-x64: - - Enabled TLSv1.2 client support, server is still disabled by default - 2008-x86: - - Enabled TLSv1.2 client support, server is still disabled by default -- version: '0.2.0' - date: 2017-12-01 - changes: - - Create a custom Vagrantfile template for the final image that includes the username and other required settings - - Moved sysprep process before the image is created - - Added `slmgr.vbs /rearm` to run just after Vagrant starts the image to get the full evaluation period possible - - Removed SSL certificates that were created during the packer build process - - Installed [Win32-OpenSSH](https://github.com/PowerShell/Win32-OpenSSH) v0.0.23.0 on all images eacept Server 2008 - - Added .travis-ci file to run [ansible-lint](https://github.com/willthames/ansible-lint) on the Ansible files for some testing sanity - - Decided to install the VirtualBox guest additions tools as part fo the build process - - Added vim to the list of chocolatey packages to help with Core OS installs or connecting via SSH - host_specific_changes: - 1709: - - Added support for Windows Server 1709 - - This won't be available in Vagrant Cloud as it is not avaible as a public evaluation ISO - 2016: - - Will not remove Features on Demand until [this](https://social.msdn.microsoft.com/Forums/en-US/2ad1c1d9-09ba-407e-ba03-951c6f2baa34/features-on-demand-server-2016-source-not-found?forum=ws2016) is resolved - 2008r2: - - Enabled TLSv1.2 cipher support for both the client and server components - 2008-x64: - - Disabled screensaver to stop auto logoff by default - - Ensure TLSv1.2 cipher support KB is installed but not enabled due to bug in the server implementation - 2008-x86: - - Disabled screensaver to stop auto logoff by default - - Ensure TLSv1.2 cipher support KB is installed but not enabled due to bug in the server implementation -- version: '0.0.1' - date: 2017-10-29 - changes: - - First images built by this process - -pri_packer_setup_builders_info: - common: - iso_url: '{{ pri_packer_setup_config.iso_url }}' - iso_checksum: '{{ pri_packer_setup_config.iso_checksum }}' - iso_checksum_type: md5 - communicator: winrm - disk_size: '{{ opt_packer_setup_disk_size_mib | default(40960) }}' - winrm_username: '{{ opt_packer_setup_username }}' - winrm_password: '{{ opt_packer_setup_password }}' - winrm_port: '5985' - winrm_timeout: '240m' - shutdown_command: schtasks.exe /Run /TN "packer-shutdown" - shutdown_timeout: 15m - virtualbox: - type: virtualbox-iso - guest_additions_mode: 'attach' - vboxmanage: - - - - modifyvm - # need to double escape the Jinja2 {{.Name}} value so Ansible doesn't see it as a variable - - "{% raw %}{{ '{{' }}.Name{{ '}}' }}{% endraw %}" - - --memory - - "2048" - - - - modifyvm - - "{% raw %}{{ '{{' }}.Name{{ '}}' }}{% endraw %}" - - --vram - - "48" - - - - modifyvm - - "{% raw %}{{ '{{' }}.Name{{ '}}' }}{% endraw %}" - - --cpus - - "2" - - - - modifyvm - - "{% raw %}{{ '{{' }}.Name{{ '}}' }}{% endraw %}" - - --natpf1 - - winrm,tcp,127.0.0.1,{{ pri_packer_setup_config.vb_forwarded_port }},,5985 - - - - storagectl - - "{% raw %}{{ '{{' }}.Name{{ '}}' }}{% endraw %}" - - --name - - Sata Controller - - --add - - sata - - --controller - - IntelAHCI - - - - storageattach - - "{% raw %}{{ '{{' }}.Name{{ '}}' }}{% endraw %}" - - --storagectl - - Sata Controller - - --port - - "1" - - --device - - "0" - - --type - - dvddrive - - --medium - - '{{ man_packer_setup_host_type }}/secondary.iso' - - - - storageattach - - "{% raw %}{{ '{{' }}.Name{{ '}}' }}{% endraw %}" - - --storagectl - - Sata Controller - - --port - - "2" - - --device - - "0" - - --type - - dvddrive - - --medium - - '{{ man_packer_setup_host_type }}/virtio.iso' - vboxmanage_post: - - - - storagectl - - "{% raw %}{{ '{{' }}.Name{{ '}}' }}{% endraw %}" - - --name - - Sata Controller - - --remove - guest_os_type: '{{ pri_packer_setup_config.vb_guest_os_type }}' - headless: '{{ opt_packer_setup_headless }}' - hyperv: - type: hyperv-iso - boot_command: - - aaaa - boot_wait: 2s - cpus: 2 - generation: '{{ pri_packer_setup_config.hv_generation|int }}' - memory: 2048 - secondary_iso_images: - - '{{ man_packer_setup_host_type }}/secondary.iso' - switch_name: '{{ opt_packer_setup_hyperv_switch|default("packer-windoze") }}' - headless: '{{ opt_packer_setup_headless }}' - qemu: - type: qemu - cpus: 2 - disk_interface: virtio - disk_compression: true - headless: '{{ opt_packer_setup_headless }}' - memory: 2048 - net_device: virtio-net - ssh_host_port_min: 5985 - ssh_host_port_max: 5985 - qemuargs: - - - - -netdev - - user,id=user.0,hostfwd=tcp::5985-:5985 - - - - -drive - - file={{ man_packer_setup_host_type }}/secondary.iso,index=0,media=cdrom - - - - -drive - - file={{ man_packer_setup_host_type }}/virtio.iso,index=1,media=cdrom - # This entry probably needs to be removed in a future release, I think it's a bug where the drive is removed if - # qemuargs is used and is fixed in an unreleased version right now. - - - - -drive - - file=output-qemu/packer-qemu,if=virtio,cache=writeback,discard=ignore,format=qcow2 - -pri_packer_setup_provisioners_info: - common: [] - virtualbox: - - type: shell-local - command: ansible-playbook main.yml -i {{ man_packer_setup_host_type }}/hosts.ini -vv - hyperv: - # get the IP address of the host and store it as a file - - type: powershell - inline: - - (Get-CimInstance -ClassName Win32_NetworkAdapterConfiguration -Filter "IPEnabled=True")[0].IPAddress[0] | Set-Content -Path C:\Windows\TEMP\ip.txt - # get the file that contains the IP address locally - - type: file - source: C:\Windows\TEMP\ip.txt - destination: '{{ man_packer_setup_host_type }}/hyper-v-ip.txt' - direction: download - # replace the IP in the Ansible inventory with the real IP - - type: shell-local - script: '{{ man_packer_setup_host_type }}\configure-hyperv-network.ps1' - execute_command: - - powershell.exe - - -ExecutionPolicy - - ByPass - - -File - - '{% raw %}{{ "{{" }}.Script{{ "}}" }}{% endraw %}' - - type: shell-local - # Packer creates a tmp script for command which won't work for bash.exe -ic, we set the actual command we want to - # use in execute_command and have a dummy value here - command: dummy - execute_command: - - bash.exe - - -ic - - ansible-playbook main.yml -i {{ man_packer_setup_host_type }}/hosts.ini -vv - qemu: - - type: shell-local - command: ansible-playbook main.yml -i {{ man_packer_setup_host_type }}/hosts.ini -vv - -pri_packer_setup_post_processors_info: - common: - type: vagrant - output: "{{ man_packer_setup_host_type }}/{{ opt_packer_setup_builder }}.box" - vagrantfile_template: "{{ man_packer_setup_host_type }}/vagrantfile.template" - virtualbox: {} - hyperv: {} - qemu: {} - -pri_packer_setup_json: - builders: - - '{{ pri_packer_setup_builders }}' - provisioners: '{{ pri_packer_setup_provisioners }}' - post-processors: - - '{{ pri_packer_setup_post_processors }}' - -# host settings like url's, checksums, vbox types, etc -# iso_url: The URL of the evaluation ISO -# iso_checksum: The md5 checksum of the evaluation ISO - -# VirtualBox specifics for the host -# vb_guest_os_type: The VirtualBox guest os type used when builing the VM, run 'VBoxManage list ostypes' to get a valid list -# vb_forwarded_port: The port to set on 127.0.0.1 that will forward to port 5986 on the Windows host, this should be unique - -# Hyper-V specific for the host -# hv_generation: The Hyper-V generation to use for the OS, either 1 or 2 - -# Host Information generic to the Ansible setup -# iso_wim_label: The WIM Name on the install ISO of the edition to install -# architecture: The architecture of the build (x86 or amd64) for the answer file -# answer_longhorn: Whether the host type will use the older Server 2008 answer file -# product_key: The KMS product key required in setup, only used in Server 2008 answer file -# box_tag: This is the box tag that I use, this can be overriden with opt_packer_setup_box_tag -# driver_host_string: The host string name used for the Virtio drivers -# bootstrap_files: A list of dictionaries that contains files to download for use in the bootstrapping process, this also modifies the bootstrapping script -pri_packer_setup_host_config: - # host pattern where architecture and option - # are optional and will default to x64 and minimal by default - '2008-x86': - box_tag: jborean93/WindowsServer2008-x86 - - iso_url: https://download.microsoft.com/download/D/D/B/DDB17DC1-A879-44DD-BD11-C0991D292AD7/6001.18000.080118-1840_x86fre_Server_en-us-KRMSFRE_EN_DVD.iso - iso_checksum: 89fbc4c7baafc0b0c05f0fa32c192a17 - vb_guest_os_type: Windows2008 - vb_forwarded_port: 55981 - hv_generation: - - iso_wim_label: Windows Longhorn SERVERSTANDARD - architecture: x86 - answer_longhorn: yes - product_key: TM24T-X9RMF-VWXK6-X8JC9-BFGM2 - driver_host_string: 2k8 - bootstrap_files: - - name: Server 2008 SP2 - url: https://download.microsoft.com/download/E/7/7/E77CBA41-0B6B-4398-BBBF-EE121EEC0535/Windows6.0-KB948465-X86.exe - - type: update - name: KB968930 # PowerShell v2.0 - product: Windows Server 2008 - - type: update - name: KB971512 # Pre-req 1 for IE9 - product: Windows Server 2008 - - type: update - name: KB2117917 # Pre-req 2 for IE9 - product: Windows Server 2008 - - name: Internet Explorer 9 - url: http://download.windowsupdate.com/msdownload/update/software/updt/2011/03/wu-ie9-windowsvista-x86_a2b66ff9e9affda9675dd85ba2b637a882979a25.exe - - name: .NET v4.5 - url: http://download.microsoft.com/download/B/A/4/BA4A7E71-2906-4B2D-A0E1-80CF16844F5F/dotNetFx45_Full_x86_x64.exe - arguments: /q /norestart - - name: PowerShell v3 - url: https://download.microsoft.com/download/E/7/6/E76850B8-DA6E-4FF5-8CCE-A24FC513FD16/Windows6.0-KB2506146-x86.msu - - name: WMFv3 Memory Hotfix - action: install-zip - zip_file_pattern: '*KB2842230*.msu' - file: KB2842230-wmfv3.zip - url: https://s3.amazonaws.com/ansible-ci-files/hotfixes/KB2842230/464091_intl_i386_zip.exe - - type: update - name: KB4493730 # April 9 2019 SSU Update (required for rollup updates) - product: Windows Server 2008 - - type: update - name: KB4474419 # SHA-2 update compat - product: Windows Server 2008 - - type: update - name: Servicing Stack Update for Windows Server 2008 - product: Windows Server 2008 - - type: update - name: Security Monthly Quality Rollup for Windows Server 2008 - product: Windows Server 2008 - '2008-x64': - box_tag: jborean93/WindowsServer2008-x64 - - iso_url: https://download.microsoft.com/download/D/D/B/DDB17DC1-A879-44DD-BD11-C0991D292AD7/6001.18000.080118-1840_amd64fre_Server_en-us-KRMSXFRE_EN_DVD.exe - iso_checksum: 0477c88678efb8ebc5cd7a9e9efd8b82 - vb_guest_os_type: Windows2008_64 - vb_forwarded_port: 55982 - hv_generation: - - iso_wim_label: Windows Longhorn SERVERSTANDARD - architecture: amd64 - answer_longhorn: yes - product_key: TM24T-X9RMF-VWXK6-X8JC9-BFGM2 - driver_host_string: 2k8 - bootstrap_files: - - name: Server 2008 SP2 - url: https://download.microsoft.com/download/4/7/3/473B909B-7B52-49FE-A443-2E2985D3DFC3/Windows6.0-KB948465-X64.exe - - type: update - name: KB968930 # PowerShell v2.0 - product: Windows Server 2008 - - type: update - name: KB971512 # Pre-req 1 for IE9 - product: Windows Server 2008 - - type: update - name: KB2117917 # Pre-req 2 for IE9 - product: Windows Server 2008 - - name: Internet Explorer 9 - url: https://download.microsoft.com/download/7/C/3/7C3BA535-1D8C-4A87-9F1D-163BBA971CA9/IE9-WIndowsVista-x64-enu.exe - - name: .NET v4.5 - url: http://download.microsoft.com/download/B/A/4/BA4A7E71-2906-4B2D-A0E1-80CF16844F5F/dotNetFx45_Full_x86_x64.exe - arguments: /q /norestart - - name: PowerShell v3 - url: https://download.microsoft.com/download/E/7/6/E76850B8-DA6E-4FF5-8CCE-A24FC513FD16/Windows6.0-KB2506146-x64.msu - - name: WMFv3 Memory Hotfix - action: install-zip - zip_file_pattern: '*KB2842230*.msu' - file: KB2842230-wmfv3.zip - url: https://s3.amazonaws.com/ansible-ci-files/hotfixes/KB2842230/464090_intl_x64_zip.exe - - type: update - name: KB4493730 # April 9 2019 SSU Update (required for rollup updates) - product: Windows Server 2008 - - type: update - name: KB4474419 # SHA-2 update compat - product: Windows Server 2008 - - type: update - name: Servicing Stack Update for Windows Server 2008 - product: Windows Server 2008 - - type: update - name: Security Monthly Quality Rollup for Windows Server 2008 - product: Windows Server 2008 - '2008r2': - box_tag: jborean93/WindowsServer2008R2 - - iso_url: https://download.microsoft.com/download/7/5/E/75EC4E54-5B02-42D6-8879-D8D3A25FBEF7/7601.17514.101119-1850_x64fre_server_eval_en-us-GRMSXEVAL_EN_DVD.iso - iso_checksum: 4263be2cf3c59177c45085c0a7bc6ca5 - vb_guest_os_type: Windows2008_64 - vb_forwarded_port: 55983 - hv_generation: 1 - - iso_wim_label: Windows Server 2008 R2 SERVERSTANDARD - architecture: amd64 - answer_longhorn: no - driver_host_string: 2k8R2 - bootstrap_files: - - name: .NET v4.5 - url: http://download.microsoft.com/download/B/A/4/BA4A7E71-2906-4B2D-A0E1-80CF16844F5F/dotNetFx45_Full_x86_x64.exe - arguments: /q /norestart - - name: PowerShell v3 - url: https://download.microsoft.com/download/E/7/6/E76850B8-DA6E-4FF5-8CCE-A24FC513FD16/Windows6.1-KB2506143-x64.msu - - name: WMFv3 Memory Hotfix - action: install-zip - zip_file_pattern: '*KB2842230*.msu' - file: KB2842230-wmfv3.zip - url: https://s3.amazonaws.com/ansible-ci-files/hotfixes/KB2842230/463984_intl_x64_zip.exe - - type: update - name: KB4490628 # March 12 2019 SSU update (required for rollup updates) - product: Windows Server 2008 R2 - - type: update - name: KB4474419 # SHA-2 update compat - product: Windows Server 2008 R2 - - type: update - name: Servicing Stack Update for Windows Server 2008 R2 - product: Windows Server 2008 R2 - - type: update - name: Security Monthly Quality Rollup for Windows Server 2008 R2 - product: Windows Server 2008 R2 - # Hotfix to automatically trust SHA2 signed certs if their root has been imported. The above update allows loading - # of these drivers but this hotfix is required to install them non-interactively. MS has also stopped their - # distribution of hotfixes so we need to rely on a 3rd party host. - - name: SHA256 Certificate Driver Hotfix - url: http://thehotfixshare.net/board/index.php?autocom=downloads&req=download&code=confirm_download&id=18882 - file: Windows6.4-KB2921916-x64.msu - # Add a checksum to make sure the right file is downloaded - checksum: sha256:39d978285d01ee4c0dfe9e2462bc4c948260aaf041aaa04faef3275f6d46a773 - '2012': - box_tag: jborean93/WindowsServer2012 - - iso_url: https://download.microsoft.com/download/6/D/A/6DAB58BA-F939-451D-9101-7DE07DC09C03/9200.16384.WIN8_RTM.120725-1247_X64FRE_SERVER_EVAL_EN-US-HRM_SSS_X64FREE_EN-US_DV5.ISO - iso_checksum: 8503997171f731d9bd1cb0b0edc31f3d - vb_guest_os_type: Windows2012_64 - vb_forwarded_port: 55984 - hv_generation: 2 - - iso_wim_label: Windows Server 2012 SERVERSTANDARD - architecture: amd64 - answer_longhorn: no - driver_host_string: 2k12 - bootstrap_files: - - name: WMFv3 Memory Hotfix - action: install-zip - zip_file_pattern: '*KB2842230*.msu' - file: KB2842230-wmfv3.zip - url: https://s3.amazonaws.com/ansible-ci-files/hotfixes/KB2842230/463941_intl_x64_zip.exe - - type: update - name: Servicing Stack Update for Windows Server 2012 - product: Windows Server 2012 - - type: update - name: Security Monthly Quality Rollup for Windows Server 2012 - product: Windows Server 2012 - '2012r2': - box_tag: jborean93/WindowsServer2012R2 - - iso_url: https://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 - iso_checksum: 5b5e08c490ad16b59b1d9fab0def883a - vb_guest_os_type: Windows2012_64 - vb_forwarded_port: 55985 - hv_generation: 2 - - iso_wim_label: Windows Server 2012 R2 SERVERSTANDARD - architecture: amd64 - answer_longhorn: no - driver_host_string: 2k12R2 - bootstrap_files: - - type: update - name: Servicing Stack Update for Windows Server 2012 R2 - product: Windows Server 2012 R2 - - type: update - name: Security Monthly Quality Rollup for Windows Server 2012 R2 - product: Windows Server 2012 R2 - '2016': - box_tag: jborean93/WindowsServer2016 - - iso_url: https://software-download.microsoft.com/download/pr/Windows_Server_2016_Datacenter_EVAL_en-us_14393_refresh.ISO - iso_checksum: 70721288bbcdfe3239d8f8c0fae55f1f - vb_guest_os_type: Windows2016_64 - vb_forwarded_port: 55986 - hv_generation: 2 - - iso_wim_label: Windows Server 2016 SERVERSTANDARD - architecture: amd64 - answer_longhorn: no - skip_feature_removal: yes - driver_host_string: 2k16 - # We manually install the latest servicing stack and cumulative update as installing them through Windows updates - # takes too long. The windows_update lookup plugin automatically retrieves the latest update for the product - # identified. - bootstrap_files: - - type: update - name: Servicing Stack Update for Windows Server 2016 - product: Windows Server 2016 - - type: update - name: Cumulative Update for Windows Server 2016 - product: Windows Server 2016 - '1709': - box_tag: WindowsServer1709 - - # no evaluation ISO is available, end user must supply the path to a local ISO file - iso_checksum: 7c73ce30c3975652262f794fc35127b5 - vb_guest_os_type: Windows2016_64 - vb_forwarded_port: 55987 - hv_generation: 2 - - iso_wim_label: Windows Server 2016 SERVERSTANDARDACORE - architecture: amd64 - answer_longhorn: no - skip_feature_removal: yes - driver_host_string: 2k16 - bootstrap_files: [] - '1803': - box_tag: WindowsServer1803 - - # no evaluation ISO is available, end user must supply the path to a local ISO file - iso_checksum: e34b375e0b9438d72e6305f36b125406 - vb_guest_os_type: Windows2016_64 - vb_forwarded_port: 55988 - hv_generation: 2 - - iso_wim_label: Windows Server 2016 SERVERSTANDARDACORE - architecture: amd64 - answer_longhorn: no - skip_feature_removal: yes - driver_host_string: 2k16 - bootstrap_files: [] - '2019': - box_tag: jborean93/WindowsServer2019 - - iso_url: https://software-download.microsoft.com/download/sg/17763.253.190108-0006.rs5_release_svc_refresh_SERVER_EVAL_x64FRE_en-us.iso - iso_checksum: 48cd91270581d1be10c3ff3ad6c41cce - vb_guest_os_type: Windows2019_64 - vb_forwarded_port: 55989 - hv_generation: 2 - - iso_wim_label: Windows Server 2019 SERVERSTANDARD - architecture: amd64 - answer_longhorn: no - skip_feature_removal: yes - driver_host_string: 2k19 - bootstrap_files: - - type: update - name: Servicing Stack Update for Windows Server 2019 - product: Windows Server 2019 - - type: update - name: Cumulative Update for Windows Server 2019 - product: Windows Server 2019 diff --git a/roles/personalise/tasks/main.yml b/roles/personalise/tasks/main.yml index ba5579d..44ef656 100644 --- a/roles/personalise/tasks/main.yml +++ b/roles/personalise/tasks/main.yml @@ -1,9 +1,10 @@ --- -- name: install and enable TLSv1.2 for Server 2008 and 2008 R2 +- name: install and enable TLSv1.3 for Server 2022 + when: inventory_hostname == 'win-2022' block: - - name: enable TLSv1.2 support for Server 2008 and 2008 R2 + - name: enable TLSv1.3 support for Server 2022 win_regedit: - path: HKLM:\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Protocols\TLS 1.2\{{ item.type }} + path: HKLM:\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Protocols\TLS 1.3\{{ item.type }} name: '{{ item.property }}' data: '{{ item.value }}' type: dword @@ -26,7 +27,17 @@ - name: reboot if TLS config was applied win_reboot: when: pri_personalise_tls_config is changed - when: man_host_type == '2008-x86' or man_host_type == '2008-x64' or man_host_type == '2008r2' + +- name: disable shutdown tracker + win_regedit: + path: HKLM:\Software\Policies\Microsoft\Windows NT\Reliability + name: '{{ item }}' + data: 0 + type: dword + state: present + loop: + - ShutdownReasonOn + - ShutdownReasonUI - name: set show hidden files/folders and file extensions for the default user profile win_regedit: @@ -42,22 +53,6 @@ - name: HideFileExt data: 0 -- name: disable screensaver for Server 2008 for the default user profile - win_regedit: - path: HKLM:\ANSIBLE\Control Panel\Desktop - name: '{{ item.name }}' - data: '{{ item.value|default(omit) }}' - type: string - state: '{{ item.state }}' - hive: C:\Users\Default\NTUSER.dat - when: man_host_type == '2008-x86' or man_host_type == '2008-x64' - loop: - - name: ScreenSaveActive - state: present - value: 0 - - name: SCRNSAVE.EXE - state: absent - - name: set This PC as the default view for Windows Explorer for the default user profile win_regedit: path: HKLM:\ANSIBLE\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\Advanced @@ -67,11 +62,8 @@ state: present hive: C:\Users\Default\NTUSER.dat when: # this is only valid for 2016/10+ - - man_host_type != '2008-x86' - - man_host_type != '2008-x64' - - man_host_type != '2008r2' - - man_host_type != '2012' - - man_host_type != '2012r2' + - inventory_hostname != 'win-2012' + - inventory_hostname != 'win-2012r2' - name: set show hidden files/folders and file extensions for the current user profile win_regedit: @@ -86,21 +78,6 @@ - name: HideFileExt data: 0 -- name: disable screensaver for Server 2008 for the current user profile - win_regedit: - path: HKCU:\Control Panel\Desktop - name: '{{ item.name }}' - data: '{{ item.value|default(omit) }}' - type: string - state: '{{ item.state }}' - when: man_host_type == '2008-x86' or man_host_type == '2008-x64' - loop: - - name: ScreenSaveActive - state: present - value: 0 - - name: SCRNSAVE.EXE - state: absent - - name: set This PC as the default view for Windows Explorer for the current user profile win_regedit: path: HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\Advanced @@ -109,11 +86,8 @@ type: dword state: present when: - - man_host_type != '2008-x86' - - man_host_type != '2008-x64' - - man_host_type != '2008r2' - - man_host_type != '2012' - - man_host_type != '2012r2' + - inventory_hostname != 'win-2012' + - inventory_hostname != 'win-2012r2' - name: disable automatic updates (prevents TrustedInstaller startup thrash on older images) win_regedit: @@ -125,21 +99,49 @@ - name: install set Chocolatey packages win_chocolatey: - name: '{{ man_personalize_choco_packages }}' + name: '{{ choco_packages }}' ignore_checksums: yes state: present + register: choco_package_install when: - - man_personalize_choco_packages|count > 0 - - man_personalize_choco_packages[0] != '' + - choco_packages | count > 0 + - choco_packages[0] != '' become: yes vars: # run with become as we aren't sure what packages the user has specified ansible_become_user: '{{ ansible_user }}' ansible_become_pass: '{{ ansible_password }}' +- name: configure pwsh + when: '"pwsh" in choco_packages' + block: + # It seems like PATH isn't updated straight await. Might need a bounce of the WinRM service + # but that's harder to achieve so just reboot the host to be safe + - name: reboot host to update PATH env + win_reboot: + when: choco_package_install is changed + + - name: configure PSRemoting for pwsh + jborean93.windoze.psremoting: + + - name: get pwsh path + win_shell: | + $pwsh = Get-Command -Name pwsh.exe -CommandType Application -ErrorAction SilentlyContinue + if ($pwsh) { + (New-Object -ComObject Scripting.FileSystemObject).GetFile($pwsh.Path).ShortPath + } + register: pri_personalize_pwsh_path + retries: 5 # Found intermittent errors here, just try a few times - might need to replace the COM side. + delay: 5 + until: pri_personalize_pwsh_path is successful + + - name: set output fact for OpenSSH to use for pwsh subsystem path + set_fact: + out_personalize_pwsh_path: '{{ pri_personalize_pwsh_path.stdout | trim }}' + - name: install the VirtualBox Guest Additions include_tasks: virtualbox.yml - when: man_provider == 'virtualbox' + when: platform == 'virtualbox' - name: install the QEMU Guest Additions include_tasks: qemu.yml - when: man_provider == 'qemu' + when: platform == 'qemu' diff --git a/roles/personalise/tasks/qemu.yml b/roles/personalise/tasks/qemu.yml index 474fb38..74237f1 100644 --- a/roles/personalise/tasks/qemu.yml +++ b/roles/personalise/tasks/qemu.yml @@ -1,7 +1,15 @@ ---- +- name: get the root drive where the VirtIO disk is mounted + win_shell: (Get-PSDrive | Where-Object { Test-Path -Path "$($_.Name):\guest-agent" }).Name + register: pri_personalise_ga_root_res + changed_when: no + +- name: get rid of module output cruft + set_fact: + pri_personalise_ga_root: '{{ pri_personalise_ga_root_res.stdout_lines[0] }}' + - name: install QEMU Guest Additions from virtio disk win_package: - path: E:\guest-agent\qemu-ga-{{ (man_host_architecture == 'x86') | ternary('i386', 'x86_64') }}.msi + path: '{{ pri_personalise_ga_root }}:\guest-agent\qemu-ga-{{ (architecture == "x86") | ternary("i386", "x86_64") }}.msi' state: present register: pri_personalise_ga_res @@ -16,7 +24,7 @@ - name: copy the Balloon memory service executable to the host win_copy: - src: E:\Balloon\{{ man_driver_host_string }}\{{ man_host_architecture }}\blnsvr.exe + src: '{{ pri_personalise_ga_root }}:\Balloon\{{ driver_host_string }}\{{ architecture }}\blnsvr.exe' dest: C:\Program Files\QEMU_Balloon\blnsvr.exe remote_src: yes @@ -31,7 +39,7 @@ # https://bugzilla.redhat.com/show_bug.cgi?id=1377155 - name: fix up ACPI0010 unknown device settings for Server 2016 - when: man_host_type == '2016' + when: inventory_hostname == 'win-2016' block: - name: get keys under DriverPackages win_reg_stat: diff --git a/roles/personalise/tasks/virtualbox.yml b/roles/personalise/tasks/virtualbox.yml index e8a7607..24d480a 100644 --- a/roles/personalise/tasks/virtualbox.yml +++ b/roles/personalise/tasks/virtualbox.yml @@ -20,7 +20,7 @@ ignore_errors: yes register: pri_personalise_vbox_guest_install -- name: attempt to recover from failed install (Server 2008 needs to add 1 more cert to the trusted store) +- name: attempt to recover from failed install block: - name: install VBoxGuest.cat signer certificate win_shell: | @@ -38,5 +38,5 @@ win_command: pnputil.exe -a "C:\Program Files\Oracle\VirtualBox Guest Additions\VBoxGuest.inf" - name: install VirtualBox Guest Additions again - win_command: '{ {pri_personalise_vbox_guest_root }}:\VBoxWindowsAdditions.exe /force /with_autologon /with_d3d /with_wddm /S' + win_command: '{{ pri_personalise_vbox_guest_root }}:\VBoxWindowsAdditions.exe /force /with_autologon /with_d3d /with_wddm /S' when: pri_personalise_vbox_guest_install.rc != 0 diff --git a/roles/setup/tasks/hyperv.yml b/roles/setup/tasks/hyperv.yml new file mode 100644 index 0000000..0d18f9d --- /dev/null +++ b/roles/setup/tasks/hyperv.yml @@ -0,0 +1,59 @@ +- name: check if VM is registered + shell: | + if (Get-VM -Name 'windoze-{{ inventory_hostname }}' -ErrorAction SilentlyContinue) { $true } else { $false } + args: + executable: powershell.exe + register: hyperv_vm_info + changed_when: False + +- name: stop and remove existing VM + when: + - force + - hyperv_vm_info.stdout | trim | bool + block: + - name: stop existing VM + shell: Stop-VM -Name 'windoze-{{ inventory_hostname }}' -TurnOff + args: + executable: powershell.exe + + - name: remove existing VM + shell: Remove-VM -Name 'windoze-{{ inventory_hostname }}' -Force + args: + executable: powershell.exe + +- name: remove existing VM artifacts + file: + path: '{{ output_dir }}/{{ inventory_hostname }}/{{ item }}' + state: absent + when: force or not (hyperv_vm_info.stdout | trim | bool) + loop: + - hyperv + - box + - hyperv.box + +- name: create hyperv temp dir + file: + path: '{{ output_dir }}/{{ inventory_hostname }}/hyperv' + state: directory + +- name: template build script + template: + src: setup-hyperv.ps1.tmpl + dest: '{{ output_dir }}/{{ inventory_hostname }}/hyperv/setup-hyperv.ps1' + +- name: get Windows path for the Hyper-V script + command: wslpath -w {{ (output_dir ~ '/' ~ inventory_hostname ~ '/hyperv/setup-hyperv.ps1') | quote }} + changed_when: False + register: hyperv_win_path + +- name: create Hyper-V VM + command: >- + powershell.exe -NoProfile -NoLogo -ExecutionPolicy Unrestricted + -File {{ hyperv_win_path.stdout | trim | quote }} + register: hyperv_create_info + changed_when: (hyperv_create_info.stdout | trim | from_json).changed + +- name: override guest hostname and port + set_fact: + guest_host: '{{ hyperv_ip }}' + guest_port: 5985 diff --git a/roles/setup/tasks/main.yml b/roles/setup/tasks/main.yml new file mode 100644 index 0000000..ee17045 --- /dev/null +++ b/roles/setup/tasks/main.yml @@ -0,0 +1,190 @@ +# Hyper-V needs to set a static IP on the interface as part of the bootstrap script. These steps determine the +# current network settings and what IP/Gateway to set the VM as. +- name: get WSL2 IP settings + set_fact: + hyperv_current_ip: '{{ lookup("jborean93.windoze.ip_info") }}' + when: platform == 'hyperv' + +- name: get Hyper-V IP settings + set_fact: + hyperv_ip: >- + {{ + hyperv_current_ip.ip | jborean93.windoze.ip_addr( + [hyperv_current_ip.gateway], 0 + groups["setup"].index(inventory_hostname) + ) + }} + hyperv_ip_prefix: '{{ hyperv_current_ip.prefixlen }}' + hyperv_gateway: '{{ hyperv_current_ip.gateway }}' + when: platform == 'hyperv' + +- name: process host specific overrides + set_fact: + iso_src: '{{ hostvars[inventory_hostname]["iso_src_" ~ inventory_hostname] | default(iso_src) }}' + iso_checksum: >- + {{ + (hostvars[inventory_hostname]["iso_src_" ~ inventory_hostname] is defined) | ternary( + hostvars[inventory_hostname]["iso_checksum_" ~ inventory_hostname] | default(""), + iso_checksum + ) + }} + iso_wim_label: '{{ hostvars[inventory_hostname]["iso_wim_label_" ~ inventory_hostname] | default(iso_wim_label) }}' + +- name: load changelog settings + include_vars: + file: '{{ playbook_dir }}/.changelog.yml' + +- name: generate the main CHANGELOG.md + template: + src: CHANGELOG.md.tmpl + dest: '{{ playbook_dir }}/CHANGELOG.md' + run_once: True + +- name: set default Chocolatey packages to install + set_fact: + default_choco_packages: + - vim + - sysinternals + +- name: add pwsh to default packages if not running 2012 + set_fact: + default_choco_packages: '{{ default_choco_packages + ["pwsh"] }}' + when: inventory_hostname != '2012' + +- name: create common build directory + file: + path: '{{ output_dir }}/common' + state: directory + run_once: True + +- name: download latest Microsoft code verification root certificate for older hosts + get_url: + url: https://www.microsoft.com/pki/certs/MicrosoftCodeVerifRoot.crt + dest: '{{ output_dir }}/common/MicrosoftCodeVerifRoot.crt' + register: verif_root_cert + run_once: True + +- name: download Virtio ISO + get_url: + url: https://fedorapeople.org/groups/virt/virtio-win/direct-downloads/archive-virtio/virtio-win-{{ virtio_version }}/virtio-win.iso + dest: '{{ output_dir }}/common/virtio-win.iso' + when: platform != 'hyperv' + run_once: True + +- name: create host specific build directory + file: + path: '{{ output_dir }}/{{ inventory_hostname }}/iso' + state: directory + +- name: template out the description for the box + template: + src: description.md.tmpl + dest: '{{ output_dir }}/{{ inventory_hostname }}/description.md' + +- name: download WMFv3 hotfix for Server 2012 + get_url: + url: https://s3.amazonaws.com/ansible-ci-files/hotfixes/KB2842230/463941_intl_x64_zip.exe + dest: '{{ output_dir }}/{{ inventory_hostname }}/iso/KB2842230-wmfv3.zip' + register: wmf3_hotfix + when: inventory_hostname == '2012' + +- name: get the latest Windows updates + jborean93.windoze.win_update_info: + name: '{{ updates.names }}' + product: '{{ updates.product }}' + architecture: '{{ architecture }}' + sort: last_updated + ignore_terms: # Sometimes .NET and Preview updates appear in our search terms, we don't want them in the initial bootstrapping + - \.NET + - Preview + register: update_information + until: update_information is successful + retries: 5 + delay: 0 + +- name: download the latest updates found + get_url: + url: '{{ item[0].url }}' + dest: '{{ output_dir }}/{{ inventory_hostname }}/iso/{{ item[0].kb }}.msu' + register: downloaded_updates + when: item | length > 0 + loop: '{{ update_information.updates }}' + loop_control: + label: '{{ "skipped" if item | length == 0 else item[0].title }}' + +- name: build update list for bootstrap script + set_fact: + update_files: >- + {{ + update_files | default([]) + [ + { + "title": item[0].title, + "filename": item[0].kb ~ '.msu', + } + ] + }} + when: item | length > 0 + loop: '{{ update_information.updates }}' + loop_control: + label: '{{ "skipped" if item | length == 0 else item[0].title }}' + +- name: create Autounattend.xml file + template: + src: '{{ item }}.tmpl' + dest: '{{ output_dir }}/{{ inventory_hostname }}/iso/{{ item }}' + newline_sequence: \r\n + register: iso_template_files + loop: + - Autounattend.xml + - bootstrap.ps1 + +- name: set secondary iso path fact + set_fact: + secondary_iso_src: '{{ output_dir }}/{{ inventory_hostname }}/{{ inventory_hostname }}-secondary.iso' + +- name: create secondary iso + command: >- + mkisofs -RJ -l + -input-charset iso8859-1 + -quiet + -o {{ secondary_iso_src | quote }} + {{ (output_dir ~ '/' ~ inventory_hostname ~ '/iso') | quote }} + {{ (output_dir ~ '/common/MicrosoftCodeVerifRoot.crt') | quote }} + when: >- + verif_root_cert is changed or + (inventory_hostname == '2012' and wmf3_hotfix is changed) or + downloaded_updates is changed or + iso_template_files is changed + +- block: + - name: download Windows ISO + get_url: + url: '{{ iso_src }}' + dest: '{{ output_dir }}/{{ inventory_hostname }}/{{ inventory_hostname }}-windows.iso' + checksum: '{{ iso_checksum | default(omit, True) }}' + + - set_fact: + iso_src: '{{ output_dir }}/{{ inventory_hostname }}/{{ inventory_hostname }}-windows.iso' + + when: iso_src.startswith('http') or iso_src.startswith('https') + +- name: determine port to forward onto VM guest + set_fact: + guest_host: 127.0.0.1 + guest_port: '{{ 29500 + (groups["setup"].index(inventory_hostname) * 2) }}' + +- name: start build VM + include_tasks: '{{ platform }}.yml' + +- name: add Windows host to host list + add_host: + name: 'win-{{ inventory_hostname }}' + ansible_host: '{{ guest_host }}' + ansible_port: '{{ guest_port }}' + windoze_version: '{{ changelog[0].version }}' + platform: '{{ platform }}' + architecture: '{{ architecture }}' + driver_host_string: '{{ driver_host_string }}' + choco_packages: '{{ choco_packages | default(default_choco_packages) }}' + groups: + - windows + changed_when: False diff --git a/roles/setup/tasks/qemu.yml b/roles/setup/tasks/qemu.yml new file mode 100644 index 0000000..6a204f1 --- /dev/null +++ b/roles/setup/tasks/qemu.yml @@ -0,0 +1,58 @@ +- name: check if VM is already running + community.general.pids: + pattern: '{{ ("qemu-system-x86_64 -name windoze-" ~ inventory_hostname ~ " -machine") | regex_escape("posix_basic") }}*' + register: qemu_pid + +- name: kill existing VM if force is specified + command: kill {{ item }} + loop: '{{ qemu_pid.pids }}' + when: force + +- name: start VM if not already running + when: force or qemu_pid.pids | length == 0 + block: + - name: ensure older artifacts are removed + file: + path: '{{ output_dir }}/{{ inventory_hostname }}/{{ item }}' + state: absent + loop: + - qemu + - box + - qemu.box + + - name: create QEMU build dir + file: + path: '{{ output_dir }}/{{ inventory_hostname }}/qemu' + state: directory + + - name: create QEMU hdd image + command: >- + qemu-img create + -f qcow2 + -o preallocation=metadata + {{ (output_dir ~ '/' ~ inventory_hostname ~ '/qemu/' ~ inventory_hostname ~ '-vm.qcow2') | quote }} + 40960M + + - name: start up QEMU VM + command: >- + qemu-system-x86_64 + -name windoze-{{ inventory_hostname }} + -machine type=pc,accel=kvm + -cpu host,hv_relaxed,hv_spinlocks=0x1fff,hv_vapic,hv_time + -smp cpus=2,sockets=2 + -m 2048M + -vga qxl + -display {{ headless | ternary('none', 'gtk') }} + -spice port={{ (guest_port | int) + 1 }},addr=127.0.0.1,disable-ticketing + -device virtio-serial-pci + -device virtserialport,chardev=spicechannel{{ inventory_hostname }}0,name=com.redhat.spice.0 + -chardev spicevmc,id=spicechannel{{ inventory_hostname }}0,name=vdagent + -usb -device usb-tablet + -netdev user,id=user0,hostfwd=tcp::{{ guest_port }}-:5985 + -device virtio-net,netdev=user0 + -drive file={{ iso_src | quote }},index=0,media=cdrom + -drive file={{ secondary_iso_src | quote }},index=1,media=cdrom + -drive file={{ (output_dir ~ '/common/virtio-win.iso') | quote }},index=2,media=cdrom + -drive file={{ (output_dir ~ '/' ~ inventory_hostname ~ '/qemu/' ~ inventory_hostname ~ '-vm.qcow2') | quote }},if=virtio,cache=writeback,discard=ignore,format=qcow2 + -boot once=d + -daemonize diff --git a/roles/setup/tasks/virtualbox.yml b/roles/setup/tasks/virtualbox.yml new file mode 100644 index 0000000..bb6cdec --- /dev/null +++ b/roles/setup/tasks/virtualbox.yml @@ -0,0 +1,92 @@ +- name: get VirtualBox version + command: VBoxManage --version + changed_when: False + register: vbox_version + +- set_fact: + vbox_version: '{{ (vbox_version.stdout | trim | regex_search("^((\d)+\.(\d)+.(\d)+)", "\1"))[0] }}' + +- name: download VirtualBox guest additions ISO + get_url: + url: https://download.virtualbox.org/virtualbox/{{ vbox_version }}/VBoxGuestAdditions_{{ vbox_version }}.iso + dest: '{{ output_dir }}/common/VBoxGuestAdditions.iso' + run_once: True + +- name: check if VM is registered + command: VBoxManage showvminfo windoze-{{ inventory_hostname }} + register: vbox_vm_info + changed_when: False + failed_when: + - vbox_vm_info.rc != 0 + - '"VBOX_E_OBJECT_NOT_FOUND" not in vbox_vm_info.stderr' + +- name: register and run vm + when: force or vbox_vm_info.rc == 1 + block: + - name: stop existing VM + command: VBoxManage controlvm {{ ('windoze-' ~ inventory_hostname) | quote }} poweroff + when: '"running" in vbox_vm_info.stdout' + + - name: remove existing VM + command: VBoxManage unregistervm {{ ('windoze-' ~ inventory_hostname) | quote }} --delete + when: vbox_vm_info.rc == 0 + + - name: remove existing VM artifacts + file: + path: '{{ output_dir }}/{{ inventory_hostname }}/{{ item }}' + state: absent + loop: + - vbox + - box + - vagrant.box + + - name: register VM hdd + shell: >- + VBoxManage createmedium disk + --filename {{ (output_dir ~ '/' ~ inventory_hostname ~ '/vbox/' ~ inventory_hostname ~ '-vm.vdi') | quote }} + --size 40960 + --format VDI + --variant Standard + + - name: template VirtualBox VM config + template: + src: VirtualBox.vbox.tmpl + dest: '{{ output_dir }}/{{ inventory_hostname }}/vbox/vm.vbox' + vars: + vbox_uuid: '{{ inventory_hostname | to_uuid }}' + vbox_name: windoze-{{ inventory_hostname }} + + - name: register VM + command: VBoxManage registervm {{ (output_dir ~ '/' ~ inventory_hostname ~ '/vbox/vm.vbox') | quote }} + + - name: randomize MAC address + command: VBoxManage modifyvm {{ ('windoze-' ~ inventory_hostname) | quote }} --macaddress1 auto + + - name: attach drives + command: >- + VBoxManage storageattach + windoze-{{ inventory_hostname }} + --storagectl SATA + --device 0 + --port {{ idx }} + --type {{ item.type }} + --medium {{ item.path | quote }} + loop: + - path: '{{ output_dir }}/{{ inventory_hostname }}/vbox/{{ inventory_hostname }}-vm.vdi' + type: hdd + - path: '{{ iso_src }}' + type: dvddrive + - path: '{{ secondary_iso_src }}' + type: dvddrive + - path: '{{ output_dir }}/common/virtio-win.iso' + type: dvddrive + - path: '{{ output_dir }}/common/VBoxGuestAdditions.iso' + type: dvddrive + loop_control: + index_var: idx + + - name: start VM + command: >- + VBoxManage startvm + {{ ('windoze-' ~ inventory_hostname) | quote }} + --type {{ headless | ternary('headless', 'separate') }} diff --git a/roles/packer-setup/templates/Autounattend.xml.j2 b/roles/setup/templates/Autounattend.xml.tmpl similarity index 65% rename from roles/packer-setup/templates/Autounattend.xml.j2 rename to roles/setup/templates/Autounattend.xml.tmpl index 906ef11..841198a 100644 --- a/roles/packer-setup/templates/Autounattend.xml.j2 +++ b/roles/setup/templates/Autounattend.xml.tmpl @@ -1,26 +1,18 @@ -{% if pri_packer_setup_config.answer_longhorn == True %} - - - - - - -{% endif %} -{% if opt_packer_setup_builder == 'qemu' %} +{% if platform == 'qemu' %} - + - E:\viostor\{{pri_packer_setup_config.driver_host_string}}\{{pri_packer_setup_config.architecture}} + F:\viostor\{{ driver_host_string }}\{{ architecture }} {% endif %} - + en-US @@ -31,28 +23,10 @@ en-US - + -{% if pri_packer_setup_config.answer_longhorn == True %} - - - 1 - Primary - true - - - - - true - NTFS - - C - 1 - 1 - - -{% elif opt_packer_setup_builder == 'hyperv' and pri_packer_setup_config.hv_generation == 2 %} +{% if platform == 'hyperv' %} 1 @@ -95,7 +69,7 @@ NTFS - + 4 4 @@ -123,7 +97,7 @@ NTFS - + C 2 2 @@ -139,14 +113,12 @@ /IMAGE/NAME - {{pri_packer_setup_config.iso_wim_label}} + {{ iso_wim_label }} 0 -{% if pri_packer_setup_config.answer_longhorn == True %} - 1 -{% elif opt_packer_setup_builder == 'hyperv' and pri_packer_setup_config.hv_generation == 2 %} +{% if platform == 'hyperv' %} 4 {% else %} 2 @@ -156,9 +128,6 @@ -{% if pri_packer_setup_config.product_key is defined %} - {{pri_packer_setup_config.product_key}} -{% endif %} OnError true @@ -168,110 +137,88 @@ -{% if pri_packer_setup_config.answer_longhorn == False %} - + 1 -{% endif %} - + false -{% if pri_packer_setup_config.answer_longhorn == False %} false -{% endif %} - + en-US en-US en-US en-US - + true Home 1 -{% if pri_packer_setup_config.answer_longhorn == False %} true -{% endif %} -{% if man_packer_setup_host_type not in ("2008-x86", "2008-x64", "2008r2") %} true true true -{% endif %} UTC -{% if pri_packer_setup_config.answer_longhorn == True %} - false -{% endif %} Administrators - {{opt_packer_setup_username}} - {{opt_packer_setup_username}} - {{opt_packer_setup_username}} + {{ setup_username }} + {{ setup_username }} + {{ setup_username }} - {{opt_packer_setup_password}} + {{ setup_password }} true</PlainText> </Password> </LocalAccount> </LocalAccounts> <AdministratorPassword> - <Value>{{opt_packer_setup_password}}</Value> + <Value>{{ setup_password }}</Value> <PlainText>true</PlainText> </AdministratorPassword> </UserAccounts> <AutoLogon> <Enabled>true</Enabled> - <Username>{{opt_packer_setup_username}}</Username> + <Username>{{ setup_username }}</Username> <Password> - <Value>{{opt_packer_setup_password}}</Value> + <Value>{{ setup_password }}</Value> <PlainText>true</PlainText> </Password> </AutoLogon> <FirstLogonCommands> -{% if pri_packer_setup_config.answer_longhorn == True %} - <SynchronousCommand wcm:action="add"> - <CommandLine>C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe -Command "Set-ExecutionPolicy -ExecutionPolicy Unrestricted"</CommandLine> - <Order>1</Order> - </SynchronousCommand> - <SynchronousCommand wcm:action="add"> - <CommandLine>C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe {{pri_packer_setup_bootstrap_drive}}:\bootstrap.ps1</CommandLine> - <Order>2</Order> - </SynchronousCommand> -{% else %} <SynchronousCommand wcm:action="add"> <CommandLine>C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe -Command "Set-ExecutionPolicy -ExecutionPolicy Unrestricted -Force"</CommandLine> <Order>1</Order> <RequiresUserInput>true</RequiresUserInput> </SynchronousCommand> <SynchronousCommand wcm:action="add"> - <CommandLine>C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe -File {{pri_packer_setup_bootstrap_drive}}:\bootstrap.ps1</CommandLine> + <CommandLine>C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe -File E:\bootstrap.ps1</CommandLine> <Order>2</Order> <RequiresUserInput>true</RequiresUserInput> </SynchronousCommand> -{% endif %} </FirstLogonCommands> </component> </settings> <settings pass="specialize"> <!-- https://docs.microsoft.com/en-us/windows-hardware/customize/desktop/unattend/microsoft-windows-servermanager-svrmgrnc --> - <component xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" name="Microsoft-Windows-ServerManager-SvrMgrNc" processorArchitecture="{{pri_packer_setup_config.architecture}}" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS"> + <component xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" name="Microsoft-Windows-ServerManager-SvrMgrNc" processorArchitecture="{{ architecture }}" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS"> <DoNotOpenServerManagerAtLogon>true</DoNotOpenServerManagerAtLogon> </component> <!-- https://docs.microsoft.com/en-us/windows-hardware/customize/desktop/unattend/microsoft-windows-ie-esc --> - <component xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" name="Microsoft-Windows-IE-ESC" processorArchitecture="{{pri_packer_setup_config.architecture}}" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS"> + <component xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" name="Microsoft-Windows-IE-ESC" processorArchitecture="{{ architecture }}" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS"> <IEHardenAdmin>false</IEHardenAdmin> <IEHardenUser>false</IEHardenUser> </component> <!-- https://docs.microsoft.com/en-us/windows-hardware/customize/desktop/unattend/microsoft-windows-outofboxexperience --> - <component xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" name="Microsoft-Windows-OutOfBoxExperience" processorArchitecture="{{pri_packer_setup_config.architecture}}" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS"> + <component xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" name="Microsoft-Windows-OutOfBoxExperience" processorArchitecture="{{ architecture }}" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS"> <DoNotOpenInitialConfigurationTasksAtLogon>true</DoNotOpenInitialConfigurationTasksAtLogon> </component> </settings> diff --git a/roles/packer-setup/templates/CHANGELOG.md.j2 b/roles/setup/templates/CHANGELOG.md.tmpl similarity index 78% rename from roles/packer-setup/templates/CHANGELOG.md.j2 rename to roles/setup/templates/CHANGELOG.md.tmpl index f4f1fe3..f3cb192 100644 --- a/roles/packer-setup/templates/CHANGELOG.md.j2 +++ b/roles/setup/templates/CHANGELOG.md.tmpl @@ -6,17 +6,17 @@ changelog entries to `roles/packer-setup/vars/main.yml` to modify this file_ This is the changelog of each image version uploaded to the Vagrant Cloud. It contains a list of changes that each incorporate. -{% for version_entry in pri_packer_setup_changelog %} -### v{{version_entry.version}} - {{version_entry.date|default("TBD")}} +{% for version_entry in changelog %} +### v{{ version_entry.version }} - {{ version_entry.date|default("TBD") }} {% for change in version_entry.changes %} -* {{change}} +* {{ change }} {% endfor %} {% if version_entry.host_specific_changes is defined %} {% for host_type in version_entry.host_specific_changes.keys() %} -* {{host_type}} +* {{ host_type }} {% for change in version_entry.host_specific_changes[host_type] %} - * {{change}} + * {{ change }} {% endfor %} {% endfor %} {% endif %} diff --git a/roles/setup/templates/VirtualBox.vbox.tmpl b/roles/setup/templates/VirtualBox.vbox.tmpl new file mode 100644 index 0000000..42c8e92 --- /dev/null +++ b/roles/setup/templates/VirtualBox.vbox.tmpl @@ -0,0 +1,53 @@ +<?xml version="1.0"?> +<!-- +** DO NOT EDIT THIS FILE. +** If you make changes to this file while any VirtualBox related application +** is running, your changes will be overwritten later, without taking effect. +** Use VBoxManage or the VirtualBox Manager GUI to make changes. +--> +<VirtualBox xmlns="http://www.virtualbox.org/" version="1.15-linux"> + <Machine uuid="{{ '{' }}{{ vbox_uuid }}{{ '}' }}" name="{{ vbox_name }}" OSType="{{ virtualbox.os_type }}" snapshotFolder="Snapshots" lastStateChange="2021-06-14T22:09:28Z"> + <ExtraData> + <ExtraDataItem name="GUI/Input/AutoCapture" value="false"/> + <ExtraDataItem name="GUI/LicenseAgreed" value="8"/> + <ExtraDataItem name="GUI/RegistrationData" value="triesLeft=0"/> + <ExtraDataItem name="GUI/SUNOnlineData" value="triesLeft=0"/> + <ExtraDataItem name="GUI/SuppressMessages" value="all,allMessageBoxes,allPopupPanes,confirmGoingFullscreen,confirmInputCapture,confirmRemoveMedium,confirmVMReset,remindAboutAutoCapture,remindAboutInaccessibleMedia,remindAboutMouseIntegrationOn,remindAboutMouseIntegrationOff,remindAboutPausedVMInput,remindAboutWrongColorDepth"/> + <ExtraDataItem name="GUI/UpdateDate" value="never"/> + </ExtraData> + <Hardware> + <CPU count="2"> + <PAE enabled="true"/> + <LongMode enabled="true"/> + <HardwareVirtExLargePages enabled="false"/> + </CPU> + <Memory RAMSize="2048"/> + <Paravirt provider="Default"/> + <Boot> + <Order position="1" device="HardDisk"/> + <Order position="2" device="DVD"/> + <Order position="3" device="None"/> + <Order position="4" device="None"/> + </Boot> + <Display controller="VBoxSVGA" VRAMSize="64"/> + <VideoCapture screens="1" file="." fps="25"/> + <RemoteDisplay enabled="false"/> + <BIOS> + <IOAPIC enabled="true"/> + <SmbiosUuidLittleEndian enabled="true"/> + </BIOS> + <Network> + <Adapter slot="0" enabled="true" MACAddress="" cable="true" type="virtio"> + <NAT> + <Forwarding name="winrm" proto="1" hostip="127.0.0.1" hostport="{{ guest_port }}" guestport="5985"/> + </NAT> + </Adapter> + </Network> + <AudioAdapter driver="Null" enabled="false"/> + <Clipboard/> + </Hardware> + <StorageControllers> + <StorageController name="SATA" type="AHCI" PortCount="5" useHostIOCache="true" Bootable="true" IDE0MasterEmulationPort="0" IDE0SlaveEmulationPort="1" IDE1MasterEmulationPort="2" IDE1SlaveEmulationPort="3"/> + </StorageControllers> + </Machine> +</VirtualBox> \ No newline at end of file diff --git a/roles/setup/templates/bootstrap.ps1.tmpl b/roles/setup/templates/bootstrap.ps1.tmpl new file mode 100644 index 0000000..e95763a --- /dev/null +++ b/roles/setup/templates/bootstrap.ps1.tmpl @@ -0,0 +1,364 @@ +[CmdletBinding()] +param ( + [String] + $Action = '' +) + +$ErrorActionPreference = 'Stop' + +trap { + $msg = "Unhandled exception`r`rn$($_ | Out-String)`r`n$($_.ScriptStackTrace)" + Write-Log -Message $msg -Level Error + Write-Error -ErrorRecord $_ -ErrorAction Continue + + Write-Host -NoNewLine "Press any key to continue..." + $null = $Host.UI.RawUI.ReadKey('NoEcho, IncludeKeyDown') +} + +Function Write-Log { + [CmdletBinding()] + param ( + [Parameter(Mandatory)] + [String] + $Message, + + [ValidateSet('Info', 'Error', 'Warning')] + [String] + $Level = 'Info' + ) + + $dateStr = Get-Date -Format s + $msg = '{0} - {1} - {2}' -f $dateStr, $Level.ToUpper(), $Message + $logPath = Join-Path ([IO.Path]::GetTempPath()) 'bootstrap.log' + + Write-Host $msg + Add-Content -LiteralPath $logPath -Value $msg +} + +Function Restart-AndResume { + [CmdletBinding()] + param ( + [Parameter(Mandatory)] + [String] + $Action + ) + + $command = '"{0}" -File "{1}" "{2}"' -f @( + "$env:SystemDrive\Windows\System32\WindowsPowerShell\v1.0\powershell.exe", + $PSCommandPath, + $Action + ) + $regKey = "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\RunOnce" + Set-ItemProperty -Path $regKey -Name 'bootstrap' -Value $command + + Write-Log -Message "Rebooting server and continuing bootstrap.ps1 with command: $command" + Restart-Computer -Force + Start-Sleep -Seconds 10 +} + +Function Get-VirtIODriverPath { + [CmdletBinding()] + param ( + [Parameter(Mandatory)] + [String] + $Name + ) + + $hostKey = '{{ driver_host_string }}' + $architecture = $env:PROCESSOR_ARCHITECTURE + + Get-PSDrive -PSProvider FileSystem | ForEach-Object -Process { + Get-ChildItem -LiteralPath "$($_.Root)\$Name\$hostKey\$architecture" -Filter '*.inf' -ErrorAction SilentlyContinue + } | Select-Object -First 1 -ExpandProperty FullName +} + +Function Import-Certificate { + [CmdletBinding()] + param ( + [Parameter(Mandatory)] + [System.Security.Cryptography.X509Certificates.X509Certificate2] + $Cert, + + [Parameter(Mandatory)] + [String] + $Store + ) + + $installedThumbprints = @(Get-ChildItem -LiteralPath "Cert:\LocalMachine\$Store" | Select-Object -ExpandProperty Thumbprint) + if ($Cert.Thumbprint -notin $installedThumbprints) { + Write-Log -Message "Certificate $($Cert.Thumbprint) not in $Store store" + $storeName = [System.Security.Cryptography.X509Certificates.StoreName]$Store + $storeLocation = [System.Security.Cryptography.X509Certificates.Storelocation]::LocalMachine + + $certStore = New-Object -TypeName System.Security.Cryptography.X509Certificates.X509Store -ArgumentList $storeName, $storeLocation + $certStore.Open([System.Security.Cryptography.X509Certificates.OpenFlags]::ReadWrite) + try { + $certStore.Add($Cert) + } finally { + # Only .NET 4.6.2 has X509 as an IDisposable, use Close for backwards compatibility + $certStore.Close() + } + } +} + +$tmpdir = 'C:\Windows\TEMP' +Write-Log -Message "Starting bootstrap.ps1 with action '$Action'" + +$bootstrapActions = @( +{% if inventory_hostname == '2012' %} +{# 2012 comes with pwsh v3 which requires a once off hostfix #} + @{ + Name = 'WMFv3 Memory Hotfix' + File = 'KB2842230-wmfv3.zip' + ZipFilePattern = '*KB2842230*.msu' + Action = 'install' + }, +{% endif %} +{% for update in update_files | default([]) %} + @{ + Name = '{{ update.title }}' + File = '{{ update.filename }}' + Action = 'install' + }, +{% endfor %} +{% if platform in ['qemu', 'virtualbox'] %} + @{ + Name = "Red Hat Virtio Network Driver" + Path = (Get-VirtIODriverPath -Name NetKVM) + Action = "driver" + } +{% endif %} +{% if platform == 'qemu' %} + @{ + Name = "Red Hat Virtio SCSI driver" + Path = (Get-VirtIODriverPath -Name vioscsi) + Action = "driver" + }, + @{ + Name = "Red Hat Virtio RNG driver" + Path = (Get-VirtIODriverPath -Name viorng) + Action = "driver" + }, + @{ + Name = "Red Hat Virtio serial driver" + Path = (Get-VirtIODriverPath -Name vioserial) + Action = "driver" + }, + @{ + Name = "Red Hat Virtio Memory Memory Balloon Driver" + Path = (Get-VirtIODriverPath -Name Balloon) + Action = "driver" + }, + @{ + Name = "Red Hat Virtio pvpanic driver" + Path = (Get-VirtIODriverPath -Name pvpanic) + Action = "driver" + }, + @{ + Name = "Red Hat Virtio Graphics Driver" + Path = (Get-VirtIODriverPath -Name qxldod) + Action = "driver" + }, + @{ + Name = "Red Hat Virtio VIOInput driver" + Path = (Get-VirtIODriverPath -Name vioinput) + Action = "driver" + }, + @{ + Name = "Red Hat Virtio PCI serial" + Path = (Get-VirtIODriverPath -Name qemupciserial) + Action = "driver" + }, +{% if inventory_hostname not in ['2012', '2012r2'] %} +{# qemufwcfg only valid for 2016+ #} + @{ + Name = "Red Hat Virtio Firmware Config Driver" + Path = (Get-VirtIODriverPath -Name qemufwcfg) + Action = "driver" + }, +{% endif %} +{% endif %} + @{ + Name = "Configure WinRM" + Action = "winrm" + } +) + +$foundAction = $false +$actualActions = @(foreach ($bootstrapAction in $bootStrapActions) { + if (-not $Action -or $foundAction) { + $bootstrapAction + } + elseif ($bootStrapAction.Name -eq $Action) { + $foundAction = $true + $bootstrapAction + } +}) + +for ($i = 0; $i -lt $actualActions.Count; $i++) { + $currentAction = $actualActions[$i] + $nextAction = $null + if ($i -lt ($actualActions.Count - 1)) { + $nextAction = $actualActions[$i + 1] + } + + switch ($currentAction.Action) { + install { + Write-Log -Message "Installing $($currentAction.Name)" + $null = Add-Type -AssemblyName System.IO.Compression.FileSystem + + $src = Join-Path $PSScriptRoot $currentAction.File + + if ($src.EndsWith('.zip', 'OrdinalIgnoreCase') -and $currentAction.ZipFilePattern) { + [System.IO.Compression.ZipFile]::ExtractToDirectory($src, $tmpdir) + + $src = (Get-Item -Path (Join-Path $tmpdir $currentAction.ZipFilePattern)).FullName + if (-not $src) { + throw "Unable to find extracted file of pattern $($currentAction.ZipFilePattern) for installing $($currentAction.Name)" + } + } + + $arguments = '' + if ($src.EndsWith('.msu', 'OrdinalIgnoreCase')) { + $arguments = '"{0}" /quiet /norestart' -f $src + $src = 'wusa.exe' + } + if ($currentAction.Arguments) { + if ($arguments) { $arguments += ' ' } + $arguments += $currentActions.Arguments + } + + $procParams = @{ + FilePath = $src + NoNewWindow = $true + Wait = $true + PassThru = $true + } + if ($arguments) { + $procParams.ArgumentList = $arguments + } + + $proc = Start-Process @procParams + $rc = $proc.ExitCode + if ($rc -eq 3010) { + Restart-AndResume -Action $nextAction.Name + } + elseif ($rc -ne 0) { + throw "Failed to install $($currentAction.Name): exit code $rc" + } + } + + driver { + Write-Log -Message "Installing driver $($currentAction.Name)" + Add-Type -TypeDefinition @' +using System; +using System.ComponentModel; +using System.Runtime.InteropServices; + +namespace PackerWindoze +{ + public class NativeMethods + { + [DllImport("Newdev.dll", EntryPoint = "DiInstallDriverW", SetLastError = true, CharSet = CharSet.Unicode)] + private static extern bool NativeDiInstallDriverW( + IntPtr hwndParent, + string InfPath, + UInt32 Flags, + out bool NeedReboot); + + public static bool DiInstallDriverW(string infPath) + { + bool needsReboot; + if (!NativeDiInstallDriverW(IntPtr.Zero, infPath, 0, out needsReboot)) + { + int err = Marshal.GetLastWin32Error(); + if (err != 0x00000103) // Is returned if the driver is already present + throw new Win32Exception(err); + else + needsReboot = false; + } + + return needsReboot; + } + } +} +'@ + + # Older hosts may not have the root Microsoft cert that has signed the VirtIO drivers installed. We + # manually install it so we can install the driver silently without user interaction. + $rootCertPath = Join-Path $PSScriptRoot 'MicrosoftCodeVerifRoot.crt' + $rootCert = New-Object -TypeName System.Security.Cryptography.X509Certificates.X509Certificate2 -ArgumentList $rootCertPath + Import-Certificate -Cert $rootCert -Store Root + + $catPath = Get-ChildItem -Path (Split-Path -Path $currentAction.Path -Parent) -Filter '*.cat' -File + $driverCert = (Get-AuthenticodeSignature -LiteralPath $catPath.FullName).SignerCertificate + if ($driverCert) { + Import-Certificate -Cert $driverCert -Store TrustedPublisher + } + + $rebootRequired = [PackerWindoze.NativeMethods]::DiInstallDriverW($currentAction.Path) + if ($rebootRequired) { + Restart-AndResume -Action $nextAction.Name + } + } + + winrm { +{% if platform == 'virtualbox' and inventory_hostname == '2022' %} +{# Bug in NetKVM for Server 2022 - https://github.com/virtio-win/kvm-guest-drivers-windows/issues/583 #} + Write-Log -Message "Disabling Offload Tx Checksum for NetKVM adapter" + Get-NetAdapter | + Where-Object DriverFileName -eq 'netkvm.sys' | + Set-NetAdapterAdvancedProperty -RegistryKeyword 'Offload.TxChecksum' -RegistryValue 0 + +{% endif %} +{% if platform == 'hyperv' %} + $ipAddr = '{{ hyperv_ip }}' + Write-Log -Message "Setting IP address to $ipAddr" + + $ipParams = @{ + IPAddress = $ipAddr + PrefixLength = {{ hyperv_ip_prefix }} + DefaultGateway = '{{ hyperv_gateway }}' + } + Get-NetAdapter | ForEach-Object { + $null = $_ | New-NetIPAddress @ipParams + $null = $_ | Set-DnsClientServerAddress -ServerAddresses ('1.1.1.1') + } + +{% endif %} + Write-Log -Message "Configuring WinRM listener to work over 5985 with Basic auth" + &winrm.cmd quickconfig -q + Set-Item -Path WSMan:\localhost\Service\Auth\Basic -Value $true + Set-Item -Path WSMan:\localhost\Service\AllowUnencrypted -Value $true + + $winrmService = Get-Service -Name winrm + if ($winrmService.Status -ne "Running") { + try { + Start-Service -Name winrm + } catch { + throw "Failed to start the WinRM service required by Ansible: $($_.Exception.Message)" + } + } + + Write-Log -Message "Configuring PSRemoting endpoints" + Enable-PSRemoting -Force -SkipNetworkProfileCheck + + Write-Log -Message "Enabling RDP" + $rdpWMI = Get-CimInstance -ClassName Win32_TerminalServiceSetting -Namespace root\CIMV2\TerminalServices + $rdpEnable = $rdpWMI | Invoke-CimMethod -MethodName SetAllowTSConnections -Arguments @{ AllowTSConnections = 1; ModifyFirewallException = 1 } + if ($rdpEnable.ReturnValue -ne 0) { + throw "Failed to change RDP connection settings, error code: $($rdpEnable.ReturnValue)" + } + + Write-Log -Message "Enabling NLA authentication for RDP" + $nlaWMI = Get-CimInstance -ClassName Win32_TSGeneralSetting -Namespace root\CIMV2\TerminalServices + $nlaWMI | Invoke-CimMethod -MethodName SetUserAuthenticationRequired -Arguments @{ UserAuthenticationRequired = 1 } | Out-Null + $nlaWMI = Get-CimInstance -ClassName Win32_TSGeneralSetting -Namespace root\CIMV2\TerminalServices + if ($nlaWMI.UserAuthenticationRequired -ne 1) { + throw "Failed to enable NLA" + } + } + } +} + +Write-Log -Message "bootstrap.ps1 complete" diff --git a/roles/packer-setup/templates/description.md.j2 b/roles/setup/templates/description.md.tmpl similarity index 59% rename from roles/packer-setup/templates/description.md.j2 rename to roles/setup/templates/description.md.tmpl index b7aef66..7cee219 100644 --- a/roles/packer-setup/templates/description.md.j2 +++ b/roles/setup/templates/description.md.tmpl @@ -1,4 +1,4 @@ -## [Packer Windoze](https://github.com/jborean93/packer-windoze) - {{pri_packer_setup_config.box_tag}} +## [Packer Windoze](https://github.com/jborean93/packer-windoze) - {{ box_tag }} ### Info @@ -9,8 +9,8 @@ and create a brand new Windows instance with WinRM up and running. Details on how to connect are; -* `username`: {{opt_packer_setup_username}} -* `password`: {{opt_packer_setup_password}} +* `username`: {{ setup_username }} +* `password`: {{ setup_password }} * `connector`: winrm Included programs (versions dependent on the Windows version); @@ -25,26 +25,25 @@ Included programs (versions dependent on the Windows version); Other configurations from the standard image; * WinRM HTTP and HTTPS listener with Basic and CredSSP enabled -{% if not pri_packer_setup_config.answer_longhorn %} -* [Win32-OpenSSH](https://github.com/PowerShell/Win32-OpenSSH) v8.0.0.0p1-Beta -{% endif %} -* Default Administrator account disabled, password is also `{{opt_packer_setup_password}}` +* [Win32-OpenSSH](https://github.com/PowerShell/Win32-OpenSSH) {{ openssh_version }} +* PowerShell (`pwsh.exe`) with SSH remoting setup +* Default Administrator account disabled, password is also `{{ setup_password }}` * Hidden files and folders and file extensions are shown by default ### Changes -{% for version_entry in pri_packer_setup_changelog %} -#### v{{version_entry.version}} - {{version_entry.date|default(lookup('pipe', 'date +%Y-%m-%d'))}} +{% for version_entry in changelog %} +#### v{{ version_entry.version }} - {{ version_entry.date|default(lookup('pipe', 'date +%Y-%m-%d')) }} {% for change in version_entry.changes %} -* {{change}} +* {{ change }} {% endfor %} -{% if version_entry.host_specific_changes is defined and man_packer_setup_host_type in version_entry.host_specific_changes.keys() %} +{% if version_entry.host_specific_changes is defined and inventory_hostname in version_entry.host_specific_changes.keys() %} Specific changes to this host type -{% for change in version_entry.host_specific_changes[man_packer_setup_host_type] %} -* {{change}} +{% for change in version_entry.host_specific_changes[inventory_hostname] %} +* {{ change }} {% endfor %} {% endif %} diff --git a/roles/setup/templates/setup-hyperv.ps1.tmpl b/roles/setup/templates/setup-hyperv.ps1.tmpl new file mode 100644 index 0000000..195fa51 --- /dev/null +++ b/roles/setup/templates/setup-hyperv.ps1.tmpl @@ -0,0 +1,208 @@ +[CmdletBinding()] +param () + +$ErrorActionPreference = 'Stop' +$ProgressPreference = 'SilentlyContinue' + +Function ConvertTo-UnattendedIso { + <# + .SYNOPSIS + Converts a Windows ISO to one that doesn't require any keys to press on boot. + + .PARAMETER Path + The Windows ISO to convert the UEFI loader to the no_prompt version. + + .PARAMETER Destination + The path to create the new Windows ISO at. + + .NOTES + This is used with Hyper-V to automatically boot to a Windows installer ISO without any manual intervention that + is required on a default ISO. It has been tested on Windows Server 2012 and newer ISOs. + + Thanks to awakecoding for the initial script of creating an ISO using builtin tools to Windows. + https://github.com/Devolutions/devolutions-labs/blob/bf9df3b89516885de29b41574ba04632caa24736/powershell/DevolutionsLabs.psm1#L67 + #> + [CmdletBinding()] + param ( + [Parameter(Mandatory)] + [String] + $Path, + + [Parameter(Mandatory)] + [String] + $Destination + ) + + $resolvedPath = $PSCmdlet.SessionState.Path.GetUnresolvedProviderPathFromPSPath($Path) + if (-not (Test-Path -LiteralPath $resolvedPath)) { + Write-Error -Message "Source ISO Path '$resolvedPath' does not exist" -Category ObjectNotFound + return + } + + $bootStream = $null + Write-Verbose -Message "Mounting input ISO from '$resolvedPath'" + $mount = Mount-DiskImage -ImagePath $resolvedPath -PassThru + try { + $volume = $mount | Get-Volume + $mountPath = $volume.DriveLetter + ':' + + # The default is efisys.bin which prompts the user to press any key in a small time period. By using the + # noprompt version Hyper-V will automatically boot the ISO. + $efiNoPrompt = [IO.Path]::Combine($mountPath, 'efi', 'microsoft', 'boot', 'efisys_noprompt.bin') + Write-Verbose -Message "Checking for efi noprompt at '$efiNoprompt'" + if (-not (Test-Path -LiteralPath $efiNoPrompt)) { + Write-Error -Message "Source ISO does not contain efisys_noprompt.bin at expected path" -Category ObjectNotFound + return + } + + $fsi = New-Object -ComObject IMAPI2FS.MsftFileSystemImage + $fsi.VolumeName = $volume.FileSystemLabel + $fsi.FileSystemsToCreate = 4 # FsiFileSystemUDF + $fsi.UDFRevision = 0x102 # 1.02 + $fsi.FreeMediaBlocks = 0 + + # This is the magic that overrides the ISO boot command that UEFI will run. + $bootStream = New-Object -ComObject ADODB.Stream + $bootStream.Type = 1 # adTypeBinary + $bootStream.Open() + $bootStream.LoadFromFile($efiNoPrompt) + $bootOptions = New-Object -ComObject IMAPI2FS.BootOptions + $bootOptions.AssignBootImage($bootStream) + + $fsi.BootImageOptions = $bootOptions + Write-Verbose -Message "Adding original ISO contents to new ISO" + $fsi.Root.AddTree($mountPath, $false) + Write-Verbose -Message "Test" + $image = $fsi.CreateResultImage() + } finally { + if ($bootStream) { $bootStream.Close() } + $mount | Dismount-DiskImage | Out-Null + } + + $resolvedDest = $PSCmdlet.SessionState.Path.GetUnresolvedProviderPathFromPSPath($Destination) + Write-Verbose -Message "Opening new FileStream to destination path at '$resolvedDest'" + $outFS = [System.IO.FileStream]::new($resolvedDest, 'Create', 'Write') + try { + # It's a lot slower to do this in pure PowerShell so we use C# + Add-Type -TypeDefinition @' +using System; +using System.IO; +using System.Runtime.InteropServices; +using System.Runtime.InteropServices.ComTypes; + +namespace Com +{ + public class IStreamExtension + { + public static void CopyTo(object src, Stream dest, int blockSize) + { + IStream inputStream = src as IStream; + + byte[] buffer = new byte[blockSize]; + IntPtr readBuffer = Marshal.AllocCoTaskMem(Marshal.SizeOf(typeof(int))); + try + { + int read = 0; + do + { + Marshal.WriteInt32(readBuffer, buffer.Length); + inputStream.Read(buffer, buffer.Length, readBuffer); + read = Marshal.ReadInt32(readBuffer); + + dest.Write(buffer, 0, read); + } + while (read == buffer.Length); + } + finally + { + Marshal.FreeCoTaskMem(readBuffer); + } + } + } +} +'@ + + Write-Verbose -Message "Writing ISO stream to destination" + [Com.IStreamExtension]::CopyTo($image.ImageStream, $outFS, $image.BlockSize) + } + finally { + $outFS.Dispose() + } +} + +$changed = $false +$windowsVersion = {{ inventory_hostname | ansible.windows.quote(shell='powershell') }} +$vmName = "windoze-$windowsVersion" +$password = ConvertTo-SecureString -AsPlainText -Force -String {{ setup_password | ansible.windows.quote(shell='powershell') }} +$cred = [PSCredential]::new({{ setup_username | ansible.windows.quote(shell='powershell') }}, $password) +$outputPath = Split-Path $PSScriptRoot -Parent + +# Hyper-V will lock the path so we copy it to a temp directory +$secIsoPath = (Join-Path $PSScriptRoot secondary.iso) +if (-not (Test-Path -LiteralPath $secIsoPath)) { + Copy-Item (Join-Path $outputPath "$windowsVersion-secondary.iso") $secIsoPath +} + +$vm = Get-VM -Name $vmName -ErrorAction SilentlyContinue +if (-not $vm) { + $changed = $true + + # Convert the ISO to one that automatically boots without any user prompts. + $winIsoPath = Join-Path $PSScriptRoot 'windows.iso' + ConvertTo-UnattendedIso -Path (Join-Path $outputPath "$windowsVersion-windows.iso") -Destination $winIsoPath + + $vhdxPath = (Join-Path $PSScriptRoot "$($windowsVersion)-vm.vhdx") + if (Test-Path -LiteralPath $vhdxPath) { + Remove-Item -LiteralPath $vhdxPath -Force + } + + $vmParams = @{ + Name = $vmName + MemoryStartupBytes = 2048MB + SwitchName = 'WSL' + NewVHDPath = $vhdxPath + NewVHDSizeBytes = 40GB + Generation = 2 + Force = $true + } + $vmSettings = @{ + AutomaticCheckpointsEnabled = $false + AutomaticStopAction = 'Shutdown' + CheckpointType = 'Disabled' + ProcessorCount = 2 + } + $vm = New-VM @vmParams + $vm | Set-VM @vmSettings + $winDvd = $vm | Add-VMDvdDrive -Path $winIsoPath -PassThru + $vm | Add-VMDvdDrive -Path $secIsoPath + $vm | Set-VMFirmware -BootOrder $vm.HardDrives[0], $winDvd + $vm | Start-VM +} + +if (-not ${{ headless }}) { + vmconnect localhost $vmName +} + +while ($true) { + # Wait until the VM is online and Windows is installed. + while ((Get-VMIntegrationService -VMName $vmName -Name Heartbeat).PrimaryStatusDescription -ne 'OK') { + Start-Sleep -Seconds 5 + } + +{% if inventory_hostname not in ['2012', '2012r2'] %} +{# Hyper-V direct only works when the target has powershell >=5 #} + # Finally check that the host is online and ready to use + try { + $null = Invoke-Command -VMName $vmName -Credential $cred -ScriptBlock { + "hello" + } + } + catch { + continue + } + +{% endif %} + break +} + +@{changed = $changed} | ConvertTo-Json -Compress diff --git a/roles/sysprep/files/PackerWindoze.psm1 b/roles/sysprep/files/PackerWindoze.psm1 index a868358..204d6a2 100644 --- a/roles/sysprep/files/PackerWindoze.psm1 +++ b/roles/sysprep/files/PackerWindoze.psm1 @@ -1,75 +1,99 @@ -Function New-LegacySelfSignedCert($subject, $valid_days) { - Write-Verbose -Message "Creating self-signed certificate of CN=$subject for $valid_days days" - $subject_name = New-Object -ComObject X509Enrollment.CX500DistinguishedName - $subject_name.Encode("CN=$subject", 0) - - $private_key = New-Object -ComObject X509Enrollment.CX509PrivateKey - $private_key.ProviderName = "Microsoft Enhanced RSA and AES Cryptographic Provider" - $private_key.KeySpec = 1 - $private_key.Length = 4096 - $private_key.SecurityDescriptor = "D:PAI(A;;0xd01f01ff;;;SY)(A;;0xd01f01ff;;;BA)(A;;0x80120089;;;NS)" - $private_key.MachineContext = 1 - $private_key.Create() - - $server_auth_oid = New-Object -ComObject X509Enrollment.CObjectId - $server_auth_oid.InitializeFromValue("1.3.6.1.5.5.7.3.1") +# Copyright: (c) 2021, Jordan Borean (@jborean93) <jborean93@gmail.com> +# MIT License (see LICENSE or https://opensource.org/licenses/MIT) + +Function New-LegacySelfSignedCert { + [CmdletBinding()] + param ( + [Parameter(Mandatory)] + [String] + $Subject, + + [Parameter(Mandatory)] + [Int32] + $ValidDays + ) + Write-Verbose -Message "Creating self-signed certificate of CN=$Subject for $ValidDays days" + $subjectName = New-Object -ComObject X509Enrollment.CX500DistinguishedName + $subjectName.Encode("CN=$Subject", 0) + + $privateKey = New-Object -ComObject X509Enrollment.CX509PrivateKey + $privateKey.ProviderName = "Microsoft Enhanced RSA and AES Cryptographic Provider" + $privateKey.KeySpec = 1 + $privateKey.Length = 4096 + $privateKey.SecurityDescriptor = "D:PAI(A;;0xd01f01ff;;;SY)(A;;0xd01f01ff;;;BA)(A;;0x80120089;;;NS)" + $privateKey.MachineContext = 1 + $privateKey.Create() + + $serverAuthOid = New-Object -ComObject X509Enrollment.CObjectId + $serverAuthOid.InitializeFromValue("1.3.6.1.5.5.7.3.1") $ekuoids = New-Object -ComObject X509Enrollment.CObjectIds - $ekuoids.Add($server_auth_oid) + $ekuoids.Add($serverAuthOid) - $eku_extension = New-Object -ComObject X509Enrollment.CX509ExtensionEnhancedKeyUsage - $eku_extension.InitializeEncode($ekuoids) + $ekuExtension = New-Object -ComObject X509Enrollment.CX509ExtensionEnhancedKeyUsage + $ekuExtension.InitializeEncode($ekuoids) - $name = @($env:COMPUTERNAME, ([System.Net.Dns]::GetHostByName($env:COMPUTERNAME).Hostname)) - $alt_names = New-Object -ComObject X509Enrollment.CAlternativeNames - foreach ($name in $name) { - $alt_name = New-Object -ComObject X509Enrollment.CAlternativeName - $alt_name.InitializeFromString(0x3, $name) - $alt_names.Add($alt_name) + $names = @($env:COMPUTERNAME, ([System.Net.Dns]::GetHostByName($env:COMPUTERNAME).Hostname)) + $altNames = New-Object -ComObject X509Enrollment.CAlternativeNames + foreach ($name in $names) { + $altName = New-Object -ComObject X509Enrollment.CAlternativeName + $altName.InitializeFromString(0x3, $name) + $altNames.Add($altName) } - $alt_names_extension = New-Object -ComObject X509Enrollment.CX509ExtensionAlternativeNames - $alt_names_extension.InitializeEncode($alt_names) + $altNamesExtension = New-Object -ComObject X509Enrollment.CX509ExtensionAlternativeNames + $altNamesExtension.InitializeEncode($altNames) - $digital_signature = [Security.Cryptography.X509Certificates.X509KeyUsageFlags]::DigitalSignature - $key_encipherment = [Security.Cryptography.X509Certificates.X509KeyUsageFlags]::KeyEncipherment - $key_usage = [int]($digital_signature -bor $key_encipherment) - $key_usage_extension = New-Object -ComObject X509Enrollment.CX509ExtensionKeyUsage - $key_usage_extension.InitializeEncode($key_usage) - $key_usage_extension.Critical = $true + $digitalSignature = [Security.Cryptography.X509Certificates.X509KeyUsageFlags]::DigitalSignature + $keyEncipherment = [Security.Cryptography.X509Certificates.X509KeyUsageFlags]::KeyEncipherment + $keyUsage = [int]($digitalSignature -bor $keyEncipherment) + $keyUsageExtension = New-Object -ComObject X509Enrollment.CX509ExtensionKeyUsage + $keyUsageExtension.InitializeEncode($keyUsage) + $keyUsageExtension.Critical = $true - $signature_oid = New-Object -ComObject X509Enrollment.CObjectId - $sha256_oid = New-Object -TypeName Security.Cryptography.Oid -ArgumentList "SHA256" - $signature_oid.InitializeFromValue($sha256_oid.Value) + $signatureOID = New-Object -ComObject X509Enrollment.CObjectId + $sha256OID = New-Object -TypeName Security.Cryptography.Oid -ArgumentList "SHA256" + $signatureOID.InitializeFromValue($sha256OID.Value) $certificate = New-Object -ComObject X509Enrollment.CX509CertificateRequestCertificate - $certificate.InitializeFromPrivateKey(2, $private_key, "") - $certificate.Subject = $subject_name + $certificate.InitializeFromPrivateKey(2, $privateKey, "") + $certificate.Subject = $subjectName $certificate.Issuer = $certificate.Subject $certificate.NotBefore = (Get-Date).AddDays(-1) - $certificate.NotAfter = $certificate.NotBefore.AddDays($valid_days) - $certificate.X509Extensions.Add($key_usage_extension) - $certificate.X509Extensions.Add($alt_names_extension) - $certificate.X509Extensions.Add($eku_extension) - $certificate.SignatureInformation.HashAlgorithm = $signature_oid + $certificate.NotAfter = $certificate.NotBefore.AddDays($ValidDays) + $certificate.X509Extensions.Add($keyUsageExtension) + $certificate.X509Extensions.Add($altNamesExtension) + $certificate.X509Extensions.Add($ekuExtension) + $certificate.SignatureInformation.HashAlgorithm = $signatureOID $certificate.Encode() $enrollment = New-Object -ComObject X509Enrollment.CX509Enrollment $enrollment.InitializeFromRequest($certificate) - $certificate_data = $enrollment.CreateRequest(0) - $enrollment.InstallResponse(2, $certificate_data, 0, "") + $certificateData = $enrollment.CreateRequest(0) + $enrollment.InstallResponse(2, $certificateData, 0, "") - $parsed_certificate = New-Object -TypeName System.Security.Cryptography.X509Certificates.X509Certificate2 - $parsed_certificate.Import([System.Text.Encoding]::UTF8.GetBytes($certificate_data)) + $parsedCertificate = New-Object -TypeName System.Security.Cryptography.X509Certificates.X509Certificate2 + $parsedCertificate.Import([System.Text.Encoding]::UTF8.GetBytes($certificateData)) - return $parsed_certificate + $parsedCertificate } Function New-FirewallRule { param( - [Parameter(mandatory=$true)][String]$Name, - [Parameter(mandatory=$true)][String]$Description, - [Parameter(mandatory=$true)][int]$Port, - [Parameter()][Switch]$Deny + [Parameter(mandatory)] + [String] + $Name, + + [Parameter(mandatory)] + [String] + $Description, + + [Parameter(mandatory)] + [Int32] + $Port, + + [Parameter()] + [Switch] + $Deny ) $fw = New-Object -ComObject HNetCfg.FWPolicy2 @@ -88,7 +112,7 @@ Function New-FirewallRule { if ($Deny.IsPresent) { $action = 0 # Deny } - $rule_details = @{ + $ruleDetails = @{ LocalPorts = $Port RemotePorts = "*" LocalAddresses = "*" @@ -101,13 +125,13 @@ Function New-FirewallRule { $rule.Protocol = 6 $changed = $false - foreach ($detail in $rule_details.GetEnumerator()) { - $original_value = $rule.$($detail.Name) - $new_value = $detail.Value - Write-Verbose -Message "Checking FW Rule property $($detail.Name) - Actual: '$original_value', Expected: '$new_value'" - if ($original_value -ne $new_value) { + foreach ($detail in $ruleDetails.GetEnumerator()) { + $originalValue = $rule.$($detail.Name) + $newValue = $detail.Value + Write-Verbose -Message "Checking FW Rule property $($detail.Name) - Actual: '$originalValue', Expected: '$newValue'" + if ($originalValue -ne $newValue) { Write-Verbose -Message "FW Rule property $($detail.Name) does not match, changing rule" - $rule.$($detail.Name) = $new_value + $rule.$($detail.Name) = $newValue $changed = $true } } @@ -155,7 +179,7 @@ Function Reset-WinRMConfig { 3. Creates a HTTP and HTTPS listener with a SHA256 self-signed certificate 4. Enables PSRemoting 5. Enables Basic auth - 6. Enabled CredSSP auth + 6. Enables CredSSP auth 7. Tests that both HTTP and HTTPS are accessible over localhost .PARAMETER CertificateThumbprint [string] - Instead of generating a self-signed certificate, use this @@ -183,10 +207,10 @@ Function Reset-WinRMConfig { # until all the steps are completed before returning. This deny rule is # removed at the end of this process Write-Verbose -Message "Creating deny WinRM Firewall rules during setup process" - $http_deny_rule = "PackerWindoze temp WinRM HTTP Deny rule" - $https_deny_rule = "PackerWindoze temp WinRM HTTPS Deny rule" - New-FirewallRule -Name $http_deny_rule -Description $http_deny_rule -Port 5985 -Deny - New-FirewallRule -Name $https_deny_rule -Description $https_deny_rule -Port 5986 -Deny + $httpDenyRule = "PackerWindoze temp WinRM HTTP Deny rule" + $httpsDenyRule = "PackerWindoze temp WinRM HTTPS Deny rule" + New-FirewallRule -Name $httpDenyRule -Description $httpDenyRule -Port 5985 -Deny + New-FirewallRule -Name $httpsDenyRule -Description $httpsDenyRule -Port 5986 -Deny Write-Verbose -Message "Enabling Basic authentication" Set-Item -Path WSMan:\localhost\Service\Auth\Basic -Value $true @@ -198,70 +222,77 @@ Function Reset-WinRMConfig { Set-Item -Path WSMan:\localhost\Service\AllowUnencrypted -Value $false Write-Verbose -Message "Setting the LocalAccountTokenFilterPolicy registry key for remote admin access" - $token_path = "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System" - $token_prop_name = "LocalAccountTokenFilterPolicy" - $token_key = Get-Item -Path $token_path - $token_value = $token_key.GetValue($token_prop_name, $null) - if ($token_value -ne 1) { - Write-Verbose -Message "Setting LocalAccountTOkenFilterPolicy to 1" - if ($null -ne $token_value) { - Remove-ItemProperty -Path $token_path -Name $token_prop_name + $tokenPath = "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System" + $tokenPropName = "LocalAccountTokenFilterPolicy" + $tokenKey = Get-Item -Path $tokenPath + try { + $tokenValue = $tokenKey.GetValue($tokenPropName, $null) + if ($tokenValue -ne 1) { + Write-Verbose -Message "Setting LocalAccountTokenFilterPolicy to 1" + if ($null -ne $tokenValue) { + Remove-ItemProperty -Path $tokenPath -Name $tokenPropName + } + New-ItemProperty -Path $tokenPath -Name $tokenPropName -Value 1 -PropertyType DWORD > $null } - New-ItemProperty -Path $token_path -Name $token_prop_name -Value 1 -PropertyType DWORD > $null + } + finally { + $tokenKey.Dispose() } Write-Verbose -Message "Creating HTTP listener" - $selector_set = @{ + $selectorSet = @{ Transport = "HTTP" Address = "*" } - $value_set = @{ + $valueSet = @{ Enabled = $true } - New-WSManInstance -ResourceURI winrm/config/listener -SelectorSet $selector_set -ValueSet $value_set > $null + New-WSManInstance -ResourceURI winrm/config/listener -SelectorSet $selectorSet -ValueSet $valueSet > $null Write-Verbose -Message "Creating HTTPS listener" if ($CertificateThumbprint) { $thumbprint = $CertificateThumbprint } else { - $certificate = New-LegacySelfSignedCert -subject $env:COMPUTERNAME -valid_days 1095 + $certificate = New-LegacySelfSignedCert -Subject $env:COMPUTERNAME -ValidDays 1095 $thumbprint = $certificate.Thumbprint } - $selector_set = @{ + $selectorSet = @{ Transport = "HTTPS" Address = "*" } - $value_set = @{ + $valueSet = @{ CertificateThumbprint = $thumbprint Enabled = $true } - New-WSManInstance -ResourceURI "winrm/config/Listener" -SelectorSet $selector_set -ValueSet $value_set > $null + New-WSManInstance -ResourceURI winrm/config/Listener -SelectorSet $selectorSet -ValueSet $valueSet > $null Write-Verbose -Message "Configuring WinRM HTTPS firewall rule" - New-FirewallRule -Name "Windows Remote Management (HTTPS-In)" ` - -Description "Inbound rule for Windows Remote Management via WS-Management. [TCP 5986]" ` - -Port 5986 + $firewallArgs = @{ + Name = 'Windows Remote Management (HTTPS-In)' + Description = 'Inbound rule for Windows Remote Management via WS-Management. [TCP 5986]' + Port = 5986 + } + New-FirewallRule @firewallArgs Write-Verbose "Enabling PowerShell Remoting" # Change the verbose output for this cmdlet only as the output is really verbose - $orig_verbose = $VerbosePreference + $origVerbose = $VerbosePreference $VerbosePreference = "SilentlyContinue" Enable-PSRemoting -Force > $null - $VerbosePreference = $orig_verbose + $VerbosePreference = $origVerbose Write-Verbose -Message "Removing WinRM deny firewall rules as config is complete" - Remove-FirewallRule -Name $http_deny_rule - Remove-FirewallRule -Name $https_deny_rule + Remove-FirewallRule -Name $httpDenyRule + Remove-FirewallRule -Name $httpsDenyRule Write-Verbose -Message "Testing out WinRM communication over localhost" - $session_option = New-PSSessionOption -SkipCACheck -SkipCNCheck -SkipRevocationCheck - $invoke_args = @{ + $invokeArgs = @{ ComputerName = "localhost" ScriptBlock = { $env:COMPUTERNAME } - SessionOption = $session_option + SessionOption = (New-PSSessionOption -SkipCACheck -SkipCNCheck -SkipRevocationCheck) } - Invoke-Command @invoke_args > $null - Invoke-Command -UseSSL @invoke_args > $null + Invoke-Command @invokeArgs > $null + Invoke-Command -UseSSL @invokeArgs > $null Write-Verbose -Message "WinRM and PS Remoting have been set up successfully" } diff --git a/roles/sysprep/tasks/main.yml b/roles/sysprep/tasks/main.yml index 2541d01..a131deb 100644 --- a/roles/sysprep/tasks/main.yml +++ b/roles/sysprep/tasks/main.yml @@ -57,11 +57,8 @@ - name: set flag to recreate pagefile after next sysprep win_shell: | - $system = Get-WmiObject -Class Win32_ComputerSystem -EnableAllPrivileges - if ($system -ne $null) { - $system.AutomaticManagedPagefile = $true - $system.Put() - } + Get-CimInstance -ClassName Win32_ComputerSystem | + Set-CimInstance -Property @{ AutomaticManagedPagefile = $true } - name: 0 out empty space for later compression win_command: 'SDelete.exe -accepteula -q -z C:' diff --git a/roles/sysprep/templates/PackerWindoze.psd1 b/roles/sysprep/templates/PackerWindoze.psd1 index 6ebb045..79c2fe9 100644 --- a/roles/sysprep/templates/PackerWindoze.psd1 +++ b/roles/sysprep/templates/PackerWindoze.psd1 @@ -1,12 +1,12 @@ @{ ModuleToProcess = 'PackerWindoze.psm1' - ModuleVersion = '{{man_packer_windoze_version}}' + ModuleVersion = '{{ windoze_version }}' GUID = '22645f60-5878-40ac-ac73-1054e7f3b921' Author = 'Jordan Borean' Copyright = 'Copyright (c) 2018 by Jordan Borean, licensed under MIT.' Description = 'Provide simple cmdlets that are used as part of the packer-windoze process like resetting the WinRM configuration' PowerShellVersion = '3.0' - FunctionsToExport = @( + FunctionsToExport = @( 'Reset-WinRMConfig' ) } diff --git a/roles/sysprep/templates/setup.ps1.tmpl b/roles/sysprep/templates/setup.ps1.tmpl index 5e5c1a3..80d1473 100644 --- a/roles/sysprep/templates/setup.ps1.tmpl +++ b/roles/sysprep/templates/setup.ps1.tmpl @@ -1,60 +1,97 @@ $ErrorActionPreference = 'Stop' -$runonce_reg_key = "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\RunOnce" -$ps_path = "$env:SystemDrive\Windows\System32\WindowsPowerShell\v1.0\powershell.exe" -$tmp_dir = "$env:SystemDrive\temp" - -Function Write-Log($message, $level="INFO") { - $date_stamp = Get-Date -Format s - $log_entry = "$date_stamp - $level - $message" - $log_file = "$tmp_dir\sysprep-setup.log" - Write-Host $log_entry - Add-Content -Path $log_file -Value $log_entry +Function Write-Log { + [CmdletBinding()] + param ( + [Parameter(Mandatory)] + [String] + $Message, + + [ValidateSet('Info', 'Error')] + [String] + $Level = 'Info' + ) + + $dateStr = Get-Date -Format s + $msg = '{0} - {1} - {2}' -f $dateStr, $Level.ToUpper(), $Message + + Write-Host $msg + $logFile = Join-Path $PSScriptRoot 'sysprep-setup.log' + Add-Content -Path $logFile -Value $msg } $action = $args[0] -switch($action) { +switch( $action) { "post-sysprep" { - Write-Log -message "Deleting packer shutdown scheduled task" + Write-Log -Message "Deleting packer shutdown scheduled task" &schtasks.exe /Delete /TN "packer-shutdown" /F - Write-Log -message "Removing the sysprep files as they are no longer needed" + Write-Log -Message "Removing the sysprep files as they are no longer needed" Remove-Item -Path C:\Windows\Panther\Unattend -Force -Recurse > $null - Write-Log -message "Disabling the Administrator account as it is not needed" + Write-Log -Message "Disabling the Administrator account as it is not needed" &cmd.exe /c net user Administrator /active:no - Write-Log -message "Disabling the password expiration of the {{ansible_user}} account" - &cmd.exe @("/c", "WMIC", "USERACCOUNT", "WHERE", "Name='{{ansible_user}}'", "SET", "PasswordExpires=FALSE") + Write-Log -Message "Disabling the password expiration of the current account" + $sid = [System.Security.Principal.WindowsIdentity]::GetCurrent().User + $adsi = [ADSI]"WinNT://$env:COMPUTERNAME" + $user = $adsi.Children | Where-Object { + if ($_.SchemaClassName -ne 'User') { + return $false + } -{% if man_is_longhorn == False %} - Write-Log -message "Setting sshd services to auto start" + $userSid = New-Object -TypeName System.Security.Principal.SecurityIdentifier -ArgumentList $_.objectSid[0], 0 + $userSid.Equals($sid) + } + $user.UserFlags = $user.UserFlags.Value -bor 65536 # ADS_UF_DONT_EXPIRE_PASSWD + $user.SetInfo() + + Write-Log -Message "Setting sshd services to auto start" Set-Service -Name sshd -StartupType Automatic Set-Service -Name ssh-agent -StartupType Automatic -{% endif %} - Write-Log -message "Rearming the host using slmgr.vbs /rearm" + Write-Log -Message "Rearming the host using slmgr.vbs /rearm" &C:\Windows\System32\cscript.exe C:\Windows\System32\slmgr.vbs /rearm - Write-Log -message "Restarting the host to rerun script with action winrm-active" - $command = "$ps_path $($script:MyInvocation.MyCommand.Path) winrm-active" - Set-ItemProperty -Path $runonce_reg_key -Name "bootstrap" -Value $command + $runonceKey = 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\RunOnce' + $psPath = "$env:SystemDrive\Windows\System32\WindowsPowerShell\v1.0\powershell.exe" + + Write-Log -Message "Restarting the host to rerun script with action winrm-active" + $command = "$psPath $PSCommandPath winrm-active" + Set-ItemProperty -Path $runonceKey -Name "bootstrap" -Value $command Restart-Computer -Force } "winrm-active" { - Write-Log -message "Deleting auto logon entries from registry" - $reg_winlogon_path = "HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon" - Set-ItemProperty -Path $reg_winlogon_path -Name AutoAdminLogon -Value 0 - Remove-ItemProperty -Path $reg_winlogon_path -Name DefaultUserName -ErrorAction SilentlyContinue - Remove-ItemProperty -Path $reg_winlogon_path -Name DefaultPassword -ErrorAction SilentlyContinue + Write-Log -Message "Deleting auto logon entries from registry" + $regWinlogon = 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon' + Set-ItemProperty -Path $regWinlogon -Name AutoAdminLogon -Value 0 + Remove-ItemProperty -Path $regWinlogon -Name DefaultUserName -ErrorAction SilentlyContinue + Remove-ItemProperty -Path $regWinlogon -Name DefaultPassword -ErrorAction SilentlyContinue - Write-Log -message "Setting the rearm key back to 0 so people in the future can rearm the OS" + Write-Log -Message "Setting the rearm key back to 0 so people in the future can rearm the OS" Set-ItemProperty -Path "HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\SoftwareProtectionPlatform" -Name SkipRearm -Value 0 - Write-Log -message "Recreate the WinRM listeners" +{% if platform == 'virtualbox' and inventory_hostname == 'win-2022' %} +{# Bug in NetKVM for Server 2022 - https://github.com/virtio-win/kvm-guest-drivers-windows/issues/583 #} + Write-Log -Message "Disabling Offload Tx Checksum for NetKVM adapter" + Get-NetAdapter | + Where-Object DriverFileName -eq 'netkvm.sys' | + Set-NetAdapterAdvancedProperty -RegistryKeyword 'Offload.TxChecksum' -RegistryValue 0 +{% endif %} + Write-Log -Message "Recreate the WinRM listeners" Reset-WinRMConfig -Verbose - Write-Log -message "Cleaning up C:\temp and logging off" + # Older Windows hosts (Server 2012) uses a SHA1 cert which newer OpenSSL versions don't support. + # We just replace on all hosts to make things simple and uniform + Write-Log -Message "Updating RDP certificate to a SHA256 backed cert" + $thumbprint = Get-Item -Path WSMan:\localhost\Listener\*\* | + Where-Object { $_.Name -eq 'CertificateThumbprint' -and $_.Value } | + Select-Object -First 1 -ExpandProperty Value + + Get-CimInstance -ClassName Win32_TSGeneralSetting -Namespace ROOT\CIMV2\TerminalServices | + Set-CimInstance -Property @{ SSLCertificateSHA1Hash = $thumbprint } + + Write-Log -Message "Cleaning up C:\temp and logging off" Remove-Item -Path C:\temp -Force -Recurse New-Item -Path C:\temp -ItemType Directory diff --git a/roles/sysprep/templates/unattend.xml.tmpl b/roles/sysprep/templates/unattend.xml.tmpl index 2741622..2ad98cb 100644 --- a/roles/sysprep/templates/unattend.xml.tmpl +++ b/roles/sysprep/templates/unattend.xml.tmpl @@ -1,49 +1,42 @@ <?xml version="1.0" encoding="utf-8"?> <unattend xmlns="urn:schemas-microsoft-com:unattend"> <settings pass="generalize"> - <component name="Microsoft-Windows-Security-SPP" processorArchitecture="{{man_host_architecture}}" 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 name="Microsoft-Windows-Security-SPP" processorArchitecture="{{ architecture }}" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS" xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> <SkipRearm>1</SkipRearm> </component> - <component name="Microsoft-Windows-PnpSysprep" processorArchitecture="{{man_host_architecture}}" 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 name="Microsoft-Windows-PnpSysprep" processorArchitecture="{{ architecture }}" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS" xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> <PersistAllDeviceInstalls>false</PersistAllDeviceInstalls> -{% if man_is_longhorn == False %} <DoNotCleanUpNonPresentDevices>false</DoNotCleanUpNonPresentDevices> -{% endif %} </component> </settings> <settings pass="oobeSystem"> - <component name="Microsoft-Windows-International-Core" processorArchitecture="{{man_host_architecture}}" 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 name="Microsoft-Windows-International-Core" processorArchitecture="{{ architecture }}" 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="{{man_host_architecture}}" 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 name="Microsoft-Windows-Shell-Setup" processorArchitecture="{{ architecture }}" 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> <ProtectYourPC>1</ProtectYourPC> <NetworkLocation>Home</NetworkLocation> -{% if man_is_longhorn == False %} <HideWirelessSetupInOOBE>true</HideWirelessSetupInOOBE> -{% endif %} </OOBE> <TimeZone>UTC</TimeZone> -{% if man_is_longhorn == True %} - <ComputerName>*</ComputerName> -{% endif %} <UserAccounts> <AdministratorPassword> - <Value>{{ansible_password}}</Value> + <Value>{{ ansible_password }}</Value> <PlainText>true</PlainText> </AdministratorPassword> <LocalAccounts> <LocalAccount wcm:action="add"> <Group>Administrators</Group> - <DisplayName>{{ansible_user}}</DisplayName> - <Name>{{ansible_user}}</Name> - <Description>{{ansible_user}}</Description> + <DisplayName>{{ ansible_user }}</DisplayName> + <Name>{{ ansible_user }}</Name> + <Description>{{ ansible_user }}</Description> <Password> - <Value>{{ansible_password}}</Value> + <Value>{{ ansible_password }}</Value> <PlainText>true</PlainText> </Password> </LocalAccount> @@ -51,9 +44,9 @@ </UserAccounts> <AutoLogon> <Enabled>true</Enabled> - <Username>{{ansible_user}}</Username> + <Username>{{ ansible_user }}</Username> <Password> - <Value>{{ansible_password}}</Value> + <Value>{{ ansible_password }}</Value> <PlainText>true</PlainText> </Password> </AutoLogon> @@ -66,11 +59,5 @@ </component> </settings> <settings pass="specialize"> -{% if man_is_longhorn == True %} - <!-- Server 2008 requires this to stop asking for the Computer Name after sysprep --> - <component name="Microsoft-Windows-Shell-Setup" processorArchitecture="{{man_host_architecture}}" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS" xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> - <ComputerName>*</ComputerName> - </component> -{% endif %} </settings> </unattend> diff --git a/roles/update/filter_plugins/update_filters.py b/roles/update/filter_plugins/update_filters.py deleted file mode 100644 index cfc675e..0000000 --- a/roles/update/filter_plugins/update_filters.py +++ /dev/null @@ -1,28 +0,0 @@ -class FilterModule(object): - def filters(self): - return { - 'update_merge': self.update_merge, - 'categories_to_list': self.categories_to_list - } - - def update_merge(self, original_list, key, value): - new_list = [] - for entry in original_list: - if entry['name'] == key: - new_list.append( - { - 'name': key, - 'finished': value - } - ) - else: - new_list.append(entry) - - return new_list - - def categories_to_list(self, update_dict): - categories = [] - for meta in update_dict: - categories.append(meta['name']) - - return categories diff --git a/roles/update/tasks/main.yml b/roles/update/tasks/main.yml index d2353b2..54dcda0 100644 --- a/roles/update/tasks/main.yml +++ b/roles/update/tasks/main.yml @@ -18,33 +18,11 @@ - name: Level data: 4 -# This is a pretty ugly hack as there is no do until over a block of tasks. This -# will loop over the update and reboot task 10 times to cover all updates, the -# tasks in the loop are skipped when there are no updates which leads to a very -# minimal loss of time when all the updates are installed and we are still -# looping. -# We also set a dict that contains each category we loop through and whether we -# have exhausted the updates for the respective category. This is only done -# win_updates sometimes fails when too many updates need to be installed. This -# way each update block is split into smaller chunks. -- name: set initial update_stat fact - set_fact: - update_stat: - - name: CriticalUpdates - finished: False - - name: SecurityUpdates - finished: False - - name: Updates - finished: False - - name: UpdateRollups - finished: False - - name: FeaturePacks - finished: False - -- include_tasks: update.yml - loop: [1, 2, 3, 4, 5, 6, 7, 8, 9, 'end'] - loop_control: - loop_var: loop_number +- name: install updates and reboot along the way + win_updates: + category_names: '*' + state: installed + reboot: yes - name: remove verbose Windows Update logging win_regedit: diff --git a/roles/update/tasks/update.yml b/roles/update/tasks/update.yml deleted file mode 100644 index ae36ea1..0000000 --- a/roles/update/tasks/update.yml +++ /dev/null @@ -1,21 +0,0 @@ ---- -# run our customised update task for each category -- name: install updates - round {{ loop_number }} - include_tasks: update_task.yml - loop: '{{ update_stat }}' - loop_control: - loop_var: category_info - -- name: run final update task at the end in case we missed anything - win_updates: - category_names: '{{ update_stat|categories_to_list }}' - state: installed - register: pri_update_final - when: loop_number == 'end' - -- name: reboot after final update task - win_reboot: - reboot_timeout: 1200 - when: - - pri_update_final.reboot_required is defined - - pri_update_final.reboot_required diff --git a/roles/update/tasks/update_task.yml b/roles/update/tasks/update_task.yml deleted file mode 100644 index 8453e17..0000000 --- a/roles/update/tasks/update_task.yml +++ /dev/null @@ -1,48 +0,0 @@ ---- -- block: - - name: check if there are missing updates - {{ category_info.name }} - round {{ loop_number }} - win_updates: - category_names: - - '{{ category_info.name }}' - state: searched - register: pri_update_count - become: yes - when: not category_info.finished - - rescue: - - name: restart the WSUS service after a failed check - win_service: - name: wuauserv - state: restarted - - - name: reboot the server after a failed check - win_reboot: - reboot_timeout: 1200 - - - name: check if there are missing updates - {{ category_info.name }} - round {{ loop_number }} ATTEMPT 2 - win_updates: - category_names: - - '{{ category_info.name }}' - state: searched - register: pri_update_count - become: yes - -- name: set no more update flag for {{ category_info.name }} if not more updates and it isn't the 1st 2 iterations - set_fact: - update_stat: '{{ update_stat|update_merge(category_info.name, True) }}' - when: pri_update_count.found_update_count is defined and pri_update_count.found_update_count == 0 and (loop_number != 1 and loop_number != 2) - -- name: install windows updates - {{ category_info.name }} - round {{ loop_number }} - win_updates: - category_names: - - '{{ category_info.name }}' - state: installed - reboot: yes - ignore_errors: yes # get intermittent errors with this, usually 2nd round succeeds so let's ignore it - become: yes - register: pri_update_result - when: pri_update_count.found_update_count is defined and pri_update_count.found_update_count > 0 - -- name: reboot to handle intermittent update failure - win_reboot: - when: pri_update_result is failed diff --git a/stop.yml b/stop.yml new file mode 100644 index 0000000..46bbbc1 --- /dev/null +++ b/stop.yml @@ -0,0 +1,64 @@ +- name: stop all build instances + hosts: setup + gather_facts: no + + tasks: + # QEMU + - name: QEMU - check if VM is already running + community.general.pids: + pattern: '{{ ("qemu-system-x86_64 -name windoze-" ~ inventory_hostname ~ " -machine") | regex_escape("posix_basic") }}*' + register: qemu_pid + + - name: QEMU - stop VMs + command: kill {{ item }} + loop: '{{ qemu_pid.pids }}' + + # VirtualBox + - name: VirtualBox - check if VM is registered + command: VBoxManage showvminfo windoze-{{ inventory_hostname }} + register: vbox_vm_info + changed_when: False + failed_when: False + + - name: VirtualBox - stop VM + command: VBoxManage controlvm {{ ('windoze-' ~ inventory_hostname) | quote }} poweroff + when: '"running" in vbox_vm_info.stdout' + + - name: VirtualBox - remove VM + command: VBoxManage unregistervm {{ ('windoze-' ~ inventory_hostname) | quote }} --delete + when: vbox_vm_info.rc == 0 + + # Hyper-V + - name: Hyper-V - stop VM and remove VM + shell: | + $changed = $false + $vmName = 'windoze-{{ inventory_hostname }}' + $vm = Get-VM -Name $vmName -ErrorAction SilentlyContinue + if ($vm) { + $changed = $true + if ($vm.State -ne 'Off') { + $vm | Stop-VM -TurnOff + + while ((Get-VM -Name $vmName).State -ne 'Off') { + Start-Sleep -Seconds 1 + } + } + + $vm | Remove-VM -Force + } + + $changed + args: + executable: powershell.exe + register: hyperv_vm + changed_when: hyperv_vm.stdout | trim | bool + + # Common + - name: remove any provider specific build artifacts + file: + path: '{{ output_dir }}/{{ inventory_hostname }}/{{ item }}' + state: absent + loop: + - hyperv + - vbox + - qemu diff --git a/updates.yml b/updates.yml deleted file mode 100644 index e69de29..0000000