From 1ed68eeab2f4d2022e3caf37e971792037d448f3 Mon Sep 17 00:00:00 2001 From: mycloudrevolution Date: Fri, 1 Sep 2017 15:45:18 +0200 Subject: [PATCH] New Module VMware-vCD-TenantReport This Module Creates a nice HTML Report for vCD Customers. Report Contains Objects like Users, Catalogs, VDCs, vApps, VMs, Edge Gateways --- Modules/VMware-vCD-TenantReport/README.md | 30 +++ .../VMware-vCD-TenantReport.psd1 | 122 +++++++++ .../functions/Get-VcdTenantReport.psm1 | 251 ++++++++++++++++++ .../media/Get-VcdTenantReport.png | Bin 0 -> 6197 bytes .../PowerStartHTML/PowerStartHTML.psd1 | Bin 0 -> 7874 bytes .../PowerStartHTML/PowerStartHTML.psm1 | 237 +++++++++++++++++ .../tests/VMware-vCD-TenantReport.Tests.ps1 | 24 ++ 7 files changed, 664 insertions(+) create mode 100644 Modules/VMware-vCD-TenantReport/README.md create mode 100644 Modules/VMware-vCD-TenantReport/VMware-vCD-TenantReport.psd1 create mode 100644 Modules/VMware-vCD-TenantReport/functions/Get-VcdTenantReport.psm1 create mode 100644 Modules/VMware-vCD-TenantReport/media/Get-VcdTenantReport.png create mode 100644 Modules/VMware-vCD-TenantReport/modules/PowerStartHTML/PowerStartHTML.psd1 create mode 100644 Modules/VMware-vCD-TenantReport/modules/PowerStartHTML/PowerStartHTML.psm1 create mode 100644 Modules/VMware-vCD-TenantReport/tests/VMware-vCD-TenantReport.Tests.ps1 diff --git a/Modules/VMware-vCD-TenantReport/README.md b/Modules/VMware-vCD-TenantReport/README.md new file mode 100644 index 0000000..ac9b9f8 --- /dev/null +++ b/Modules/VMware-vCD-TenantReport/README.md @@ -0,0 +1,30 @@ +VMware-vCD-TenantReport PowerShell Module +============= + +# About + +## Project Owner: + +Markus Kraus [@vMarkus_K](https://twitter.com/vMarkus_K) + +MY CLOUD-(R)EVOLUTION [mycloudrevolution.com](http://mycloudrevolution.com/) + +## Project WebSite: + +[mycloudrevolution.com](http://mycloudrevolution.com/) + +## Project Documentation: + +[Read the Docs](http://readthedocs.io/) + +## Project Description: + +The 'VMware-vCD-TenantReport' PowerShell Module creates with the Fuction 'Get-VcdTenantReport' a HTML Report of your vCloud Director Objects. + +![Get-VcdTenantReport](/media/Get-VcdTenantReport.png) + +Big thanks to [Timothy Dewin](https://twitter.com/tdewin) for his great [PowerStartHTML](https://github.com/tdewin/randomsamples/tree/master/powerstarthtml) PowerShell Module which is used to generate the Report for this Module. + + + + diff --git a/Modules/VMware-vCD-TenantReport/VMware-vCD-TenantReport.psd1 b/Modules/VMware-vCD-TenantReport/VMware-vCD-TenantReport.psd1 new file mode 100644 index 0000000..32afbb5 --- /dev/null +++ b/Modules/VMware-vCD-TenantReport/VMware-vCD-TenantReport.psd1 @@ -0,0 +1,122 @@ +# +# Modulmanifest für das Modul "VMware-vCD-TenantReport" +# +# Generiert von: Markus Kraus +# +# Generiert am: 8/22/2017 +# + +@{ + +# Die diesem Manifest zugeordnete Skript- oder Binärmoduldatei. +# RootModule = 'VMware-vCD-TenantReport.psm1' + +# Die Versionsnummer dieses Moduls +ModuleVersion = '1.0.2' + +# ID zur eindeutigen Kennzeichnung dieses Moduls +GUID = '21a71eaa-d259-48c5-8482-643ba152af76' + +# Autor dieses Moduls +Author = 'Markus' + +# Unternehmen oder Hersteller dieses Moduls +CompanyName = 'mycloudrevolution.com' + +# Urheberrechtserklärung für dieses Modul +Copyright = '(c) 2017 Markus Kraus. Alle Rechte vorbehalten.' + +# Beschreibung der von diesem Modul bereitgestellten Funktionen +# Description = '' + +# Die für dieses Modul mindestens erforderliche Version des Windows PowerShell-Moduls +# PowerShellVersion = '' + +# Der Name des für dieses Modul erforderlichen Windows PowerShell-Hosts +# PowerShellHostName = '' + +# Die für dieses Modul mindestens erforderliche Version des Windows PowerShell-Hosts +# PowerShellHostVersion = '' + +# Die für dieses Modul mindestens erforderliche Microsoft .NET Framework-Version +# DotNetFrameworkVersion = '' + +# Die für dieses Modul mindestens erforderliche Version der CLR (Common Language Runtime) +# CLRVersion = '' + +# Die für dieses Modul erforderliche Prozessorarchitektur ("Keine", "X86", "Amd64"). +# ProcessorArchitecture = '' + +# Die Module, die vor dem Importieren dieses Moduls in die globale Umgebung geladen werden müssen +# RequiredModules = @() + +# Die Assemblys, die vor dem Importieren dieses Moduls geladen werden müssen +# RequiredAssemblies = @() + +# Die Skriptdateien (PS1-Dateien), die vor dem Importieren dieses Moduls in der Umgebung des Aufrufers ausgeführt werden. +# ScriptsToProcess = @() + +# Die Typdateien (.ps1xml), die beim Importieren dieses Moduls geladen werden sollen +# TypesToProcess = @() + +# Die Formatdateien (.ps1xml), die beim Importieren dieses Moduls geladen werden sollen +# FormatsToProcess = @() + +# Die Module, die als geschachtelte Module des in "RootModule/ModuleToProcess" angegebenen Moduls importiert werden sollen. +NestedModules = @( "modules/PowerStartHTML/PowerStartHTML.psd1", + "functions/Get-VcdTenantReport.psm1" ) + +# Aus diesem Modul zu exportierende Funktionen +FunctionsToExport = '*' + +# Aus diesem Modul zu exportierende Cmdlets +CmdletsToExport = '*' + +# Die aus diesem Modul zu exportierenden Variablen +VariablesToExport = '*' + +# Aus diesem Modul zu exportierende Aliase +AliasesToExport = '*' + +# Aus diesem Modul zu exportierende DSC-Ressourcen +# DscResourcesToExport = @() + +# Liste aller Module in diesem Modulpaket +# ModuleList = @() + +# Liste aller Dateien in diesem Modulpaket +# FileList = @() + +# Die privaten Daten, die an das in "RootModule/ModuleToProcess" angegebene Modul übergeben werden sollen. Diese können auch eine PSData-Hashtabelle mit zusätzlichen von PowerShell verwendeten Modulmetadaten enthalten. +PrivateData = @{ + + PSData = @{ + + # 'Tags' wurde auf das Modul angewendet und unterstützt die Modulermittlung in Onlinekatalogen. + # Tags = @() + + # Eine URL zur Lizenz für dieses Modul. + # LicenseUri = '' + + # Eine URL zur Hauptwebsite für dieses Projekt. + # ProjectUri = '' + + # Eine URL zu einem Symbol, das das Modul darstellt. + # IconUri = '' + + # 'ReleaseNotes' des Moduls + # ReleaseNotes = '' + + } # Ende der PSData-Hashtabelle + +} # Ende der PrivateData-Hashtabelle + +# HelpInfo-URI dieses Moduls +# HelpInfoURI = '' + +# Standardpräfix für Befehle, die aus diesem Modul exportiert werden. Das Standardpräfix kann mit "Import-Module -Prefix" überschrieben werden. +# DefaultCommandPrefix = '' + +} + + diff --git a/Modules/VMware-vCD-TenantReport/functions/Get-VcdTenantReport.psm1 b/Modules/VMware-vCD-TenantReport/functions/Get-VcdTenantReport.psm1 new file mode 100644 index 0000000..ee1296e --- /dev/null +++ b/Modules/VMware-vCD-TenantReport/functions/Get-VcdTenantReport.psm1 @@ -0,0 +1,251 @@ +function Get-VcdTenantReport { +<# + .NOTES + =========================================================================== + Created by: Markus Kraus + Twitter: @VMarkus_K + Private Blog: mycloudrevolution.com + =========================================================================== + Changelog: + 1.0.0 - Inital Release + 1.0.1 - Removed "Test-IP" Module + 1.0.2 - More Detailed Console Log + =========================================================================== + External Code Sources: + Examle Usage of BOOTSTRAP with PowerShell + https://github.com/tdewin/randomsamples/tree/master/powershell-veeamallstat + BOOTSTRAP with PowerShell + https://github.com/tdewin/randomsamples/tree/master/powerstarthtml + =========================================================================== + Tested Against Environment: + vCD Version: 8.20 + PowerCLI Version: PowerCLI 6.5.1 + PowerShell Version: 5.0 + OS Version: Windows 8.1 + Keyword: VMware, vCD, Report, HTML + =========================================================================== + + .DESCRIPTION + This Function creates a HTML Report for your vCloud Director Organization. + + This Function is fully tested as Organization Administrator. + With lower permissions a unexpected behavior is possible. + + .Example + Get-VcdTenantReport -Server $ServerFQDN -Org $OrgName -Credential $MyCedential + + .Example + Get-VcdTenantReport -Server $ServerFQDN -Org $OrgName -Path "C:\Temp\Report.html" + + .PARAMETER Server + The FQDN of your vCloud Director Endpoint. + + .PARAMETER Org + The Organization Name. + + .PARAMETER Credential + PowerShell Credentials to access the Eénvironment. + + .PARAMETER Path + The Path of the exported HTML Report. + +#> +#Requires -Version 5 +#Requires -Modules VMware.VimAutomation.Cloud, @{ModuleName="VMware.VimAutomation.Cloud";ModuleVersion="6.5.1.0"} + +[CmdletBinding()] +param( + [Parameter(Mandatory=$True, ValueFromPipeline=$False)] + [ValidateNotNullorEmpty()] + [String] $Server, + [Parameter(Mandatory=$True, ValueFromPipeline=$False)] + [ValidateNotNullorEmpty()] + [String] $Org, + [Parameter(Mandatory=$False, ValueFromPipeline=$False)] + [ValidateNotNullorEmpty()] + [PSCredential] $Credential, + [Parameter(Mandatory=$false, ValueFromPipeline=$False)] + [ValidateNotNullorEmpty()] + [String] $Path = ".\Report.html" + +) + +Process { + + # Start Connection to vCD + + if ($global:DefaultCIServers) { + "$(Get-Date -Format "yyyy-MM-dd HH:mm:ss") - Disconnect existing vCD Server ..." + $Trash = Disconnect-CIServer -Server * -Force:$true -Confirm:$false -ErrorAction SilentlyContinue + } + + "$(Get-Date -Format "yyyy-MM-dd HH:mm:ss") - Connect vCD Server ..." + if ($Credential) { + $Trash = Connect-CIServer -Server $Server -Org $Org -Credential $Credential -ErrorAction Stop + } + else { + $Trash = Connect-CIServer -Server $Server -Org $Org -ErrorAction Stop + } + "$(Get-Date -Format "yyyy-MM-dd HH:mm:ss") - Create HTML Report..." + + # Init HTML Report + $ps = New-PowerStartHTML -title "vCD Tenant Report" + + #Set CSS Style + $ps.cssStyles['.bgtitle'] = "background-color:grey" + $ps.cssStyles['.bgsubsection'] = "background-color:#eee;" + + # Processing Data + ## Get Main Objects + [Array] $OrgVdcs = Get-OrgVdc + [Array] $Catalogs = Get-Catalog + [Array] $Users = Get-CIUser + + ## Add Header to Report + $ps.Main().Add("div","jumbotron").N() + $ps.Append("h1","display-3",("vCD Tenant Report" -f $OrgVdcs.Count)).Append("p","lead","Organization User Count: {0}" -f $Users.Count).Append("p","lead","Organization Catalog Count: {0}" -f $Catalogs.Count).Append("p","lead","Organization VDC Count: {0}" -f $OrgVdcs.Count).Append("hr","my-4").Append("p","font-italic","This Report lists the most important objects in your vCD Environmet. For more details contact your Service Provider").N() + + ## add Org Users to Report + $ps.Main().Append("h2",$null,"Org Users").N() + + $ps.Add('table','table').Add("tr","bgtitle text-white").Append("th",$null,"User Name").Append("th",$null,"Locked").Append("th",$null,"DeployedVMCount").Append("th",$null,"StoredVMCount").N() + $ps.Add("tr").N() + + foreach ($User in $Users) { + $ps.Append("td",$null,$User.Name).N() + $ps.Append("td",$null,$User.Locked).N() + $ps.Append("td",$null,$User.DeployedVMCount).N() + $ps.Append("td",$null,$User.StoredVMCount).N() + $ps.Up().N() + + } + $ps.Up().N() + + ## add Org Catalogs to Report + $ps.Main().Append("h2",$null,"Org Catalogs").N() + + foreach ($Catalog in $Catalogs) { + $ps.Add('table','table').Add("tr","bgtitle text-white").Append("th",$null,"Catalog Name").N() + $ps.Add("tr").N() + $ps.Append("td",$null,$Catalog.Name).Up().N() + + $ps.Add("td","bgsubsection").N() + $ps.Add("table","table bgcolorsub").N() + $ps.Add("tr").N() + + $headers = @("Item") + foreach ($h in $headers) { + $ps.Append("th",$null,$h).N() + } + $ps.Up().N() + + ### add Itens of the Catalog to the Report + [Array] $Items = $Catalog.ExtensionData.CatalogItems.CatalogItem + + foreach ($Item in $Items) { + $ps.Add("tr").N() + $ps.Append("td",$null,$Item.Name).N() + + $ps.Up().N() + + } + + $ps.Up().Up().N() + } + $ps.Up().N() + + ## add Org VDC`s to the Report + $ps.Main().Append("h2",$null,"Org VDCs").N() + + foreach ($OrgVdc in $OrgVdcs) { + $ps.Main().Add('table','table table-striped table-inverse').Add("tr").Append("th",$null,"VDC Name").Append("th",$null,"Enabled").Append("th",$null,"CpuUsedGHz").Append("th",$null,"MemoryUsedGB").Append("th",$null,"StorageUsedGB").Up().N() + $ps.Add("tr").N() + $ps.Append("td",$null,$OrgVdc.Name).Append("td",$null,$OrgVdc.Enabled).Append("td",$null,$OrgVdc.CpuUsedGHz).Append("td",$null,$OrgVdc.MemoryUsedGB).Append("td",$null,[Math]::Round($OrgVdc.StorageUsedGB,2)).Up().N() + + ### add Edge Gateways of this Org VDC to Report + $ps.Main().Append("h3",$null,"Org VDC Edge Gateways").N() + [Array] $Edges = Search-Cloud -QueryType EdgeGateway -Filter "Vdc==$($OrgVdc.Id)" + + foreach ($Edge in $Edges) { + $ps.Add('table','table').Add("tr","bgtitle text-white").Append("th",$null,"Edge Name").N() + $ps.Add("tr").N() + $ps.Append("td",$null,$Edge.Name).Up().N() + + $ps.Add("td","bgsubsection").N() + $ps.Add("table","table bgcolorsub").N() + $ps.Append("tr").Append("td","font-weight-bold","HaStatus").Append("td",$null,($Edge.HaStatus)).N() + $ps.Append("td","font-weight-bold","AdvancedNetworkingEnabled").Append("td",$null,$Edge.AdvancedNetworkingEnabled).N() + $ps.Append("tr").Append("td","font-weight-bold","NumberOfExtNetworks").Append("td",$null,($Edge.NumberOfExtNetworks)).N() + $ps.Append("td","font-weight-bold","NumberOfOrgNetworks").Append("td",$null,$Edge.NumberOfOrgNetworks).N() + + $ps.Up().Up().N() + } + $ps.Up().N() + + ### add Org Networks of this Org VDC to Report + $ps.Main().Append("h3",$null,"Org VDC Networks").N() + [Array] $Networks = $OrgVdc | Get-OrgVdcNetwork + + foreach ($Network in $Networks) { + $ps.Add('table','table').Add("tr","bgtitle text-white").Append("th",$null,"Network Name").N() + $ps.Add("tr").N() + $ps.Append("td",$null,$Network.Name).Up().N() + + $ps.Add("td","bgsubsection").N() + $ps.Add("table","table bgcolorsub").N() + $ps.Append("tr").Append("td","font-weight-bold","DefaultGateway").Append("td",$null,($Network.DefaultGateway)).N() + $ps.Append("td","font-weight-bold","Netmask").Append("td",$null,$Network.Netmask).N() + $ps.Append("tr").Append("td","font-weight-bold","NetworkType").Append("td",$null,($Network.NetworkType)).N() + $ps.Append("td","font-weight-bold","StaticIPPool").Append("td",$null,$Network.StaticIPPool).N() + + $ps.Up().Up().N() + } + $ps.Up().N() + + ### add vApps of this Org VDC to Report + $ps.Main().Append("h3",$null,"Org VDC vApps").N() + + [Array] $Vapps = $OrgVdc | Get-CIVApp + + foreach ($Vapp in $Vapps) { + $ps.Add('table','table').Add("tr","bgtitle text-white").Append("th",$null,"vApp Name").Append("th",$null,"Owner").Up().N() + $ps.Add("tr").N() + $ps.Append("td",$null,$Vapp.Name).Append("td",$null,$Vapp.Owner).Up().N() + + #### add VMs of this vApp to Report + $ps.Add("td","bgsubsection").N() + $ps.Add("table","table bgcolorsub").N() + $ps.Add("tr").N() + + $headers = @("Name","Status","GuestOSFullName","CpuCount","MemoryGB") + foreach ($h in $headers) { + $ps.Append("th",$null,$h).N() + } + $ps.Up().N() + + [Array] $VMs = $Vapp | Get-CIVM + + foreach ($VM in $VMs) { + $ps.Add("tr").N() + $ps.Append("td",$null,$VM.Name).N() + $ps.Append("td",$null,$VM.Status).N() + $ps.Append("td",$null,$VM.GuestOSFullName).N() + $ps.Append("td",$null,$VM.CpuCount).N() + $ps.Append("td",$null,$VM.MemoryGB).N() + + $ps.Up().N() + + } + $ps.Up().Up().N() + + } + $ps.Up().N() + + } + $ps.save($Path) + + "$(Get-Date -Format "yyyy-MM-dd HH:mm:ss") - Open HTML Report..." + Start-Process $Path + +} +} diff --git a/Modules/VMware-vCD-TenantReport/media/Get-VcdTenantReport.png b/Modules/VMware-vCD-TenantReport/media/Get-VcdTenantReport.png new file mode 100644 index 0000000000000000000000000000000000000000..c42dacfdb845605ada5fa72b3bb898333356c5ed GIT binary patch literal 6197 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
" + [xml]$xmlDocument = $null + $onLoadJS = $null + $cssStyles= @{} + $lastEl = $null + $newEl = $null + $indentedOutput = $false + $bootstrapAtCompile = $false + PowerStartHTML([string]$title) { + $this.xmlDocument = $this.PowerStartHtmlTemplate + $this.xmlDocument.html.head.title = $title + $this.lastEl = $this.xmlDocument.html.body.ChildNodes[0] + $this.onLoadJS = New-Object System.Collections.Generic.List[System.String] + } + [string] GetHtml() { + $xmlclone = $this.xmlDocument.Clone() + $csb = [System.Text.StringBuilder]::new() + foreach ($cssStyle in $this.cssStyles.GetEnumerator()) { + $null = $csb.AppendFormat("{0} {{ {1} }}",$cssStyle.Name,$cssStyle.Value) + } + $this.xmlDocument.html.head.style = $csb.toString() + $this.AddBootStrapAtCompile() + if($this.onLoadJS.Count -gt 0) { + $this.onLoadJs.Insert(0,"`r`n`$(document).ready(function() {") + $this.onLoadJs.Add("})`r`n") + $el = $this.xmlDocument.CreateElement("script") + $el.AppendChild($this.xmlDocument.CreateTextNode([System.String]::Join("`r`n",$this.onLoadJs))) + $this.xmlDocument.html.body.AppendChild($el) + } + $ms = [System.IO.MemoryStream]::new() + $xmlWriter = [System.Xml.XmlTextWriter]::new($ms,[System.Text.Encoding]::UTF8) + if($this.indentedOutput) { + $xmlWriter.Formatting = [System.Xml.Formatting]::Indented + } + $this.xmlDocument.WriteContentTo($xmlWriter) + $xmlWriter.Flush() + $ms.Flush() + #make sure that everytime we do gethtml we keep it clean + $this.xmlDocument = $xmlclone + $ms.Position = 0; + $sr = [System.IO.StreamReader]::new($ms); + return ("{0}`r`n" -f $sr.ReadToEnd()) + } + Save($path) { + $this.GetHtml() | Set-Content -path $path -Encoding UTF8 + } + + AddAttr($el,$name,$value) { + $attr = $this.xmlDocument.CreateAttribute($name) + $attr.Value = $value + $el.Attributes.Append($attr) + } + + AddAttrs($el,$dict) { + foreach($a in $dict.GetEnumerator()) { + $this.AddAttr($el,$a.Name,$a.Value) + } + } + [PowerStartHTML] AddBootStrap() { + $this.bootstrapAtCompile = $true + return $this + } + AddJSScript($href,$integrity) { + $el = $this.xmlDocument.CreateElement("script") + $attrs = @{ + "src"="$href"; + "integrity"="$integrity"; + "crossorigin"="anonymous" + } + $this.AddAttrs($el,$attrs) + $el.AppendChild($this.xmlDocument.CreateTextNode("")) + $this.xmlDocument.html.body.AppendChild($el) + } + AddBootStrapAtCompile() { #Bootstrap script needs to be added at the end + if($this.bootstrapAtCompile) { + $el = $this.xmlDocument.CreateElement("link") + $attrs = @{ + "rel"="stylesheet"; + "href"='https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-beta/css/bootstrap.min.css'; + "integrity"="sha384-/Y6pD6FV/Vv2HJnA6t+vslU6fwYXjCFtcEpHbNJ0lyAFsXTsjBbfaDjzALeQsN6M"; + "crossorigin"="anonymous" + } + $this.AddAttrs($el,$attrs) + $el.AppendChild($this.xmlDocument.CreateTextNode("")) + $this.xmlDocument.html.head.AppendChild($el) + $this.AddJSScript('https://code.jquery.com/jquery-3.2.1.slim.min.js',"sha384-KJ3o2DKtIkvYIK3UENzmM7KCkRr/rE9/Qpg6aAZGJwFDMVNA/GpGFF93hXpG5KkN") + $this.AddJSScript('https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.11.0/umd/popper.min.js',"sha384-b/U6ypiBEHpOf/4+1nzFpr53nxSS+GLCkfwBdFNTxtclqqenISfwAzpKaMNFNmj4") + $this.AddJSScript('https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-beta/js/bootstrap.min.js',"sha384-h0AbiXch4ZDo7tp9hKZ4TsHbi047NrKGLO3SEJAg45jXxnGIfYzk4Si90RDIqNm1") + } + } + [PowerStartHTML] AddContainerAttrToMain() { + $this.AddAttr($this.xmlDocument.html.body.ChildNodes[0],"class","container") + return $this + } + [PowerStartHTML] Append($elType = "table",$className=$null,[string]$text=$null) { + $el = $this.xmlDocument.CreateElement($elType) + if($text -ne $null) { + $el.AppendChild($this.xmlDocument.CreateTextNode($text)) + } + if($className -ne $null) { + $this.AddAttr($el,"class",$className) + } + $this.lastEl.AppendChild($el) + $this.newEl = $el + + return $this + } + [PowerStartHTML] Append($elType = "table",$className=$null) { return $this.Append($elType,$className,$null) } + [PowerStartHTML] Append($elType = "table") { return $this.Append($elType,$null,$null) } + [PowerStartHTML] Add($elType = "table",$className=$null,[string]$text=$null) { + $this.Append($elType,$className,$text) + $this.lastEl = $this.newEl + return $this + } + [PowerStartHTML] Add($elType = "table",$className=$null) { return $this.Add($elType,$className,$null) } + [PowerStartHTML] Add($elType = "table") { return $this.Add($elType,$null,$null) } + [PowerStartHTML] Main() { + $this.lastEl = $this.xmlDocument.html.body.ChildNodes[0]; + return $this + } + [PowerStartHTML] Up() { + $this.lastEl = $this.lastEl.ParentNode; + return $this + } + N() {} +} +class PowerStartHTMLPassThroughLine { + $object;$cells + PowerStartHTMLPassThroughLine($object) { + $this.object = $object; + $this.cells = new-object System.Collections.HashTable; + } +} +class PowerStartHTMLPassThroughElement { + $name;$text;$element;$id + PowerStartHTMLPassThroughElement($name,$text,$element,$id) { + $this.name = $name; $this.text = $text; $this.element = $element;$this.id = $id + } +} +function New-PowerStartHTML { + param( + [Parameter(Mandatory=$true)][string]$title, + [switch]$nobootstrap=$false + ) + $pshtml = (new-object PowerStartHTML($title)) + if(-not $nobootstrap) { + $pshtml.AddBootStrap().AddContainerAttrToMain().N() + } + return $pshtml +} +function Add-PowerStartHTMLTable { + param( + [Parameter(Mandatory=$True,ValueFromPipeline=$True)]$object, + [PowerStartHTML]$psHtml, + [string]$tableTitle = $null, + [string]$tableClass = $null, + [string]$idOverride = $(if($tableTitle -ne $null) {($tableTitle.toLower() -replace "[^a-z0-9]","-") }), + [switch]$passthroughTable = $false, + [switch]$noheaders = $false + ) + begin { + if($tableTitle -ne $null) { + $psHtml.Main().Append("h1",$null,$tableTitle).N() + if($idOverride -ne $null) { + $psHtml.AddAttr($psHtml.newEl,"id","header-$idOverride") + } + } + $psHtml.Main().Add("table").N() + [int]$r = 0 + [int]$c = 0 + if($idOverride -ne $null) { + $psHtml.AddAttr($psHtml.newEl,"id","table-$idOverride") + } + if($tableClass -ne $null) { + $psHtml.AddAttr($psHtml.newEl,"class",$tableClass) + } + [bool]$isFirst = $true + } + process { + $c = 0 + + $props = $object | Get-Member -Type Properties + if(-not $noheaders -and $isFirst) { + $psHtml.Add("tr").N() + if($idOverride -ne $null) { + $psHtml.AddAttr($psHtml.newEl,"id","table-$idOverride-trh") + } + $props | % { + $n = $_.Name; + $psHtml.Append("th",$null,$n).N() + if($idOverride -ne $null) { + $cellid = "table-$idOverride-td-$r-$c" + $psHtml.AddAttr($psHtml.newEl,"id",$cellid) + } + $c++ + } + $c = 0 + $psHtml.Up().N() + } + + $psHtml.Add("tr").N() + if($idOverride -ne $null) { + $psHtml.AddAttr($psHtml.newEl,"id","table-$idOverride-tr-$r") + } + $pstableln = [PowerStartHTMLPassThroughLine]::new($object) + + $props | % { + $n = $_.Name; + $psHtml.Append("td",$null,$object."$n").N() + $cellid = $null + if($idOverride -ne $null) { + $cellid = "table-$idOverride-td-$r-$c" + $psHtml.AddAttr($psHtml.newEl,"id",$cellid) + } + if($passthroughTable) { + $pstableln.cells.Add($n,[PowerStartHTMLPassThroughElement]::new($n,($object."$n"),$psHtml.newEl,$cellid)) + } + + $c++ + } + if($passthroughTable) { + $pstableln + } + $psHtml.Up().N() + $isFirst = $false + $r++ + } + end { + } +} + + +Export-ModuleMember -Function @('New-PowerStartHTML','Add-PowerStartHTMLTable') + + + diff --git a/Modules/VMware-vCD-TenantReport/tests/VMware-vCD-TenantReport.Tests.ps1 b/Modules/VMware-vCD-TenantReport/tests/VMware-vCD-TenantReport.Tests.ps1 new file mode 100644 index 0000000..2effa50 --- /dev/null +++ b/Modules/VMware-vCD-TenantReport/tests/VMware-vCD-TenantReport.Tests.ps1 @@ -0,0 +1,24 @@ +$moduleRoot = Resolve-Path "$PSScriptRoot\.." +$moduleName = "VMware-vCD-TenantReport" + +Describe "General project validation: $moduleName" { + + $scripts = Get-ChildItem $moduleRoot -Include *.ps1, *.psm1, *.psd1 -Recurse + + # TestCases are splatted to the script so we need hashtables + $testCase = $scripts | Foreach-Object {@{file = $_}} + It "Script should be valid powershell" -TestCases $testCase { + param($file) + + $file.fullname | Should Exist + + $contents = Get-Content -Path $file.fullname -ErrorAction Stop + $errors = $null + $null = [System.Management.Automation.PSParser]::Tokenize($contents, [ref]$errors) + $errors.Count | Should Be 0 + } + + It "Module '$moduleName' can import cleanly" { + {Import-Module (Join-Path $moduleRoot "$moduleName.psd1") -force } | Should Not Throw + } +}