Unable to add the Ping Response to Output variable - powershell

I have written a script to export specific registry keys and the sub keys inside it with the server ping response, but my scripts works as expected but the ping response value when I add it to output it is giving null
Please help me to get the ping response value for each server.
## Clear the host
Clear-Host
## Install Export-Excel Module if it is not installed
If (-Not (Get-InstalledModule -Name ImportExcel)){
Install-Module -Name ImportExcel -ErrorAction SilentlyContinue -Force
}
## Set Script Location
Set-Location $PSScriptRoot
## Out File Name
$FileName = "$PSScriptRoot\TCPIP_Interface_Details.xlsx"
## Get full list of servers
$Servers = GC -Path ".\Servers.txt"
## Delete if Old file exists
if (Test-Path $FileName) { Remove-Item $FileName
write-host "$FileName has been deleted" -BackgroundColor DarkMagenta }
else { Write-host "$FileName doesn't exist" -BackgroundColor Red }
## Loop through each server
$Result = foreach ($vm in $Servers) {
## Check the Ping reponse for each server
Write-Host "Pinging Server" $vm
$Ping = Test-Connection -Server $vm -Quiet -Verbose
if ($Ping){Write-host "Server" $vm "is Online" -BackgroundColor Green}
else{Write-host "Unable to ping Server" $vm -BackgroundColor Red}
## Check the Network Share path Accessibility
Write-Host "Checking Share Path on" $vm
$SharePath = Test-Path "\\$vm\E$" -Verbose
if ($SharePath){Write-host "Server" $vm "Share Path is Accessible" -BackgroundColor Green}
else{Write-host "Server" $vm "Share path access failed" -BackgroundColor Red}
Invoke-Command -ComputerName $vm {
## Get ChildItems under HKLM TCPIP Parameter Interface
Get-ChildItem -Path 'HKLM:\SYSTEM\CurrentControlSet\Services\Tcpip\Parameters\Interfaces' | ForEach-Object {
Get-ItemProperty -Path $_.PSPath | Where-Object { $_.PsObject.Properties.Name -like 'Dhcp*' }
} | Select-Object -Property #{Name = 'ComputerName'; Expression = {$env:COMPUTERNAME+"."+$env:USERDNSDOMAIN}},
#{Name = 'Ping_Response'; Expression = {if($using:Ping) {'Pinging'} else {'Unable to ping'}}},
#{Name = 'Share_Path_Access'; Expression = {if($using:SharePath) {'Accessible'} else {'Not Accessible'}}},
DhcpIPAddress, #{Name = 'DhcpNameServer'; Expression = {$_.DhcpNameServer -split ' ' -join '; '}},
DhcpServer, #{Name = 'DhcpDefaultGateway'; Expression = {$_.DhcpDefaultGateway -join '; '}}
}}
$Result | Select-Object * -Exclude PS*, RunspaceId

As commented, just change your calculated property into
#{Name = 'Ping_Response'; Expression = {$using:Ping}}
By scoping the variable $Ping with using:, it will be known in the scriptblock for Invoke-Command. Without that, the scritblock simply sees it as new, undefined variable (i.e. $null)
If as you commented, you would rather see some text instead of just True or False, have the expression in that calculated property output whatever you want like:
#{Name = 'Ping_Response'; Expression = {if($using:Ping) {'Pinging'} else {'Unable to ping'}}}
Another way of letting the scriptblock know what $Ping is, is by adding this to the command as parameter:
Invoke-Command -ComputerName $vm {
param([bool]$Ping)
# ... the rest of the scriptblock
# now, you do not have to use the `using:` scope on $Ping
} -ArgumentList $Ping
To answer your latest comment, if pinging the machine did not work, there is really no reason to try and get properties from it using Invoke-Command. If you also want to have the non-pingables in your output, do:
$result = foreach ($vm in $Servers) {
$Ping = Test-Connection -Server $vm -Quiet -Verbose
if ($Ping) {
# we can reach this machine, so gather the properties we need and output an object
# the 'Ping_Response 'can now be hardcoded
Invoke-Command -ComputerName $vm -ScriptBlock {
Get-ChildItem -Path 'HKLM:\SYSTEM\CurrentControlSet\Services\Tcpip\Parameters\Interfaces' | ForEach-Object {
Get-ItemProperty -Path $_.PSPath | Where-Object { $_.PsObject.Properties.Name -like 'Dhcp*' }
}
} |
Select-Object -Property #{Name = 'ComputerName'; Expression = {$_.PSComputerName}},
#{Name = 'Ping_Response'; Expression = { 'Pinging' }},
DhcpIPAddress,
#{Name = 'DhcpNameServer'; Expression = {$_.DhcpNameServer -split ' ' -join '; '}},
DhcpServer,
#{Name = 'DhcpDefaultGateway'; Expression = {$_.DhcpDefaultGateway -join '; '}}
}
else {
# return a similar object for machines that didn't respond to Ping
# since we cannot reach this machine, most properties will be empty
'' | Select-Object -Property #{Name = 'ComputerName'; Expression = {$vm}},
#{Name = 'Ping_Response'; Expression = { 'Unable to ping' }},
DhcpIPAddress,DhcpNameServer,DhcpServer,DhcpDefaultGateway
}
}
# output the results
$result | Select-Object * -ExcludeProperty PS*, RunspaceId | Export-Csv -Path "C:\temp\TCPIP_Interface_Details.csv" -NoTypeInformation

Related

How to get details of OS name, .net framework details for multiple servers using powershell?

I am trying to get details of OS Name and .net framework details for multiple servers using PowerShell script below.
$servers = Get-Content -Path "C:\Users\vinay\Desktop\servers.txt"
Foreach ($s in $servers)
{
write-host $s
$s.PSDrive
$s.PSChildName
Add-Content C:\Users\vinay\Desktop\specs.txt "Specs:"
$OS = (Get-WmiObject Win32_OperatingSystem).CSName
Add-Content C:\Users\vinay\Desktop\specs.txt "`nOS:$OS"
$Bit = (Get-WMIObject win32_operatingsystem).name
Add-Content C:\Users\vinay\Desktop\specs.txt "`nOS Bit: $Bit"
$name = (Get-WmiObject Win32_OperatingSystem).OSArchitecture
Add-Content C:\Users\vinay\Desktop\specs.txt "`nServer Name: $name"
$dotnet = Get-ChildItem 'HKLM:\SOFTWARE\Microsoft\NET Framework Setup\NDP' -Recurse | Get-ItemProperty -Name version -EA 0 | Where { $_.PSChildName -Match '^(?!S)\p{L}'} | Select PSChildName, version
Add-Content C:\Users\vinay\Desktop\specs.txt "`n.NET VERSION $dotnet"
$release = (Get-ItemProperty "HKLM:SOFTWARE\Microsoft\NET Framework Setup\NDP\v4\Full").Release
Add-Content C:\Users\vinay\Desktop\specs.txt "`nRelease number: $release"
Add-Content C:\Users\vinay\Desktop\specs.txt "`n----------------------------"
}
But i am getting details of the server in which i am running the script but not for other servers in the text file.
however write-host $s reads all the servers in the text file. Please help where i am doing wrong.
Continuing from my comment, you need to perform your code looping over the servers in your list and have that code run on that server instead of your own machine you are running the script from.
Also, I would have the code output objects instead of trying to add lines to a text file, so that you ca save the results in a structured format like CSV.
Try
$servers = Get-Content -Path "C:\Users\vinay\Desktop\servers.txt"
$specs = foreach ($s in $servers) {
if (Test-Connection -ComputerName $s -Count 1 -Quiet) {
Write-Host "Probing server $s"
# you may need to add parameter -Credential and supply the credentials
# of someone with administrative permissions on the server
Invoke-Command -ComputerName $s -ScriptBlock {
$os = (Get-CimInstance -ClassName Win32_OperatingSystem)
Get-ChildItem 'HKLM:\SOFTWARE\Microsoft\NET Framework Setup\NDP' -Recurse |
Get-ItemProperty -Name Version, Release -Erroraction SilentlyContinue |
Where-Object { $_.PSChildName -Match '^(?!S)\p{L}'} |
ForEach-Object {
[PsCustomObject]#{
'ComputerName' = $os.CSName
'Operating System' = $os.Caption
'Architecture' = $os.OSArchitecture
'Net Version' = [version]$_.Version
'Net Framework' = $_.PsChildName
'Net Release' = $_.Release
}
}
}
}
else {
Write-Warning "Computer '$s' cannot be reached.."
}
}
# remove extra properties PowerShell added
$specs = $specs | Select-Object * -ExcludeProperty PS*, RunspaceId
# output on screen
$specs | Format-Table -AutoSize
# output to CSV file you can open in Excel
$specs | Export-Csv -Path 'C:\Users\vinay\Desktop\specs.csv' -UseCulture -NoTypeInformation

PowerShell Exporting

Can someone point me in the right direction? Basically, I would like to export the results of my testpath to a csv. Below is what I am working with. I have read a couple Microsoft documents but they only seem to confuse me even more. Any feedback is appreciated.
$ComputerList = (Get-ADComputer -Filter *).name
$ComputerList
write-host "`n"
Foreach ($Computer in $ComputerList)
{
$userfolders = get-childitem "\\$Computer\C$\users\"
foreach ($user in $userfolders) {
$ErrorActionPreference= 'silentlycontinue'
$path = $user.fullname
write-host $path
$t = test-path -Path "$path\AppData\Local\Google\Chrome\User Data\Default"
IF ($t -eq 'True') {write-host "Has it" -ForegroundColor yellow} ELSE {write-host "no"}
write-host "`n"
}
$Output =New-Object -TypeName PSObject -Property #{
} | Select-Object
}
$Output | C:\Users\"user"\Chrome.csv
write-output "Script finished. Please check output files"
Assuming you want a record per user per computer, there's two things you want to change structurally:
Create new objects in the inner foreach loop
Assign all the objects created to $Output:
$ComputerList = (Get-ADComputer -Filter *).name
$ComputerList
write-host "`n"
$Output = Foreach ($Computer in $ComputerList) {
$userfolders = get-childitem "\\$Computer\C$\users\"
foreach ($user in $userfolders) {
$ErrorActionPreference = 'silentlycontinue'
$path = $user.fullname
write-host $path
$t = test-path -Path "$path\AppData\Local\Google\Chrome\User Data\Default"
IF ($t -eq 'True') {write-host "Has it" -ForegroundColor yellow} ELSE {write-host "no"}
write-host "`n"
New-Object -TypeName PSObject -Property #{
# We still need a bit of magic here
}
}
}
$Output | C:\Users\"user"\Chrome.csv
write-output "Script finished. Please check output files"
Now we just need to decide on what properties to add to our output objects:
New-Object -TypeName PSObject -Property #{
# We definitely want to know which computer and user profile the results are for!
ComputerName = $Computer
ProfileName = $user.Name
# And finally we want the results of `Test-Path`
Result = $t
}
Here's another option. Though nowhere near as elegant as what Matthias gave you. ;-}
It's just a refactor, to narrow down your code and pass everything directly and output by default, without the need for all the, Write-* stuff and the like. PowerShell just grants a number of ways to accomplish a use case.
Clear-Host
$null = New-Item -Path 'C:\Temp\Chrome.csv' -Force
$Status = $null
$env:COMPUTERNAME,'Localhost', '127.0.0.1' |
Foreach {
Get-ChildItem "\\$PSItem\C$\users\" |
foreach {
$ErrorActionPreference = 'silentlycontinue'
# Use variable squeezing to assign and output to the screen
($path = $PSItem.fullname)
If (test-path -Path "$path\AppData\Local\Google\Chrome\User Data\Default") {$Status = 'Has it'}
Else {$Status = 'no'}
}
[PSCustomObject] #{
ComputerName = $PSItem
Status = $Status
} | Export-Csv -Path 'C:\Temp\Chrome.csv' -Append
}
'Script finished. Please check output files'
# Results on screen
<#
\\104DB2FE-76B8-4\C$\users\ContainerAdministrator
\\104DB2FE-76B8-4\C$\users\ContainerUser
\\104DB2FE-76B8-4\C$\users\Public
\\104DB2FE-76B8-4\C$\users\WDAGUtilityAccount
\\Localhost\C$\users\ContainerAdministrator
\\Localhost\C$\users\ContainerUser
\\Localhost\C$\users\Public
\\Localhost\C$\users\WDAGUtilityAccount
\\127.0.0.1\C$\users\ContainerAdministrator
\\127.0.0.1\C$\users\ContainerUser
\\127.0.0.1\C$\users\Public
\\127.0.0.1\C$\users\WDAGUtilityAccount
Script finished. Please check output files
#>
Import-Csv -Path 'C:\Temp\Chrome.csv'
# Results
<#
104DB2FE-76B8-4 no
Localhost no
127.0.0.1 no
#>
Clear-Host
$null = New-Item -Path 'C:\Temp\Chrome.csv' -Force
$Status = $null
$env:COMPUTERNAME,'Localhost', '127.0.0.1' |
Foreach {
Get-ChildItem "\\$PSItem\C$\users\" |
foreach {
$ErrorActionPreference = 'silentlycontinue'
# Use variable squeezing to assign and output to the screen
($path = $PSItem.fullname)
If (test-path -Path "$path\AppData\Local\MicrosoftEdge") {$Status = 'Has it'}
Else {$Status = 'no'}
}
[PSCustomObject] #{
ComputerName = $PSItem
Status = $Status
} | Export-Csv -Path 'C:\Temp\Chrome.csv' -Append
}
'Script finished. Please check output files'
# Results
<#
\\104DB2FE-76B8-4\C$\users\ContainerAdministrator
\\104DB2FE-76B8-4\C$\users\ContainerUser
\\104DB2FE-76B8-4\C$\users\Public
\\104DB2FE-76B8-4\C$\users\WDAGUtilityAccount
\\Localhost\C$\users\ContainerAdministrator
\\Localhost\C$\users\ContainerUser
\\Localhost\C$\users\Public
\\Localhost\C$\users\WDAGUtilityAccount
\\127.0.0.1\C$\users\ContainerAdministrator
\\127.0.0.1\C$\users\ContainerUser
\\127.0.0.1\C$\users\Public
\\127.0.0.1\C$\users\WDAGUtilityAccount
Script finished. Please check output files
#>
Import-Csv -Path 'C:\Temp\Chrome.csv'
# Results
<#
ComputerName Status
------------ ------
104DB2FE-76B8-4 Has it
Localhost Has it
127.0.0.1 Has it
#>

Powershell IIS Audit Scripting

I'm in the early stages of learning powershell, and I'm trying to put together a script that remotely gathers information from our IIS servers, but I'm encountering several issues.
The first one is that the IP Address and OU columns remain empty in the output file.
The second one is that I'm not able to format the Administrator group column to have 1 group per line, or delimited by commas.
This is the current version of the code:
$computers = Get-Content "C:\servers.txt"
#Running the invoke-command on remote machine to run the iisreset
$output = foreach ($computer in $computers)
{
Write-Host "Details from server $computer..."
try{
Invoke-command -ComputerName $computer -ErrorAction Stop -ScriptBlock{
# Ensure to import the WebAdministration and AD module
Import-Module WebAdministration
Import-Module ActiveDirectory
$webapps = Get-WebApplication
$list = #()
foreach ($webapp in get-childitem IIS:\AppPools\)
{
$name = "IIS:\AppPools\" + $webapp.name
$item = #{}
$item.server = $env:computername
$item.WebAppName = $webapp.name
$item.Version = (Get-ItemProperty $name managedRuntimeVersion).Value
$item.State = (Get-WebAppPoolState -Name $webapp.name).Value
$item.UserIdentityType = $webapp.processModel.identityType
$item.Username = $webapp.processModel.userName
$item.Password = $webapp.processModel.password
$item.OU = (Get-ADComputer $_ | select DistinguishedNAme)
$item.IPAddress = (Get-NetIPAddress -AddressFamily IPv4)
$item.Administrators = (Get-LocalGroupMember -Group "Administrators")
$obj = New-Object PSObject -Property $item
$list += $obj
}
$list | select -Property "Server","WebAppName", "Version", "State", "UserIdentityType", "Username", "Password", "OU", "Ip Address", "Administrators"
}
} catch {
Add-Content .\failedservers.txt -Force -Value $computer
}
}
$output | Export-Csv -Path .\output.csv
#Stop-Transcript
I'd really appreciate any input on how to get it to work properly or improve on the code itself.
Thanks in advance!
For the OU property, you'll want to reference $env:COMPUTERNAME instead of $_:
$item.OU = (Get-ADComputer $env:COMPUTERNAME |Select -Expand DistinguishedName)
For the IPAddress and Administrators fields you'll want to use -join to create comma-separated lists of the relevant values:
$item.IPAddress = (Get-NetIPAddress -AddressFamily IPv4).IPAddress -join ','
$item.Administrators = (Get-LocalGroupMember -Group "Administrators").Name -join ','

get last user logon time without AD

I'm trying to create a script that can get the user profiles that haven't logged on a specific computer within 30 days NOT using active directory but my script didn't work. I am using Powershell version 3. This is my code:
netsh advfirewall firewall set rule group="Windows Management Instrumentation (WMI)" new enable=yes
$ComputerList = Get-Content C:\temp\Computers1.txt
$myDomain = Get-Content C:\temp\Domain.txt
$csvFile = 'C:\temp\Profiles.csv'
# Create new .csv output file
New-Item $csvFile -type file -force
# Output the field header-line to the CSV file
"HOST,PROFILE" | Add-Content $csvFile
# Loop over the list of computers from the input file
foreach ($Computer in $ComputerList) {
# see if ping test succeeds for this computer
if (Test-Connection $Computer -Count 3 -ErrorAction SilentlyContinue) {
$ComputerFQDN = $Computer + $myDomain
$Profiles = Get-WmiObject -Class Win32_UserProfile -Computer $ComputerFQDN | Where{$_.LocalPath -notlike "*$env:SystemRoot*"}
foreach ($profile in $profiles) {
try {
$objSID = New-Object System.Security.Principal.SecurityIdentifier($profile.LocalPath) | Where {((Get-Date)-$_.lastwritetime).days -ge 30}
#| Where-Object {$_.LastLogonDate -le $CurrentDate.AddDays(-60)}
$objuser = $objsid.Translate([System.Security.Principal.NTAccount])
$objusername = $objuser.value
} catch {
$objusername = $profile.LocalPath
}
switch($profile.status){
1 { $profileType="Temporary" }
2 { $profileType="Roaming" }
4 { $profileType="Mandatory" }
8 { $profileType="Corrupted" }
default { $profileType = "LOCAL" }
}
$User = $objUser.Value
#output profile detail for this host
"$($Computer.toUpper()), $($objusername)" | Add-Content $csvFile
}
} else {
#output failure message for this host
"$($Computer.toUpper()), PING TEST FAILED" | Add-Content $csvFile
}
#LOOP
}
I tried to change the -ge to -le in the line $objSID = New-Object System.Security.Principal.SecurityIdentifier($profile.LocalPath) | Where {((Get-Date)-$_.lastwritetime).days -ge 30}, as well as changing the range after it but it still gave me the same list of computers regardless of my changes.
There are a few problems with the script, most notable is that your use of Where-Object is testing an object (SID) that doesn't know anything about dates.
I would break it down a little differently. I would write a function to catch all the stuff I need to do to attempt to figure out the last logon. That's my goes in my stack of utility functions in case I need it again.
Then I have something to use that function which deals with implementing the logic for the immediate requirement.
So you end up with this. It's a bit long, see what you think.
function Get-LastLogon {
[CmdletBinding()]
param(
[Parameter(ValueFromPipeline = $true)]
[String]$ComputerName = $env:COMPUTERNAME
)
process {
Get-WmiObject Win32_UserProfile -ComputerName $ComputerName -Filter "Special='FALSE'" | ForEach-Object {
# Attempt to get the UserAccount using WMI
$userAccount = Get-WmiObject Win32_UserAccount -Filter "SID='$($_.SID)'" -ComputerName $ComputerName
# To satisfy WMI all single \ in a path must be escaped.
# Prefer to use NTUser.dat for last modification
$path = (Join-Path $_.LocalPath 'ntuser.dat') -replace '\\', '\\'
$cimObject = Get-WmiObject CIM_DataFile -Filter "Name='$path'" -ComputerName $ComputerName
if ($null -eq $cimObject) {
# Fall back to the directory
$path = $_.LocalPath -replace '\\', '\\'
$cimObject = Get-WmiObject CIM_Directory -Filter "Name='$path'" -ComputerName $ComputerName
}
$lastModified = $null
if ($null -ne $cimObject) {
$lastModified = [System.Management.ManagementDateTimeConverter]::ToDateTime($cimObject.LastModified)
}
# See if LastUseTime is more useful.
$lastUsed = $null
if ($null -ne $_.LastUseTime) {
$lastUsed = [System.Management.ManagementDateTimeConverter]::ToDateTime($_.LastUseTime)
}
# Profile type
$profileType = switch ($_.Status) {
1 { "Temporary" }
2 { "Roaming" }
4 { "Mandatory" }
8 { "Corrupted" }
0 { "LOCAL" }
}
[PSCustomObject]#{
ComputerName = $ComputerName
Username = $userAccount.Caption
LastChanged = $lastModified
LastUsed = $lastUsed
SID = $_.SID
Path = $_.LocalPath
ProfileType = $profileType
}
}
}
}
$myDomain = Get-Content C:\temp\Domain.txt
Get-Content C:\temp\Computers1.txt | ForEach-Object {
$ComputerName = $_ + $myDomain
if (Test-Connection $ComputerName -Quiet -Count 3) {
Get-LastLogon -ComputerName $ComputerName | Select-Object *, #{Name='Status';Expression={ 'OK' }} |
Where-Object { $_.LastChanged -lt (Get-Date).AddDays(-30) }
} else {
# Normalise the output so we don't lose columns in the export
$ComputerName | Select-Object #{Name='ComputerName';e={ $ComputerName }},
Username, LastChanged, LastUsed, SID, Path, ProfileType, #{Name='Status';Expression={ 'PING FAILED' }}
}
} | Export-Csv 'C:\temp\Profiles.csv' -NoTypeInformation

Remote Registry Query Powershell

I am trying to make a powershell script that gets computer names from a txt file, checks the registry to see what the current version of Flash is installed, and if it is less than 18.0.0.203, run an uninstall exe. Here is what I have been trying:
# Retrieve computer names
$Computers = Get-Content C:\Users\araff\Desktop\FlashUpdater\Servers.txt
# Select only the name from the output
#$Computers = $Computers | Select-Object -ExpandProperty Name
#Sets command to execute if the version is not 18.0.0.203
$command = #'
cmd.exe /C uninstall_flash_player.exe -uninstall
'#
#Iterate through each computer and execute the command if the version is not 18.0.0.203
[Array]$Collection = foreach ($Computer in $Computers){
$AD = Get-ADComputer $computer -Properties LastLogonDate
$ping = Test-Connection -quiet -computername $computer -Count 2
$datetime = Get-Date
$Reg = [Microsoft.Win32.RegistryKey]::OpenRemoteBaseKey('LocalMachine', $computer)
$RegKey= $Reg.OpenSubKey("SOFTWARE\Macromedia\FlashPlayerActiveX")
$version = $RegKey.GetValue("Version")
if ($version -eq '= 18.0.0.203') {
$installed = "Flash is up to date!"
}
Else {
$installed = "Removing old version..."
Invoke-Expression -Command:$command
}
New-Object -TypeName PSObject -Property #{
TimeStamp = $datetime
ComputerName = $computer
Installed = $installed
OnlineStatus = $ping
LastLogonDate = $AD.LastLogonDate
} | Select-Object TimeStamp, ComputerName, Installed, OnlineStatus, LastLogonDate
}
#Exports csv
$Collection | Export-Csv FlashUpdaterOutput.csv -NoTypeInformation
It exports the CSV just fine, but all the installed columns say "Removing" even if it is the current version. Any ideas on what I am doing wrong? Thanks!
Rather than opening a remote registry key and running cmd /c why not make a [scriptblock] and pipe it to Invoke-Command.
AsJob it and come back for the results later.
[scriptblock]$code = {
$uninst = gci "C:\Windows\System32\Macromed\Flash" -Filter "*.exe" | ?{ $_.Name -ne "FlashUtil64_18_0_0_204_ActiveX.exe" -and $_.Name -like "FlashUtil64_18_0_0_*_ActiveX.exe" }
foreach($flash in $uninst) {
Write-Host $flash.FullName
$proc = New-Object System.Diagnostics.Process
$proc.StartInfo.FileName = $flash.FullName
$proc.StartInfo.Arguments = "-uninstall"
$proc.Start()
$proc.WaitForExit()
Write-Host ("Uninstalling {0} from {1}" -f $flash.BaseName,$env:COMPUTERNAME)
}
}
Invoke-Command -ScriptBlock $code -ComputerName frmxncsge01 -AsJob
Then later just come back and Get-Job | Receive-Job -Keep and see the results.