diff --git a/ansible/playbook.yml b/ansible/playbook.yml index 7c89e00..5cf18b5 100644 --- a/ansible/playbook.yml +++ b/ansible/playbook.yml @@ -1,5 +1,6 @@ --- - hosts: all gather_facts: false + become: true roles: - os diff --git a/scripts/Update-Manifest.ps1 b/scripts/Update-Manifest.ps1 new file mode 100644 index 0000000..03e3ff8 --- /dev/null +++ b/scripts/Update-Manifest.ps1 @@ -0,0 +1,55 @@ +#Requires -Modules 'powershell-yaml' +[CmdletBinding()] +Param( + [Parameter(Mandatory)] + [ValidateScript({ + If (Test-Path($_)) { + $True + } Else { + Throw "'$_' is not a valid filename (within working directory '$PWD'), or access denied; aborting." + } + })] + [string]$ManifestFileName +) + +$GetItemSplat = @{ + Path = $ManifestFileName +} +$ManifestFile = Get-Item @GetItemSplat + +$SetLocationSplat = @{ + Path = $ManifestFile.DirectoryName +} +Set-Location @SetLocationSplat + +$GetContentSplat = @{ + Path = $ManifestFile.FullName +} +$Manifest = Get-Content @GetContentSplat + +$UpdatedManifest = ForEach ($Line in $Manifest) { + Write-Host "Processing '$($Line)' ..." + If ($Line -match '^SHA256\((.+)\)= ([0-9a-fA-F]{64})$') { + If (Test-Path $Matches[1]) { + $GetFileHashSplat = @{ + Path = $Matches[1] + Algorithm = 'SHA256' + } + Write-Host "Updating checksum..." + "SHA256($($Matches[1]))= $((Get-FileHash @GetFileHashSplat).Hash)" + } + } +} + +If ($UpdatedManifest -ne $Null) { + $SetContentSplat = @{ + Path = $ManifestFile.FullName + Value = $UpdatedManifest + Force = $True + Confirm = $False + } + Set-Content @SetContentSplat +} Else { + Write-Host "Failed updating manifest." + Exit 1 +} diff --git a/scripts/Update-OvfConfiguration.ps1 b/scripts/Update-OvfConfiguration.ps1 new file mode 100644 index 0000000..4bef5ae --- /dev/null +++ b/scripts/Update-OvfConfiguration.ps1 @@ -0,0 +1,312 @@ +#Requires -Modules 'powershell-yaml' +[CmdletBinding()] +Param( + [Parameter(Mandatory)] + [ValidateScript({ + If (Test-Path($_)) { + $True + } Else { + Throw "'$_' is not a valid filename (within working directory '$PWD'), or access denied; aborting." + } + })] + [string]$OVFFile, + [hashtable]$Parameter +) + +$GetContentSplat = @{ + Path = "$($PSScriptRoot)\$($MyInvocation.MyCommand)".Replace('.ps1', ".yml") + Raw = $True +} +$RawContent = Get-Content @GetContentSplat +$ConvertFromYamlSplat = @{ + Yaml = $RawContent + AllDocuments = $True +} +$YamlDocuments = ConvertFrom-Yaml @ConvertFromYamlSplat + +# Check if the respective .yml file declared substitutions which need to be parsed +If (($YamlDocuments.Count -gt 1) -and $YamlDocuments[-1].Variables) { + ForEach ($Pattern in $YamlDocuments[-1].Variables) { + $RawContent = $RawContent -replace "\{\{ ($($Pattern.Name)) \}\}", [string](Invoke-Expression -Command $Pattern.Expression) + } + # Perform conversion to Yaml again, now with parsed file contents + $ConvertFromYamlSplat = @{ + Yaml = $RawContent + AllDocuments = $True + } + $YamlDocuments = ConvertFrom-Yaml @ConvertFromYamlSplat + $OVFConfig = $YamlDocuments[0..($YamlDocuments.Count - 2)] +} +Else { + $OVFConfig = $YamlDocuments +} + +$SourceFile = Get-Item -Path $OVFFile +$GetContentSplat = @{ + Path = $SourceFile.FullName +} +$XML = [xml](Get-Content @GetContentSplat) +$NS = [System.Xml.XmlNamespaceManager]$XML.NameTable +[void]$NS.AddNamespace('ns', $XML.DocumentElement.xmlns) +[void]$NS.AddNamespace('ovf', $XML.DocumentElement.ovf) +[void]$NS.AddNamespace('rasd', $XML.DocumentElement.rasd) +[void]$NS.AddNamespace('vmw', $XML.DocumentElement.vmw) + +# Create copy of existing 'Item/ResourceType'=17 (=Hard disk) node +$XMLDiskTemplate = $XML.SelectSingleNode("//ns:VirtualHardwareSection/ns:Item/rasd:ResourceType[.='17']", $NS).ParentNode.CloneNode($True) + +ForEach ($Disk in $OVFConfig.DynamicDisks) { + # Determine next free available 'diskId' + $XMLDisks = $XML.SelectNodes("//ns:DiskSection/ns:Disk[contains(@ovf:diskId,'vmdisk')]", $NS) + $DiskId = 1 + While ($XMLDisks.DiskId -contains "vmdisk$($DiskId)") { + $DiskId++ + } + + # Add new 'Disk' node (under 'DiskSection') + $XMLDisk = $XML.CreateElement('Disk', $XML.DocumentElement.xmlns) + $PowersMap = @{ + KB = 10 + MB = 20 + GB = 30 + TB = 40 + PB = 50 + } + If ($PowersMap.Keys -notcontains $Disk.UnitSize) { + # Invalid UnitSize; skipping adding new disk + Continue + } + + [void]$XMLDisk.SetAttribute('capacityAllocationUnits', $NS.LookupNamespace('ovf'), "byte * 2^$($PowersMap[$Disk.UnitSize])") + [void]$XMLDisk.SetAttribute('format', $NS.LookupNamespace('ovf'), 'http://www.vmware.com/interfaces/specifications/vmdk.html#streamOptimized') + [void]$XMLDisk.SetAttribute('diskId', $NS.LookupNamespace('ovf'), "vmdisk$($DiskId)") + [void]$XMLDisk.SetAttribute('capacity', $NS.LookupNamespace('ovf'), '${{vmconfig.disksize.{0}}}' -f $DiskId) + [void]$XMLDisk.SetAttribute('populatedSize', $NS.LookupNamespace('ovf'), 0) + [void]$XML.SelectSingleNode('//ns:DiskSection', $NS).AppendChild($XMLDisk) + + # Add new 'Item/ResourceType' node (under 'VirtualHardwareSection') + $XMLDiskItem = $XMLDiskTemplate.CloneNode($True) + $XMLDiskItem.SelectSingleNode('rasd:AddressOnParent', $NS).InnerText = ($DiskId - 1) + $XMLDiskItem.SelectSingleNode('rasd:ElementName', $NS).InnerText = "Hard Disk $($DiskId)" + $XMLDiskItem.SelectSingleNode('rasd:HostResource', $NS).InnerText = "ovf:/disk/vmdisk$($DiskId)" + # Determine next free available and highest 'InstanceID' + $InstanceIDs = $XML.SelectNodes('//ns:VirtualHardwareSection/ns:Item/rasd:InstanceID', $NS).InnerText + $InstanceID = 1 + While ($InstanceIDs -contains $InstanceID) { + $InstanceID++ + } + $HighestInstanceID = ($InstanceIDs | Measure-Object -Maximum).Maximum + $XMLDiskItem.SelectSingleNode('rasd:InstanceID', $NS).InnerText = $InstanceID + [void]$XML.SelectSingleNode('//ns:VirtualHardwareSection', $NS).InsertAfter( + $XMLDiskItem, + $XML.SelectSingleNode("//ns:VirtualHardwareSection/ns:Item/rasd:InstanceID[.='$($HighestInstanceID)']", $NS).ParentNode + ) + + $OVFConfig.PropertyCategories[0].ProductProperties += @{ + Key = "vmconfig.disksize.$($DiskId)" + Type = If ([boolean]$Disk.Constraints.Minimum -or [boolean]$Disk.Constraints.Maximum) { + "Int($($Disk.Constraints.Minimum)..$($Disk.Constraints.Maximum))" + } + Else { + 'Int' + } + Label = "Disk $($DiskId) size*" + Description = "$($Disk.Description) (in $($Disk.UnitSize))".Trim() + DefaultValue = "$($Disk.Constraints.Minimum)" + Configurations = '*' + UserConfigurable = 'true' + } +} +Write-Host "Inserted $($OVFConfig.DynamicDisks.Count) new node(s) into 'DiskSection' and 'VirtualHardwareSection' respectively" + +If ($OVFConfig.DeploymentConfigurations.Count -gt 0) { + $XMLSection = $XML.CreateElement('DeploymentOptionSection', $XML.DocumentElement.xmlns) + $XMLSectionInfo = $XML.CreateElement('Info', $XML.DocumentElement.xmlns) + $XMLSectionInfo.InnerText = 'Deployment Type' + [void]$XMLSection.AppendChild($XMLSectionInfo) + + ForEach ($Configuration in $OVFConfig.DeploymentConfigurations) { + $XMLConfig = $XML.CreateElement('Configuration', $XML.DocumentElement.xmlns) + + [void]$XMLConfig.SetAttribute('id', $NS.LookupNamespace('ovf'), $Configuration.Id) + + $XMLConfigLabel = $XML.CreateElement('Label', $XML.DocumentElement.xmlns) + $XMLConfigLabel.InnerText = $Configuration.Label + $XMLConfigDescription = $XML.CreateElement('Description', $XML.DocumentElement.xmlns) + $XMLConfigDescription.InnerText = $Configuration.Description + + [void]$XMLConfig.AppendChild($XMLConfigLabel) + [void]$XMLConfig.AppendChild($XMLConfigDescription) + + [void]$XMLSection.AppendChild($XMLConfig) + } + [void]$XML.SelectSingleNode('//ns:Envelope', $NS).InsertAfter($XMLSection, $XML.SelectSingleNode('//ns:NetworkSection', $NS)) + Write-Host "Inserted 'DeploymentOptionSection' with $($Configuration.Count) nodes" + + If ($OVFConfig.DeploymentConfigurations.Count -eq $OVFConfig.DeploymentConfigurations.Size.Count) { + # Create copies of existing 'Item/ResourceType' nodes + $XMLCPUTemplate = $XML.SelectSingleNode("//ns:VirtualHardwareSection/ns:Item/rasd:ResourceType[.='3']", $NS).ParentNode.CloneNode($True) + $XMLMemoryTemplate = $XML.SelectSingleNode("//ns:VirtualHardwareSection/ns:Item/rasd:ResourceType[.='4']", $NS).ParentNode.CloneNode($True) + # Delete existing nodes + ForEach ($Node in $XML.SelectNodes("//ns:VirtualHardwareSection/ns:Item/rasd:ResourceType[.='3' or .='4']", $NS).ParentNode) { + [void]$Node.ParentNode.RemoveChild($Node) + } + # Add adjusted 'Item/ResourceType' nodes + ForEach ($Configuration in $OVFConfig.DeploymentConfigurations) { + $XMLCPU = $XMLCPUTemplate.CloneNode($True) + [void]$XMLCPU.SetAttribute('configuration', $NS.LookupNamespace('ovf'), $Configuration.Id) + $XMLCPU.SelectSingleNode('rasd:ElementName', $NS).InnerText = '{0} virtual CPU(s)' -f $Configuration.Size.CPU + $XMLCPU.SelectSingleNode('rasd:VirtualQuantity', $NS).InnerText = $Configuration.Size.CPU + + $XMLMemory = $XMLMemoryTemplate.CloneNode($True) + [void]$XMLMemory.SetAttribute('configuration', $NS.LookupNamespace('ovf'), $Configuration.Id) + $XMLMemory.SelectSingleNode('rasd:ElementName', $NS).InnerText = '{0}MB of memory' -f $Configuration.Size.Memory + $XMLMemory.SelectSingleNode('rasd:VirtualQuantity', $NS).InnerText = $Configuration.Size.Memory + + [void]$XML.SelectSingleNode('//ns:VirtualHardwareSection', $NS).InsertAfter( + $XMLCPU, + $XML.SelectSingleNode('//ns:VirtualHardwareSection/ns:System', $NS) + ) + [void]$XML.SelectSingleNode('//ns:VirtualHardwareSection', $NS).InsertAfter( + $XMLMemory, + $XML.SelectSingleNode('//ns:VirtualHardwareSection/ns:System', $NS) + ) + } + } +} + +[void]$XML.SelectSingleNode('//ns:VirtualHardwareSection', $NS).SetAttribute('transport', $NS.LookupNamespace('ovf'), 'com.vmware.guestInfo') +ForEach ($ExtraConfig in $OVFConfig.AdvancedOptions) { + $XMLExtraConfig = $XML.CreateElement('vmw:ExtraConfig', $XML.DocumentElement.vmw) + + [void]$XMLExtraConfig.SetAttribute('required', $NS.LookupNamespace('ovf'), "$([boolean]$ExtraConfig.Required)".ToLower()) + [void]$XMLExtraConfig.SetAttribute('key', $NS.LookupNamespace('vmw'), $ExtraConfig.Key) + [void]$XMLExtraConfig.SetAttribute('value', $NS.LookupNamespace('vmw'), $ExtraConfig.Value) + + [void]$XML.SelectSingleNode('//ns:VirtualHardwareSection', $NS).AppendChild($XMLExtraConfig) +} +Write-Host "Added $($OVFConfig.AdvancedOptions.Count) 'vmw:ExtraConfig' node(s)" + +$XMLProductSection = $XML.SelectSingleNode('//ns:ProductSection', $NS) +If ($XMLProductSection -eq $Null) { + $XMLProductSection = $XML.CreateElement('ProductSection', $XML.DocumentElement.xmlns) + [void]$XML.SelectSingleNode('//ns:VirtualSystem', $NS).AppendChild($XMLProductSection) + Write-Host "Inserted 'ProductSection'" +} Else { + ForEach ($Child in $XMLProductSection.SelectNodes('//ns:ProductSection/child::*', $NS)) { + [void]$Child.ParentNode.RemoveChild($Child) + } + Write-Host "Destroyed pre-existing children in 'ProductSection'" +} +$XMLProductSectionInfo = $XML.CreateElement('Info', $XML.DocumentElement.xmlns) +$XMLProductSectionInfo.InnerText = 'Information about the installed software' +[void]$XMLProductSection.AppendChild($XMLProductSectionInfo) +Write-Host "Inserted new 'Info' into 'ProductSection'" + +ForEach ($Category in $OVFConfig.PropertyCategories) { + If ($Category.Name -ne '') { + $XMLCategory = $XML.CreateElement('Category', $XML.DocumentElement.xmlns) + $XMLCategory.InnerText = $Category.Name + [void]$XMLProductSection.AppendChild($XMLCategory) + Write-Host "Inserted new 'Category' into 'ProductSection'" + } + + ForEach ($Property in $Category.ProductProperties) { + $XMLProperty = $XML.CreateElement('Property', $XML.DocumentElement.xmlns) + + [void]$XMLProperty.SetAttribute('key', $NS.LookupNamespace('ovf'), $Property.Key) + Switch -regex ($Property.Type) { + '^boolean' { + [void]$XMLProperty.SetAttribute('type', $NS.LookupNamespace('ovf'), 'boolean') + } + '^int' { + [void]$XMLProperty.SetAttribute('type', $NS.LookupNamespace('ovf'), 'uint16') + $Qualifiers = @() + If ($Property.Type -match '^int\((\d*)\.\.(\d*)\)') { + If ($Matches[1]) { + $Qualifiers += "MinValue($($Matches[1]))" + } + If ($Matches[2]) { + $Qualifiers += "MaxValue($($Matches[2]))" + } + [void]$XMLProperty.SetAttribute('qualifiers', $NS.LookupNamespace('ovf'), $Qualifiers -join ' ') + } + } + '^ip' { + [void]$XMLProperty.SetAttribute('type', $NS.LookupNamespace('ovf'), 'string') + [void]$XMLProperty.SetAttribute('qualifiers', $NS.LookupNamespace('vmw'), 'Ip') + } + '^password' { + [void]$XMLProperty.SetAttribute('type', $NS.LookupNamespace('ovf'), 'string') + [void]$XMLProperty.SetAttribute('password', $NS.LookupNamespace('ovf'), 'true') + $Qualifiers = @() + If ($Property.Type -match '^password\((\d*)\.\.(\d*)\)') { + If ($Matches[1]) { + $Qualifiers += "MinLen($($Matches[1]))" + } + If ($Matches[2]) { + $Qualifiers += "MaxLen($($Matches[2]))" + } + [void]$XMLProperty.SetAttribute('qualifiers', $NS.LookupNamespace('ovf'), $Qualifiers -join ' ') + } + } + '^string' { + [void]$XMLProperty.SetAttribute('type', $NS.LookupNamespace('ovf'), 'string') + $Qualifiers = @() + If ($Property.Type -match '^string\((\d*)\.\.(\d*)\)') { + If ($Matches[1]) { + $Qualifiers += "MinLen($($Matches[1]))" + } + If ($Matches[2]) { + $Qualifiers += "MaxLen($($Matches[2]))" + } + [void]$XMLProperty.SetAttribute('qualifiers', $NS.LookupNamespace('ovf'), $Qualifiers -join ' ') + } ElseIf ($Property.Type -match '^string\[(.*)\]') { + [void]$XMLProperty.SetAttribute('qualifiers', $NS.LookupNamespace('ovf'), "ValueMap{$($Matches[1] -replace '","', '", "')}") + } + } + } + [void]$XMLProperty.SetAttribute('userConfigurable', $NS.LookupNamespace('ovf'), "$([boolean]$Property.UserConfigurable)".ToLower()) + + If ($Property.Type -eq 'boolean') { + [void]$XMLProperty.SetAttribute('value', $NS.LookupNamespace('ovf'), "$([boolean]$Property.DefaultValue)".ToLower()) + } Else { + [void]$XMLProperty.SetAttribute('value', $NS.LookupNamespace('ovf'), $Property.DefaultValue) + } + + If ($Property.Label) { + $XMLPropertyLabel = $XML.CreateElement('Label', $XML.DocumentElement.xmlns) + $XMLPropertyLabel.InnerText = $Property.Label + [void]$XMLProperty.AppendChild($XMLPropertyLabel) + } + If ($Property.Description) { + $XMLPropertyDescription = $XML.CreateElement('Description', $XML.DocumentElement.xmlns) + $XMLPropertyDescription.InnerText = $Property.Description + [void]$XMLProperty.AppendChild($XMLPropertyDescription) + } + + If (($Property.Configurations.Count -eq 1) -and ($Property.Configurations -eq '*')) { + [void]$XMLProperty.SetAttribute('configuration', $NS.LookupNamespace('ovf'), $OVFConfig.DeploymentConfigurations.Id -join ' ') + } ElseIf ($Property.Configurations.Count -gt 0) { + [void]$XMLProperty.SetAttribute('configuration', $NS.LookupNamespace('ovf'), $Property.Configurations -join ' ') + } + + If ($Property.Value.Count -eq 1) { + [void]$XMLProperty.SetAttribute('value', $NS.LookupNamespace('ovf'), $Property.Value) + } ElseIf ($Property.Value.Count -gt 1) { + ForEach ($Value in $Property.Value) { + $XMLValue = $XML.CreateElement('Value', $XML.DocumentElement.xmlns) + + [void]$XMLValue.SetAttribute('value', $NS.LookupNamespace('ovf'), $Value) + [void]$XMLValue.SetAttribute('configuration', $NS.LookupNamespace('ovf'), $Value) + + [void]$XMLProperty.AppendChild($XMLValue) + } + } + + [void]$XMLProductSection.AppendChild($XMLProperty) + } + Write-Host "Inserted $($Category.ProductProperties.Count) new node(s) into 'ProductSection'" +} + +$XML.Save($SourceFile.FullName) \ No newline at end of file diff --git a/scripts/Update-OvfConfiguration.yml b/scripts/Update-OvfConfiguration.yml new file mode 100644 index 0000000..0aca639 --- /dev/null +++ b/scripts/Update-OvfConfiguration.yml @@ -0,0 +1,96 @@ +DeploymentConfigurations: +- Id: ubuntu-small + Label: 'Ubuntu Server 20.04 [SMALL]' + Description: | + Ubuntu Server 20.04 + 1 vCPU/2GB RAM + Size: + CPU: 1 + Memory: 2048 +- Id: ubuntu-large + Label: 'Ubuntu Server 20.04 [LARGE]' + Description: | + Ubuntu Server 20.04 + 4 vCPU/8GB RAM + Size: + CPU: 4 + Memory: 8192 +DynamicDisks: [] +PropertyCategories: +- Name: 0) Deployment information + ProductProperties: + - Key: deployment.type + Type: string + Value: + - ubuntu-small + - ubuntu-large + UserConfigurable: false +- Name: 1) Operating System + ProductProperties: + - Key: guestinfo.hostname + Type: string(1..15) + Label: Hostname* + Description: '(max length: 15 characters)' + DefaultValue: '' + Configurations: '*' + UserConfigurable: true + - Key: guestinfo.rootpw + Type: password(7..) + Label: Local root password* + Description: '' + DefaultValue: password + Configurations: '*' + UserConfigurable: true + - Key: guestinfo.ntpserver + Type: string(1..) + Label: Time server* + Description: A comma-separated list of timeservers + DefaultValue: 0.pool.ntp.org,1.pool.ntp.org,2.pool.ntp.org + Configurations: '*' + UserConfigurable: true +- Name: 2) Networking + ProductProperties: + - Key: guestinfo.ipaddress + Type: ip + Label: IP Address* + Description: '' + DefaultValue: '' + Configurations: '*' + UserConfigurable: true + - Key: guestinfo.prefixlength + Type: int(8..32) + Label: Subnet prefix length* + Description: '' + DefaultValue: '24' + Configurations: '*' + UserConfigurable: true + - Key: guestinfo.dnsserver + Type: ip + Label: DNS server* + Description: '' + DefaultValue: '' + Configurations: '*' + UserConfigurable: true + - Key: guestinfo.gateway + Type: ip + Label: Gateway* + Description: '' + DefaultValue: '' + Configurations: '*' + UserConfigurable: true +AdvancedOptions: +- Key: appliance.name + Value: "{{ appliance.name }}" + Required: false +- Key: appliance.version + Value: "{{ appliance.version }}" + Required: false + +--- +Variables: +- Name: appliance.name + Expression: | + $Parameter['appliance.name'] +- Name: appliance.version + Expression: | + $Parameter['appliance.version']