Monday 20 January 2020

PowerShell to Send Mail in HTML format using SMTP

PowerShell Script to Send mail in html format using smtp Server:

PS Script to Send mail with attachment(optional):
Information required:
1. Create ".html page" like in script using "content.html"
2. Make a note of file path, if needs to be attached with an email.
3. smtp server details.
4. Save the below script somewhere with ".ps1" extension, That's it!!

############################################

###########Define Variables########

$fromaddress = "donotreply@labtest.com"
$toaddress = "A@labtest.com"
$bccaddress = "A@labtest.com"
$CCaddress = "A@labtest.com"
$Subject = "ACtion Required"
$body = get-content C:\dba_path\content.html
$attachment = "C:\dba_path\test.txt"
$smtpserver = "smtp.lab.com"

####################################

$message = new-object System.Net.Mail.MailMessage
$message.From = $fromaddress
$message.To.Add($toaddress)
$message.CC.Add($CCaddress)
$message.Bcc.Add($bccaddress)
$message.IsBodyHtml = $True
$message.Subject = $Subject
$attach = new-object Net.Mail.Attachment($attachment)
$message.Attachments.Add($attach)
$message.body = $body
$smtp = new-object Net.Mail.SmtpClient($smtpserver)
$smtp.Send($message)

###############################################

Wednesday 15 January 2020

PowerShell Script to execute command on multiple Servers

PowerShell Script to execute same command and get some information from multiple servers:

PS Script to run remotely on multiple servers:

#path of the text file with the list of all the servers

$path = C:\PSScripts\serverslist.txt
$computers = Get-Content -Path $path

$software = "SQL Server*";
$installed = (Get-Service | Where-Object {$_.DisplayName -like $software}) -ne $null

#Another way is to use like below
#$computers = @("Server1",”Server2”,Server3)

#Can also ask\save for credential who have access on all servers before execution
#$Cred = Get-Credential # Add Credentials for all Servers (Domain or non-Domain)
  

# Run Command 

foreach($computer in $computers){
 Write-Host "Running process on $computer" -ForegroundColor green

    
if (Test-Connection -ComputerName $computer -Count 1 -ErrorAction SilentlyContinue)
    {

#With $cred option
#Invoke-Command -ComputerName $computer -ScriptBlock `
#{Get-WmiObject -Class Win32_Volume -EA silentlyContinue | `
#Select-Object $env:computername,Name,Label,BlockSize | Format-Table -AutoSize} -Credential $cred

 #Without $cred option
         Invoke-Command -ComputerName $computer -ScriptBlock `
         {Get-WmiObject -Class Win32_Volume -EA silentlyContinue | `
          Select-Object $env:computername,Name,Label,BlockSize | Format-Table -AutoSize}
                    

#additional check on running particular script on SQL Server
             If(-Not $installed) { Write-Host "'$software' is not installed." }
             else { Invoke-Sqlcmd -InputFile `
                    "C:\PSScripts\TempDBValidation.sql" -ServerInstance $computer}


    }
else{ Write-Host "$computer Server is not accessiable" -ForegroundColor red }

#For loop end
}
Save the script as ".ps1" extension.

Ways to execute the script and capture output in text\csv file :
1. Open PowerShell as admin mode
2. Go to path where ".ps1" file is saved.
3. Two ways to execute-
Simple way to execute & output on PowerShell console:
.\Remote_PS-Script.ps1

Capture Output in csv:
.\Remote_PS-Script.ps1 | `
Out-File -FilePath C:\output\PS_Script-$(Get-Date -format yyyyMMdd_hhmmsstt).csv -Append

Friday 10 January 2020

Useful PowerShell scripts -1

Here are the few very useful PowerShell short commands for basic checks:

Disk info:
Get-WmiObject Win32_volume -ComputerName .|Format-Table Name, Label,
@{Name="SizeGB";Expression={($_.capacity/1gb -as [int])}},
@{Name="FreeSpaceGB";Expression={([math]::Round($_.Freespace/1GB,0))}},
@{Name="UsedGB";Expression={($_.capacity/1gb -as [int])-([math]::Round($_.Freespace/1GB,0))}}


Disk block size format info:
Script-1:
Get-CimInstance -ClassName Win32_Volume | Select-Object Name, Label, BlockSize | Format-Table -AutoSize Script-2:
Get-WmiObject -Class Win32_Volume -ComputerName .| Select-Object Name,Label,BlockSize | Format-Table -AutoSize

Disk Lun ID\Physical location info:
Get-WmiObject Win32_DiskDrive | sort scsibus,scsilogicalunit | 
ft @{Label=”ScsiBus”;Expression={$_.scsibus}},@{Label=”LUN”;Expression={$_.scsilogicalunit}}, 
@{Label=”Disk Number”;Expression={$_.index}}, 
@{Label=”Size (GB)”;Expression={[Math]::Round($_.size / 1GB)}} -autosize


Disk partition style (GPT\MBR) info:

Get-Disk | Select-Object  DiskNumber,FriendlyName,SerialNumber,HealthStatus,OperationalStatus,
@{Name="SizeGB";Expression={($_.Size/1gb -as [int])}},PartitionStyle | Format-Table -AutoSize


Add User at server level remotely\locally :
Invoke-Command -ComputerName . 
-ScriptBlock {Add-LocalGroupMember -Group Administrators -Member "domain\account"}


Sample code to check if software is installed, do something & if not, do something else :
$software = "SQL Server*";
$installed = (Get-Service | Where-Object {$_.DisplayName -eq $software}) -like $null

If(-Not $installed)
{
    Write-Host "Copy '$software'..."

    Copy-Item -Path "Source_Path" -Destination "C:\dba\xxxx.msi"

    Get-Date

    Write-Host "Ready to install '$software'..."
    Read-Host -Prompt "Press Enter to continue"

    Invoke-Command -ComputerName . -ScriptBlock `
    {Start-Process msiexec.exe -Wait -ArgumentList '/I C:\dba\xxx.msi /quiet'}
    Write-Host "driver Installed!!"
}
else { Write-Host "'$software' is installed." }


Tuesday 7 January 2020

Azure Useful PowerShel Scripts -1

Here are the few very useful Azure PowerShell-CLI short commands for basic checks:

Select Subscription:
Get-AzureRmSubscription
Select-AzureRmSubscription -SubscriptionId "12y127128y172kajsnkdaj"
List RG:
Get-AzureRmResourceGroup | SELECT ResourceGroupName,Location
List of Storage Account:
Get-AzureRmStorageAccount | Select StorageAccountName, Location
Get Storage Account Key:
Get-AzureRmStorageAccountKey -ResourceGroupName "RGName" -AccountName "Storage_AccName"
List Containers:
$context = New-AzureStorageContext -StorageAccountName "Storage_AccName" `
-StorageAccountKey "XXXXXXXXXXXX"
List of all Blobs\Files:

Get-AzureStorageBlob -Container 'Container_Name' -Context $context
 | Select-Object @{name="Name"; 
expression={"https://xyz.blob.core.windows.net/sqldbbackups1/"+$_.Name}}
 | Where-Object { $_.Name -like '*.bak*'}

OR to use in restore command:

Get-AzureStorageBlob -Container 'Container_Name' -Context $context
 | Select-Object @{name="Name"; 
expression={"URL = 'https://xyz.blob.core.windows.net/sqldbbackups1/"+$_.Name + "',"}}
 | Where-Object { $_.Name -like '*.bak*'} | Format-List *

Change/replace $ sign to %24 to convert into URL:

Get-AzureStorageBlob -Container 'Container_Name' -Context $context
 | Select-Object @{name="Name"; 
expression={"URL = 'https://xyz.blob.core.windows.net/sqldbbackups1/"+$_.Name.replace('$','%24')
 + "',"}}
 | Where-Object { $_.Name -like '*.bak*'} | Format-List *
Real time used Sample Scripts :
Get the list of files with last modified date:
Get-AzureStorageContainer -Context $context | SELECT Name, Lastmodified

Another example, Check if files are older than 7 days on Storage blob:

$lastdate = Get-AzureStorageBlob -Container "Cont_Name" -Context $context  `
| SELECT Lastmodified | sort @{expression="LastModified";Descending=$false} | `
SELECT -First 1 | ft -HideTableHeaders | Out-String
$checkdate = Get-Date -date $(Get-Date).AddDays(-7) -Format "MM/dd/yyyy HH:mm K"

if($lastdate -le $checkdate) {
$fromaddress = "donotreply@xyz.com"
$toaddress = "amit@xyz.com"
$Subject = "Action Required : No Truncation "
$body = "Backup files are not getting truncated!!"
$smtpserver = "smtpmail.xyz.com"
$message = new-object System.Net.Mail.MailMessage
$message.From = $fromaddress
$message.To.Add($toaddress)
$message.Subject = $Subject
$message.body = $body
$smtp = new-object Net.Mail.SmtpClient($smtpserver)
$smtp.Send($message)
Write-Output "Alert triggered!!"}
else{
     Write-Output "All Good!!"
}

Count Total Files and Size Azure blob: 

$resourceGroup = "RGName"
$storageAccountName = "Storage_AccName"
$containerName = "containerName"

# get a reference to the storage account and the context
$storageAccount = Get-AzureRmStorageAccount -ResourceGroupName `
$resourceGroup -Name $storageAccountName
$context = $storageAccount.Context

# get a list of all of the blobs in the container 
$listOfBLobs = Get-AzureStorageBlob -Container $ContainerName -Context $context 

# zero out our total
$length = 0

# this loops through the list of blobs and retrieves the length for each blob
#   and adds it to the total
$listOfBlobs | ForEach-Object {$length = $length + $_.Length}

$count=$listOfBlobs | select Name, Length
$length= (($length)/1073741824)

# output the blobs and their sizes and the total 
Write-Host "List of Blobs and their size (length)"
Write-Host " " 
Write-Host " "
Write-Host "Total Files Count = " $count.Count
Write-Host "Total Size in GiB = " $length 

Wednesday 1 January 2020

SQL Server Database Partitioning

Today, We will be going with the example of SQL Database table Partitioning.
First we have to identify on the bases of which column we are going to partition the rows. mostly rows are partition on the bases of DateTime data type column which we are going to implement today.
Here the case is, we are creating table partition on basis of DateTime column in existing running database. Follow the below steps:
1. Add new file groups to existing database. So You could add files in multiple file groups on the same disk or on different disks. If you plan on using multiple aligned tables and data retrieval statements will almost always affect a small subset of rows in each table that could be grouped in partitions you should consider using multiple disk arrays. However, if you anticipate many SELECT statements retrieving a large majority of rows from each table, then performing input / output operations against multiple disk arrays could actually worsen performance. In such a case, you can still benefit from partitioning but should try to create multiple filegroups on the same disk array.
The following statements create filegroups in DBADB001 database:
ALTER DATABASE DBADB001 ADD FILEGROUP [FG_2012]  
GO  
ALTER DATABASE DBADB001 ADD FILEGROUP [FG_2013]  
GO  
ALTER DATABASE DBADB001 ADD FILEGROUP [FG_2014]  
GO  
ALTER DATABASE DBADB001 ADD FILEGROUP [FG_2015]  
GO
Each filegroup can have one or multiple files associated with it. 2. Add one file to each filegroup so that you can store partition data in each filegroup:
ALTER DATABASE DBADB001
  ADD FILE
  (NAME = N'DataFG_2012',
  FILENAME = N'D:\mssql\mssqlserver\mdf\DataFG_2012.ndf',
  SIZE = 50MB,
  MAXSIZE = 100MB,
  FILEGROWTH = 5MB)
  TO FILEGROUP [FG_2012]  
GO  
ALTER DATABASE DBADB001
  ADD FILE
  (NAME = N'DataFG_2013',
  FILENAME = N'D:\mssql\mssqlserver\mdf\DataFG_2013.ndf',
  SIZE = 50MB,
  MAXSIZE = 100MB,
  FILEGROWTH = 5MB)
  TO FILEGROUP [FG_2013]  
GO 
ALTER DATABASE DBADB001
  ADD FILE
  (NAME = N'DataFG_2014',
  FILENAME = N'D:\mssql\mssqlserver\mdf\DataFG_2014.ndf',
  SIZE = 50MB,
  MAXSIZE = 100MB,
  FILEGROWTH = 5MB)
  TO FILEGROUP [FG_2014]  
GO 
ALTER DATABASE DBADB001
  ADD FILE
  (NAME = N'DataFG_2015',
  FILENAME = N'D:\mssql\mssqlserver\mdf\DataFG_2015.ndf',
  SIZE = 50MB,
  MAXSIZE = 100MB,
  FILEGROWTH = 5MB)
  TO FILEGROUP [FG_2015]  
GO 
The CREATE TABLE statement normally specifies a particular filegroup on which the table is built. However, with SQL Server 2005 and later, you can reference the partition scheme as opposed to a filegroup, because you can spread each table across multiple filegroups. Partition functions commonly reference a column with DATETIME data type. This makes sense if you want to spread the data based on its creation timeframe. Data warehouse fact tables typically don't contain the DATETIME column. Instead, they normally include a foreign key referencing the date and time value in the time dimension (or, more accurately, the date dimension). Important point to keep in mind is that you're not limited to columns with DATETIME data type for partitioning keys. You could use the INTEGER data type key referencing the date dimension. However, if you use the integer values for partitioning and you want to partition based on date, then your application must ensure that records for a given week, month, quarter or year (depending on your implementation) must fall into certain ranges of integers.
3. Create the new table and will insert random data about 5000 rows.
use DBADB001
GO

CREATE TABLE [dbo].[TestTable] 
([pkcol] [int] NOT NULL,
 [Int1] [int] NULL,
 [Int2] [int] NULL,
 [TestName] [varchar](50) NULL,
 [partitioncol] DateTime)
GO

ALTER TABLE dbo.TestTable ADD CONSTRAINT PK_TestTable PRIMARY KEY CLUSTERED (pkcol) 
GO
CREATE NONCLUSTERED INDEX IX_TABLE1 ON dbo.TestTable (Int1,Int2)
  WITH (STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, 
        ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) 
  ON [PRIMARY]
GO
-- Populate table data
DECLARE @val INT
SELECT @val=1
WHILE @val < 5001
BEGIN  
   INSERT INTO dbo.TestTable(pkcol, Int1, Int2, TestName, partitioncol) 
      VALUES (@val,@val,@val,'TEST',getdate()-@val)
   SELECT @val=@val+1
END
GO

--To get table rows count
SELECT o.name objectname,i.name indexname, partition_id, partition_number, [rows]
FROM sys.partitions p
INNER JOIN sys.objects o ON o.object_id=p.object_id
INNER JOIN sys.indexes i ON i.object_id=p.object_id and p.index_id=i.index_id
WHERE o.name LIKE '%TestTable%'
4. Now Create a Partition Function:
CREATE PARTITION FUNCTION DateRangePFunc (DATETIME) AS
RANGE LEFT FOR VALUES 
('20121231 23:59:59.997', 
'20131231 23:59:59.997',
'20141231 23:59:59.997',
'20151231 23:59:59.997'
)
GO
5. Now Create Partition Scheme, which takes advantage of the partition function and maps each data range to different filegroups:
CREATE PARTITION SCHEME DateRangePScheme  AS
  PARTITION DateRangePFunc  TO 
  ([FG_2012],
  [FG_2013],
  [FG_2014],
  [FG_2015],
  [PRIMARY]  )
Using this partition scheme, all records with FullDate value prior to December 31, 2011 will be placed on [FG_2011] file group; values between January 1st, 2012 and December 31st, 2012 will be place on [FG_2012] and so forth. Any records for which FullDate column has a value after December 31st, 2015 will be placed on the PRIMARY file group.
Note that the "SaveAll" filegroup has to be specified. In this case, the PRIMARY file group for records that do not fall into any explicitly defined date ranges. Normally as a best practice, you should reserve the PRIMARY filegroup for system objects and have a separate SaveAll filegroup for user data.
Now that there is a partition function and scheme, you can create a partitioned table. The syntax is very similar to any other CREATE TABLE statement except it references the partition scheme instead of a referencing filegroup:
CREATE TABLE [dbo].[TestTable_PT] 
([pkcol] [int] NOT NULL,
 [Int1] [int] NULL,
 [Int2] [int] NULL,
 [TestName] [varchar](50) NULL,
 [partitioncol] DateTime)
  ON DateRangePScheme  (partitioncol)
GO
Now that the table has been created on a partition scheme, populate it based on the existing table "TestTable" and subsequently examine the row count in each partition:
/* normally you should avoid using "SELECT *" construct.
   But since this is an example of populating a sample table
   "SELECT *" won't cause any performance or usability issues.
   In production environments you will typically use BULK INSERT
   statement to populate partitioned tables based on flat files.  
*/
INSERT TestTable_PT
SELECT *
FROM   TestTable
       
/* next we can use $PARTITION function to retrieve row counts
  for each partition:  
*/
DECLARE @TableName NVARCHAR(200) = N'dbo.TestTable_PT'
 
SELECT SCHEMA_NAME(o.schema_id) + '.' + OBJECT_NAME(i.object_id) AS [object]
     , p.partition_number AS [p#]
     , fg.name AS [filegroup]
     , p.rows
     , au.total_pages AS pages
     , CASE boundary_value_on_right
       WHEN 1 THEN 'less than'
       ELSE 'less than or equal to' END as comparison
     , rv.value
     , CONVERT (VARCHAR(6), CONVERT (INT, SUBSTRING (au.first_page, 6, 1) +
       SUBSTRING (au.first_page, 5, 1))) + ':' + CONVERT (VARCHAR(20),
       CONVERT (INT, SUBSTRING (au.first_page, 4, 1) +
       SUBSTRING (au.first_page, 3, 1) + SUBSTRING (au.first_page, 2, 1) +
       SUBSTRING (au.first_page, 1, 1))) AS first_page
FROM sys.partitions p
INNER JOIN sys.indexes i
     ON p.object_id = i.object_id
AND p.index_id = i.index_id
INNER JOIN sys.objects o
     ON p.object_id = o.object_id
INNER JOIN sys.system_internals_allocation_units au
     ON p.partition_id = au.container_id
INNER JOIN sys.partition_schemes ps
     ON ps.data_space_id = i.data_space_id
INNER JOIN sys.partition_functions f
     ON f.function_id = ps.function_id
INNER JOIN sys.destination_data_spaces dds
     ON dds.partition_scheme_id = ps.data_space_id
     AND dds.destination_id = p.partition_number
INNER JOIN sys.filegroups fg
     ON dds.data_space_id = fg.data_space_id
LEFT OUTER JOIN sys.partition_range_values rv
     ON f.function_id = rv.function_id
     AND p.partition_number = rv.boundary_id
WHERE i.index_id < 2
     AND o.object_id = OBJECT_ID(@TableName);
Results:
PartitionID Row_count
1 1013000
2 2677000
3 24443000
4 32265000

Notice that partition identifiers start at 1. The "catchall" partition located on PRIMARY file group will have partition id equal to 5. This partition isn't retrieved by the above statement because it is empty - it doesn't have any records. You could retrieve all records for a particular partition identifier using the following syntax, again using $PARTITION function:
SELECT * FROM dbo.FactInternetSales_Partitioned
  WHERE $Partition.FullOrderDateRangePFN (FullDate) = 2
Now if you create an index on this table; by default the index will be partitioned using the same partition scheme as the table:
CREATE INDEX ix_FactInternetSales_Partitioned_cl
  ON FactInternetSales_Partitioned (   ProductKey,   FullDate)
  ON FullOrderDateRangePScheme  (FullDate)
It is possible to omit the partition scheme specification in this statement if you want the index to be aligned with the table. Note that an index doesn't have to use the same partition scheme and partition function to be aligned with the table. As long as an index is partitioned based on the same data type, has the same number of partitions as the table and each partition has the same data boundaries as the table, the index will be aligned.
Partitioning key of an index doesn't have to be part of the index key. So you could use the same partition scheme to create an index that does not reference FullDate as its index key. For example, the following statement is valid:
CREATE INDEX ix_FactInternetSales_Partitioned_ProductKey
  ON FactInternetSales_Partitioned (   ProductKey)
  ON FullOrderDateRangePScheme  (FullDate)
Use a partition strategy for an index which is completely different from the underlying table's partition scheme and partition function. Some examples of where this makes sense are:
The table isn't partitioned, but you wish to partition the index.
The table is partitioned but you would like to collocate the index data with other tables' indexes because these tables will be frequently joined.
You have a unique index you wish to partition and the index key isn't part of the table's partition function.
Table and index partition metadata can be retrieved for FactInternetSales_partitioned table using the following statement:
SELECT OBJECT_NAME([object_id]) AS table_name, *
  FROM sys.partitions
  WHERE [object_id] = OBJECT_ID('dbo.FactInternetSales_partitioned')
  ORDER BY index_id, partition_number
Results:
table_name partition_id object_id index_id partition_number hobt_id rows
FactInternetSales_Partitioned 72057594052411300 1294627655 0 1 72057594052411300 2026000
FactInternetSales_Partitioned 72057594052476900 1294627655 0 2 72057594052476900 5354000
FactInternetSales_Partitioned 72057594052542400 1294627655 0 3 72057594052542400 48886000
FactInternetSales_Partitioned 72057594052608000 1294627655 0 4 72057594052608000 64530000
FactInternetSales_Partitioned 72057594052673500 1294627655 0 5 72057594052673500 0
FactInternetSales_Partitioned 72057594052739000 1294627655 3 1 72057594052739000 2026000
FactInternetSales_Partitioned 72057594052804600 1294627655 3 2 72057594052804600 5354000
FactInternetSales_Partitioned 72057594052870100 1294627655 3 3 72057594052870100 48886000
FactInternetSales_Partitioned 72057594052935600 1294627655 3 4 72057594052935600 64530000
FactInternetSales_Partitioned 72057594053001200 1294627655 3 5 72057594053001200 0
It's important to note that in order to enable partition switching, all indexes on the table must be aligned. Partitioned indexes are normally implemented for query performance benefits, whereas partition switching yields great benefits in large table's manageability. So whether you align indexes with underlying tables depends on whether you are implementing table and index partitions primarily for performance tuning or for manageability.