Oracle Multitenant and custom DBCA templates

Today I’ve encountered an annoying issue while adapting a few scripts for automatic database creation. I track it here so hopefully it may save a few hours of troubleshooting to someone…

I’ve used the DBCA to prepare a new template:

dbca -> Create Database -> Advanced Mode -> General Purpose -> 
Create As Container Database -> Create an Empty Container Database

 

Then continued by customizing other options, including init parameters and datafile/logfile paths. Finally, I’ve saved it as a new template instead of creating the database.

I’ve checked the resulting .dbc and seen that there was, as expected, the parameter “enable_pluggable_database”=”true”.

Then I’ve moved the template file to my $HOME directory and tested the silent database creation with the option “-createAsContainerDatabase true”:

$ ${ORACLE_HOME}/bin/dbca -silent \
-createDatabase \
-createAsContainerDatabase true  \
-templateName /home/oracle/Template_12_EE_MULTITENANT.dbc \
-gdbname cdb1 \
-sid cdb1 \
-responseFile NO_VALUE \
-characterSet AL32UTF8 \
-SYSPASSWORD *** \
-SYSTEMPASSWORD *** \
-redoLogFileSize 256 \
-initparams open_cursors=1300 \
-initparams processes=3000 \
-initparams enable_pluggable_database=true

The database configuration has completed successfully, without errors. I’ve accessed my new container, and I’ve been surprised by seing:

SQL> select * from v$pdbs;

no rows selected

SQL>

In fact, there were no pdb$seed datafiles:

SQL> select distinct con_id from cdb_data_files;

    CON_ID
----------
         1

SQL>

After little investigation, I’ve found these lines in the dbca trace.log:

[main] [ 2014-03-28 10:37:53.956 CET ] [Host.startOperation:2651]  is Template CDB false
[main] [ 2014-03-28 10:37:53.956 CET ] [TemplateManager.isInstallTemplate:2314]  Selected Template by user:=Template CEI 8K 12 EE MULTITENANT
[main] [ 2014-03-28 10:37:53.956 CET ] [TemplateManager.isInstallTemplate:2321]  The Message Id to be searched:=null
[main] [ 2014-03-28 10:37:53.957 CET ] [Host.startOperation:2663]  Template Selected is User created NON-CDB Template. Creating database as NON-CDB
[main] [ 2014-03-28 10:37:53.957 CET ] [HAUtils.getCurrentOracleHome:490]  Oracle home from system property: /ccv/app/oracle/product/12.1.0.1

 

Then I’ve struggled with dbca and templates a few times before finding that, actually, the magic “enable pluggable database” is done by dbca only if the template file name is not customized.

Running the same exact command with the very same template file but renamed to $ORACLE_HOME/assistants/dbca/templates/General_Purpose.dbc actually works (notice the diff at the first line):

$ diff /home/oracle/Template_12_EE_MULTITENANT.dbc $ORACLE_HOME/assistants/dbca/templates/General_Purpose.dbc
$ ${ORACLE_HOME}/bin/dbca -silent \
-createDatabase \
-createAsContainerDatabase true  \
-templateName General_Purpose.dbc \
-gdbname cdb1 \
-sid cdb1 \
-responseFile NO_VALUE \
-characterSet AL32UTF8 \
-SYSPASSWORD *** \
-SYSTEMPASSWORD *** \
-redoLogFileSize 256 \
-initparams open_cursors=1300 \
-initparams processes=3000 \
-initparams enable_pluggable_database=true

 

SQL> select * from v$pdbs;

    CON_ID       DBID    CON_UID GUID
---------- ---------- ---------- --------------------------------
NAME                           OPEN_MODE  RES
------------------------------ ---------- ---
OPEN_TIME
---------------------------------------------------------------------------
CREATE_SCN TOTAL_SIZE
---------- ----------
         2 4086042395 4086042395 F5A8226121F93B96E0434B96780A8C91
PDB$SEED                       READ ONLY  NO
28-MAR-14 10.20.42.813 AM
   1720341  283115520

SQL> select distinct con_id from cdb_data_files;

    CON_ID
----------
         2
         1

SQL>

 

I’ve also tried to cheat and use a symlink to my previous custom template, and surprisingly, it still works:

$ ls -l $ORACLE_HOME/assistants/dbca/templates/General_Purpose.dbc
lrwxrwxrwx 1 oracle dba 74 Mar 28 13:07 /u01/app/oracle/product/12.1.0.1/assistants/dbca/templates/General_Purpose.dbc -> /home/oracle/Template_12_EE_MULTITENANT.dbc
$

In the dbca trace log the message saying that the DB will be NON-CDB disappears:

[main] [ 2014-03-28 10:12:14.683 CET ] [Host.startOperation:2651]  is Template CDB false
[main] [ 2014-03-28 10:12:14.683 CET ] [TemplateManager.isInstallTemplate:2314]  Selected Template by user:=General Purpose
[main] [ 2014-03-28 10:12:14.683 CET ] [TemplateManager.isInstallTemplate:2321]  The Message Id to be searched:=GENERAL_PURPOSE
[main] [ 2014-03-28 10:12:14.683 CET ] [HAUtils.getCurrentOracleHome:490]  Oracle home from system property: /ccv/app/oracle/product/12.1.0.1

So the problem is really caused by the different filename/location of the template.

IMHO it’s a kind of bug, the decision between a CDB and NON-CDB should not be taken by DBCA.  Moreover, it’s not based on the content of the template, which would be logic. But today I’m late and lazy, I will not open a SR for this.

:-/

SQLServer 2008R2 unattended installation and configuration via powershell

My first steps on Powershell

Ok, Ok, as an “Oracle on Linux Certified Expert”, I’ve never been a great fan of SQLServer (I shouldn’t say this, I’m working on SQLServer since release 6.5…) and I’ve always hated the DOS command prompt.
However, things are changing fast after Microsoft released the Powershell some years ago. It’s surprising, now Windows powershell support new best of breed features like aliases and pipelines. 😀

Today Microsoft itself recommends Windows Core installations instead of the full ones, and also SQLServer 2012 comes with a lot of new Commandlets to manage your server.

So I’ve decided to move my first steps in the Powershell world and I’ve created a script for a customer that installs and configure a SQL2008 with a single Powershell script.

It has been quite painful to complete, you can download the complete script HERE.

Taking parameters

The very first line accepts named parameters. I’ve tried to reduce the number but I’ve preferred to take, as an example, different disks for different tasks.

param ([string] $instance, [string] $collation, $ServiceAccountPassword, $saPassword, $sourceDir, $servicePackExec, $instDrive, $userDbDrive, $userLogDrive, $tempDbDrive,  $tempLogDrive, $backupDrive , $port )

Then I’ve put a little of interaction if some parameters are missing. In facts, I can launch my scripts without inline parameters and specify everything when prompted by the script.

######################
# Getting Parameters #
######################
if ( -not $sourceDir) { $sourceDir = Read-Host 'Source Path containing SQL2008R2 installation ? ' }
if ( -not $servicePackExec) { $servicePackExec = Read-Host 'Full Path to service pack executable [Empty for None]? ' }
if ( -not $instance) { $instance = Read-Host 'Instance Name? ' }
if ( -not $collation ) {$collation = Read-Host 'Collation [Latin1_General_CI_AS]? ' }
if ( -not $collation ) { $collation = "Latin1_General_CI_AS" }

if ( -not $port ) { $port = Read-Host 'TCP Port [50000]? ' }
if ( -not $port ) { $port = "50000" }

if ( -not $ServiceAccountPassword ) {
    [System.Security.SecureString]$ServiceAccountPassword = Read-Host "Enter the service account password: " -AsSecureString;
    [String]$syncSvcAccountPassword = [Runtime.InteropServices.Marshal]::PtrToStringAuto([Runtime.InteropServices.Marshal]::SecureStringToBSTR($ServiceAccountPassword));
} else { [String]$syncSvcAccountPassword = $ServiceAccountPassword; }

if ( -not $saPassword ) {
    [System.Security.SecureString]$saPasswordSec = Read-Host "Enter the sa password: " -AsSecureString;
    [String]$saPassword = [Runtime.InteropServices.Marshal]::PtrToStringAuto([Runtime.InteropServices.Marshal]::SecureStringToBSTR($saPasswordSec));
} else { [String]$saPassword = $ServiceAccountPassword; }

$instance = $instance.ToUpper()

Before I prompt for the drive letters for the installation paths, I display a little table with the available local disks:

function displayDrives () {
    Get-WmiObject -class "Win32_LogicalDisk" | ?{ @(2, 3) -contains $_.DriveType } | where {$_.Freespace} | select Name, VolumeName, Size, FreeSpace
}

########################
# Getting Disk Letters #
########################

$driveDisplayed=0

if ( -not $instDrive ) {
    if ( $driveDisplayed -eq 0 ) { displayDrives ; $driveDisplayed=1 }
    $instDrive = Read-Host 'Drive letter (without colon) for Instance Installation? '
}

if ( -not $userDbDrive ) {
    if ( $driveDisplayed -eq 0 ) { displayDrives ; $driveDisplayed=1 }
    $userDbDrive = Read-Host 'Drive letter (without colon) for User Databases? '
}

if ( -not $userLogDrive ) {
    if ( $driveDisplayed -eq 0 ) { displayDrives ; $driveDisplayed=1 }
    $userLogDrive = Read-Host 'Drive letter (without colon) for Transaction Logs ? '
}

if ( -not $tempDbDrive ) {
    if ( $driveDisplayed -eq 0 ) { displayDrives ; $driveDisplayed=1 }
    $tempDbDrive = Read-Host 'Drive letter (without colon) for Temp Database ? '
}

if ( -not $tempLogDrive ) {
    if ( $driveDisplayed -eq 0 ) { displayDrives ; $driveDisplayed=1 }
    $tempLogDrive = Read-Host 'Drive letter (without colon) for Temp Logs ? '
}

if ( -not $backupDrive ) {
    if ( $driveDisplayed -eq 0 ) { displayDrives ; $driveDisplayed=1 }
    $backupDrive = Read-Host 'Drive letter (without colon) for Backups ? '
}

 

 Installing prerequisites

###############################
# install prerequisites for SQL2008R2
function installPrereqs () {
    Import-Module ServerManager
    Add-WindowsFeature Application-Server,AS-NET-Framework,NET-Framework,NET-Framework-Core,WAS,WAS-Process-Model,WAS-NET-Environment,WAS-Config-APIs
    # get-windowsfeature | Where {$_.Installed} | Sort FeatureType,Parent,Name | Select Name,Displayname,FeatureType,Parent
}

######################
# Installing Prereqs #
######################
"Installing Prerequisites (.Net, etc) ..."
installPrereqs

The commented command is to get the installed features after the installation. No really need to display it, it works really well.

 Dynamically prepare a configuration file

The unattended installation needs some parameters prepared in a configuration file.
This is likely where you will change most of your stuff depending on your standards:
Components, paths, service accounts, you can change everything or modify the script to accept also this variables as parameters.

The full documentation about filling the configuration file is on the MSDN:

http://msdn.microsoft.com/en-us/library/ms144259%28v=sql.105%29.aspx

#########################################
# prepare the standard configuration file
function prepareConfigFile ([String]$instance, [String]$collation, $instDrive, $userDbDrive, $userLogDrive, $tempDbDrive, $tempLogDrive, $backupDrive ) {
$config = "[SQLSERVER2008]
ACTION=""Install""
FEATURES=SQLENGINE,REPLICATION,FULLTEXT,BIDS,CONN,IS,BC,BOL,SSMS,ADV_SSMS,SNAC_SDK
INSTANCENAME=""$instance""
INSTANCEID=""$instance""
INSTALLSHAREDDIR=""C:\Program Files\Microsoft SQL Server""
INSTALLSHAREDWOWDIR=""C:\Program Files (x86)\Microsoft SQL Server""
INSTANCEDIR=""C:\Program Files\Microsoft SQL Server""
INSTALLSQLDATADIR="""+$instDrive+":\MSSQL\$instance""
SQLUSERDBDIR="""+$userDbDrive+":\MSSQL\$instance\MSSQL10_50."+$instance+"\MSSQL\Data""
SQLUSERDBLOGDIR="""+$userLogDrive+":\MSSQL\$instance\MSSQL10_50."+$instance+"\MSSQL\Tlog""
SQLTEMPDBDIR="""+$tempDbDrive+":\MSSQL\$instance\MSSQL10_50."+$instance+"\MSSQL\Data""
SQLTEMPDBLOGDIR="""+$tempLogDrive+":\MSSQL\$instance\MSSQL10_50."+$instance+"\MSSQL\Tlog""
SQLBACKUPDIR="""+$backupDrive+":\MSSQL\$instance\MSSQL10_50."+$instance+"\MSSQL\Backup""
FILESTREAMLEVEL=""0""
TCPENABLED=""1""
NPENABLED=""1""
SQLCOLLATION=""$collation""
SQLSVCACCOUNT=""MYDOM\sqlsrvc""
SQLSVCSTARTUPTYPE=""Automatic""
AGTSVCACCOUNT=""MYDOM\sqlsrvc""
AGTSVCSTARTUPTYPE=""Automatic""
ISSVCACCOUNT=""NT AUTHORITY\NetworkService""
ISSVCSTARTUPTYPE=""Automatic""
BROWSERSVCSTARTUPTYPE=""Automatic""
SQLSYSADMINACCOUNTS=""MYDOM\sqlsrvc""
SECURITYMODE=""SQL""
SQMREPORTING=""False""
IACCEPTSQLSERVERLICENSETERMS=""True"""

$config
}

#####################
# Creating Ini File #
#####################
$workDir = pwd

"Creating Ini File for Installation..."
$configFile = "$workDir\sql2008_"+$instance+"_install.ini"

prepareConfigFile $instance $collation $instDrive $userDbDrive $userLogDrive $tempDbDrive $tempLogDrive $backupDrive | Out-File $configFile

"Configuration File written to: "+$configFile

Starting the SQL Server 2008 R2 installation

Off course you’ll need an installation media downloaded from the Microsoft customers site with the correct License Keys and mounted somewhere.  (remember the $sourceDir parameter?) I’ve decided to change the path in the directory containing the media and then change it back.

#######################################
# Starting SQL 2008 Base Installation #
#######################################

set-location $sourceDir

"Starting SQL 2008 Base Installation..."
$installCmd = ".\setup.exe /qs /SQLSVCPASSWORD=""$syncSvcAccountPassword"" /AGTSVCPASSWORD=""$syncSvcAccountPassword"" /SAPWD=""$saPassword"" /ConfigurationFile=""$configFile"""
$installCmd

Invoke-Expression $installCmd

set-location $workDir

 

Launching the Service Pack installation

The Service Pack installation has been a little more painful, normally would be simple but actually the powershell prompt is returned immediately after firing the command. So, to wait it, I’ve had to figure out the name of the process (is the executable name without the file extension .exe), get its process id and wait for that process:

#######################################
# Starting SQL 2008 SP Installation #
#######################################
if ( $servicePackExec) {
    "Starting Service Pack Installation..."
    $patchCmd = "$servicePackExec /Action=Patch /Quiet /IAcceptSQLServerLicenseTerms /Instancename=""$Instance"""
    $patchCmd
    Invoke-Expression $patchCmd

    ## have to take the name of the process and wait for the completion of the pid because service packs
    ## return prompt immediately and then run in background
    $process=[System.IO.Path]::GetFileNameWithoutExtension($servicePackExec)
    $nid = (Get-Process $process).id
    Wait-Process -id $nid
}

 Changing the TCP port

By default SQLServer starts listening on a dynamic port. If you have a default and you want to configure it without opening the configuration manager, you can do it with this snipplet that I’ve copied from sirSql (thank you for sharing this).

 ###############################
# change the TCP port at the end of the installation
function changePort ($SQLName , $Instance, $port) {
    Try
        {

    $SQLName
    $Instance

    # Load SMO Wmi.ManagedComputer assembly
    [System.Reflection.Assembly]::LoadWithPartialName("Microsoft.SqlServer.SqlWmiManagement") | out-null

    Trap {
        $err = $_.Exception
        while ( $err.InnerException ) {
            $err = $err.InnerException
            write-output $err.Message
        }
        continue
    }

    # Connect to the instance using SMO
    $m = New-Object ('Microsoft.SqlServer.Management.Smo.Wmi.ManagedComputer') $SQLName
    $urn = "ManagedComputer[@Name='$SQLName']/ServerInstance[@Name='$Instance']/ServerProtocol[@Name='Tcp']"
    $Tcp = $m.GetSmoObject($urn)
    $Enabled = $Tcp.IsEnabled
    #Enable TCP/IP if not enabled
    IF (!$Enabled)
        {$Tcp.IsEnabled = $true }

    #Set to listen on 50000 and disable dynamic ports
    $m.GetSmoObject($urn + "/IPAddress[@Name='IPAll']").IPAddressProperties[1].Value = $port
    $m.GetSmoObject($urn + "/IPAddress[@Name='IPAll']").IPAddressProperties['TcpDynamicPorts'].Value = ''
    $TCP.alter()

        "Success: SQL set to listen on TCP/IP port $port. Please restart the SQL service for changes to take effect."
    }
    Catch { Write-Warning "Unable to enable TCP/IP & set SQL to listen on port $port" }
}

####################
# Changing TCPport #
####################

"Changing TCP port to $port..."
changePort $hostName $instance $port

Adding your stuff at the end

Having the installation completed is in midstream. After the installation you may want to add tempfiles to your installation, modify your model database, add default accounts.

That’s up to you. If your scripts are identical you can execute them with sqlcmd.If you want to take benefit of the variables already set in the script you can execute them directly:

###############################
# Resizing / Adding Tempfiles #
###############################

$Connection = New-Object System.Data.SQLClient.SQLConnection
$hostName = get-content env:computername

$Connection.ConnectionString ="Server=$hostName\$instance;Database=master;uid=sa;Pwd=$saPassword;"
$Connection.Open()

$Command = New-Object System.Data.SQLClient.SQLCommand
$Command.Connection = $Connection

$Command.CommandText = "ALTER DATABASE tempdb MODIFY FILE (NAME = tempdev, SIZE = 1024MB, filegrowth = 64MB, maxsize=unlimited);"
$Command.ExecuteNonQuery() | out-null
$Command.CommandText = "ALTER DATABASE tempdb MODIFY FILE (NAME = templog, SIZE = 512MB, filegrowth = 64MB, maxsize=unlimited);"
$Command.ExecuteNonQuery() | out-null

$Command.CommandText = "ALTER DATABASE tempdb ADD FILE (NAME = tempdev2, FILENAME = '"+$tempDrive+":\MSSQL\$instance\MSSQL10_50.$instance\MSSQL\Data\tempdb2.ndf', SIZE = 1024MB, filegrowth = 64MB, maxsize=unlimited);"
$Command.ExecuteNonQuery() | out-null
$Command.CommandText = "ALTER DATABASE tempdb ADD FILE (NAME = tempdev3, FILENAME = '"+$tempDrive+":\MSSQL\$instance\MSSQL10_50.$instance\MSSQL\Data\tempdb3.ndf', SIZE = 1024MB, filegrowth = 64MB, maxsize=unlimited);"
$Command.ExecuteNonQuery() | out-null
$Command.CommandText = "ALTER DATABASE tempdb ADD FILE (NAME = tempdev4, FILENAME = '"+$tempDrive+":\MSSQL\$instance\MSSQL10_50.$instance\MSSQL\Data\tempdb4.ndf', SIZE = 1024MB, filegrowth = 64MB, maxsize=unlimited);"
$Command.ExecuteNonQuery() | out-null

$Connection.Close()

Putting all together…

Well, I’ll never paste again all the content here, you can download the script HERE. Just change the file extension from .txt to .ps1.

I know it’s not a rock-solid procedure but it works well for my purposes, feel free to comment or review my script. Just, if you do some improvement on it, please share it and back-link this post!

Cheers