bin/projects/dbatools/dbatools/Parameter/DbaInstanceParameter.cs

using System;
using System.Management.Automation;
using System.Net;
using System.Net.NetworkInformation;
using System.Text.RegularExpressions;
using Sqlcollaborative.Dbatools.Connection;
using Sqlcollaborative.Dbatools.Exceptions;
using Sqlcollaborative.Dbatools.Utility;
 
namespace Sqlcollaborative.Dbatools.Parameter
{
    /// <summary>
    /// Input converter for instance information
    /// </summary>
    public class DbaInstanceParameter
    {
        #region Fields of contract
        /// <summary>
        /// Name of the computer as resolvable by DNS
        /// </summary>
        [ParameterContract(ParameterContractType.Field, ParameterContractBehavior.Mandatory)]
        public string ComputerName
        {
            get
            {
                // Pretend to be localhost for all non-sql functions
                if (_ComputerName == "(localdb)")
                    return "localhost";
                return _ComputerName;
            }
        }
 
        /// <summary>
        /// Name of the instance on the target server
        /// </summary>
        [ParameterContract(ParameterContractType.Field, ParameterContractBehavior.Optional)]
        public string InstanceName
        {
            get
            {
                if (String.IsNullOrEmpty(_InstanceName))
                    return "MSSQLSERVER";
                return _InstanceName;
            }
        }
 
        /// <summary>
        /// The port over which to connect to the server. Only present if non-default
        /// </summary>
        [ParameterContract(ParameterContractType.Field, ParameterContractBehavior.Optional)]
        public int Port
        {
            get
            {
                if (_Port == 0 && String.IsNullOrEmpty(_InstanceName))
                    return 1433;
                return _Port;
            }
        }
 
        /// <summary>
        /// The network protocol to connect over
        /// </summary>
        [ParameterContract(ParameterContractType.Field, ParameterContractBehavior.Mandatory)]
        public SqlConnectionProtocol NetworkProtocol
        {
            get
            {
                return _NetworkProtocol;
            }
        }
 
        /// <summary>
        /// Verifies, whether the specified computer is localhost or not.
        /// </summary>
        [ParameterContract(ParameterContractType.Field, ParameterContractBehavior.Mandatory)]
        public bool IsLocalHost
        {
            get
            {
                // Pretend to be localhost for all non-sql functions
                if (_ComputerName == "(localdb)")
                    return true;
                return Utility.Validation.IsLocalhost(_ComputerName);
            }
        }
 
        /// <summary>
        /// Full name of the instance, including the server-name
        /// </summary>
        [ParameterContract(ParameterContractType.Field, ParameterContractBehavior.Mandatory)]
        public string FullName
        {
            get
            {
                string temp = _ComputerName;
                if (_Port > 0) { temp += (":" + _Port); }
                if (!String.IsNullOrEmpty(_InstanceName)) { temp += ("\\" + _InstanceName); }
                return temp;
            }
        }
 
        /// <summary>
        /// Full name of the instance, including the server-name, used when connecting via SMO
        /// </summary>
        [ParameterContract(ParameterContractType.Field, ParameterContractBehavior.Mandatory)]
        public string FullSmoName
        {
            get
            {
                string temp = _ComputerName;
                if (_NetworkProtocol == SqlConnectionProtocol.NP) { temp = "NP:" + temp; }
                if (_NetworkProtocol == SqlConnectionProtocol.TCP) { temp = "TCP:" + temp; }
                if (!String.IsNullOrEmpty(_InstanceName) && _Port > 0) { return String.Format(@"{0}\{1},{2}", temp, _InstanceName, _Port); }
                if (_Port > 0) { return temp + "," + _Port; }
                if (!String.IsNullOrEmpty(_InstanceName)) { return temp + "\\" + _InstanceName; }
                return temp;
            }
        }
 
        /// <summary>
        /// Name of the computer as used in an SQL Statement
        /// </summary>
        [ParameterContract(ParameterContractType.Field, ParameterContractBehavior.Mandatory)]
        public string SqlComputerName
        {
            get { return "[" + _ComputerName + "]"; }
        }
 
        /// <summary>
        /// Name of the instance as used in an SQL Statement
        /// </summary>
        [ParameterContract(ParameterContractType.Field, ParameterContractBehavior.Mandatory)]
        public string SqlInstanceName
        {
            get
            {
                if (String.IsNullOrEmpty(_InstanceName))
                    return "[MSSQLSERVER]";
                return "[" + _InstanceName + "]";
            }
        }
 
        /// <summary>
        /// Full name of the instance, including the server-name as used in an SQL statement
        /// </summary>
        [ParameterContract(ParameterContractType.Field, ParameterContractBehavior.Mandatory)]
        public string SqlFullName
        {
            get
            {
                if (String.IsNullOrEmpty(_InstanceName)) { return "[" + _ComputerName + "]"; }
                return "[" + _ComputerName + "\\" + _InstanceName + "]";
            }
        }
 
        /// <summary>
        /// Whether the input is a connection string
        /// </summary>
        [ParameterContract(ParameterContractType.Field, ParameterContractBehavior.Mandatory)]
        public bool IsConnectionString { get; private set; }
 
        /// <summary>
        /// The original object passed to the parameter class.
        /// </summary>
        [ParameterContract(ParameterContractType.Field, ParameterContractBehavior.Mandatory)]
        public object InputObject;
        #endregion Fields of contract
 
        private string _ComputerName;
        private string _InstanceName;
        private int _Port;
        private SqlConnectionProtocol _NetworkProtocol = SqlConnectionProtocol.Any;
 
        #region Uncontracted properties
        /// <summary>
        /// What kind of object was bound to the parameter class? For efficiency's purposes.
        /// </summary>
        public DbaInstanceInputType Type
        {
            get
            {
                try
                {
                    PSObject tempObject = new PSObject(InputObject);
                    string typeName = tempObject.TypeNames[0].ToLower();
 
                    switch (typeName)
                    {
                        case "microsoft.sqlserver.management.smo.server":
                            return DbaInstanceInputType.Server;
                        case "microsoft.sqlserver.management.smo.linkedserver":
                            return DbaInstanceInputType.Linked;
                        case "microsoft.sqlserver.management.registeredservers.registeredserver":
                            return DbaInstanceInputType.RegisteredServer;
                        case "system.data.sqlclient.sqlconnection":
                            return DbaInstanceInputType.SqlConnection;
                        default:
                            return DbaInstanceInputType.Default;
                    }
                }
                catch { return DbaInstanceInputType.Default; }
            }
        }
 
        /// <summary>
        /// Returns, whether a live SMO object was bound for the purpose of accessing LinkedServer functionality
        /// </summary>
        public bool LinkedLive
        {
            get
            {
                return (((DbaInstanceInputType.Linked | DbaInstanceInputType.Server) & Type) != 0);
            }
        }
 
        /// <summary>
        /// Returns the available Linked Server objects from live objects only
        /// </summary>
        public object LinkedServer
        {
            get
            {
                switch (Type)
                {
                    case DbaInstanceInputType.Linked:
                        return InputObject;
                    case DbaInstanceInputType.Server:
                        PSObject tempObject = new PSObject(InputObject);
                        return tempObject.Properties["LinkedServers"].Value;
                    default:
                        return null;
                }
            }
        }
        #endregion Uncontracted properties
 
        /// <summary>
        /// Converts the parameter class to its full name
        /// </summary>
        /// <param name="Input">The parameter class object to convert</param>
        [ParameterContract(ParameterContractType.Operator, ParameterContractBehavior.Conversion)]
        public static implicit operator string(DbaInstanceParameter Input)
        {
            return Input.FullName;
        }
 
        #region Constructors
        /// <summary>
        /// Creates a DBA Instance Parameter from string
        /// </summary>
        /// <param name="Name">The name of the instance</param>
        public DbaInstanceParameter(string Name)
        {
            InputObject = Name;
 
            if (string.IsNullOrWhiteSpace(Name))
                throw new BloodyHellGiveMeSomethingToWorkWithException("Please provide an instance name", "DbaInstanceParameter");
 
            if (Name == ".")
            {
                _ComputerName = Name;
                _NetworkProtocol = SqlConnectionProtocol.NP;
                return;
            }
             
            string tempString = Name.Trim();
            tempString = Regex.Replace(tempString, @"^\[(.*)\]$", "$1");
 
            if (UtilityHost.IsLike(tempString, @".\*"))
            {
                _ComputerName = Name;
                _NetworkProtocol = SqlConnectionProtocol.NP;
 
                string instanceName = tempString.Substring(2);
 
                if (!Utility.Validation.IsValidInstanceName(instanceName))
                    throw new ArgumentException(String.Format("Failed to interpret instance name: '{0}' is not a legal name!", instanceName));
 
                _InstanceName = instanceName;
 
                return;
            }
 
            if (UtilityHost.IsLike(tempString, "*.WORKGROUP"))
            tempString = Regex.Replace(tempString, @"\.WORKGROUP$", "", RegexOptions.IgnoreCase);
 
            // Named Pipe path notation interpretation
            if (Regex.IsMatch(tempString, @"^\\\\[^\\]+\\pipe\\([^\\]+\\){0,1}sql\\query$", RegexOptions.IgnoreCase))
            {
                try
                {
                    _NetworkProtocol = SqlConnectionProtocol.NP;
 
                    _ComputerName = Regex.Match(tempString, @"^\\\\([^\\]+)\\").Groups[1].Value;
 
                    if (Regex.IsMatch(tempString, @"\\MSSQL\$[^\\]+\\", RegexOptions.IgnoreCase))
                        _InstanceName = Regex.Match(tempString, @"\\MSSQL\$([^\\]+)\\", RegexOptions.IgnoreCase).Groups[1].Value;
                }
                catch (Exception e)
                {
                    throw new ArgumentException(String.Format("Failed to interpret named pipe path notation: {0} | {1}", InputObject, e.Message), e);
                }
 
                return;
            }
 
            // Connection String interpretation
            try
            {
                System.Data.SqlClient.SqlConnectionStringBuilder connectionString =
                    new System.Data.SqlClient.SqlConnectionStringBuilder(tempString);
                DbaInstanceParameter tempParam = new DbaInstanceParameter(connectionString.DataSource);
                _ComputerName = tempParam.ComputerName;
                if (tempParam.InstanceName != "MSSQLSERVER")
                {
                    _InstanceName = tempParam.InstanceName;
                }
                if (tempParam.Port != 1433)
                {
                    _Port = tempParam.Port;
                }
                _NetworkProtocol = tempParam.NetworkProtocol;
                 
                if (UtilityHost.IsLike(tempString, @"(localdb)\*"))
                    _NetworkProtocol = SqlConnectionProtocol.NP;
 
                IsConnectionString = true;
 
                return;
            }
            catch (ArgumentException ex)
            {
                string name = "unknown";
                try
                {
                    name = ex.TargetSite.GetParameters()[0].Name;
                }
                catch
                {
                }
                if (name == "keyword")
                {
                    throw;
                }
            }
            catch (FormatException)
            {
                throw;
            }
            catch { }
 
            // Handle and clear protocols. Otherwise it'd make port detection unneccessarily messy
            if (Regex.IsMatch(tempString, "^TCP:", RegexOptions.IgnoreCase)) //TODO: Use case insinsitive String.BeginsWith()
            {
                _NetworkProtocol = SqlConnectionProtocol.TCP;
                tempString = tempString.Substring(4);
            }
            if (Regex.IsMatch(tempString, "^NP:", RegexOptions.IgnoreCase)) // TODO: Use case insinsitive String.BeginsWith()
            {
                _NetworkProtocol = SqlConnectionProtocol.NP;
                tempString = tempString.Substring(3);
            }
 
            // Case: Default instance | Instance by port
            if (tempString.Split('\\').Length == 1)
            {
                if (Regex.IsMatch(tempString, @"[:,]\d{1,5}$") && !Regex.IsMatch(tempString, RegexHelper.IPv6) && ((tempString.Split(':').Length == 2) || (tempString.Split(',').Length == 2)))
                {
                    char delimiter;
                    if (Regex.IsMatch(tempString, @"[:]\d{1,5}$"))
                        delimiter = ':';
                    else
                        delimiter = ',';
 
                    try
                    {
                        Int32.TryParse(tempString.Split(delimiter)[1], out _Port);
                        if (_Port > 65535) { throw new PSArgumentException("Failed to parse instance name: " + tempString); }
                        tempString = tempString.Split(delimiter)[0];
                    }
                    catch
                    {
                        throw new PSArgumentException("Failed to parse instance name: " + Name);
                    }
                }
 
                if (Utility.Validation.IsValidComputerTarget(tempString))
                {
                    _ComputerName = tempString;
                }
 
                else
                {
                    throw new PSArgumentException("Failed to parse instance name: " + Name);
                }
            }
 
            // Case: Named instance
            else if (tempString.Split('\\').Length == 2)
            {
                string tempComputerName = tempString.Split('\\')[0];
                string tempInstanceName = tempString.Split('\\')[1];
 
                if (Regex.IsMatch(tempComputerName, @"[:,]\d{1,5}$") && !Regex.IsMatch(tempComputerName, RegexHelper.IPv6))
                {
                    char delimiter;
                    if (Regex.IsMatch(tempComputerName, @"[:]\d{1,5}$"))
                        delimiter = ':';
                    else
                        delimiter = ',';
 
                    try
                    {
                        Int32.TryParse(tempComputerName.Split(delimiter)[1], out _Port);
                        if (_Port > 65535) { throw new PSArgumentException("Failed to parse instance name: " + Name); }
                        tempComputerName = tempComputerName.Split(delimiter)[0];
                    }
                    catch
                    {
                        throw new PSArgumentException("Failed to parse instance name: " + Name);
                    }
                }
                else if (Regex.IsMatch(tempInstanceName, @"[:,]\d{1,5}$") && !Regex.IsMatch(tempInstanceName, RegexHelper.IPv6))
                {
                    char delimiter;
                    if (Regex.IsMatch(tempString, @"[:]\d{1,5}$"))
                        delimiter = ':';
                    else
                        delimiter = ',';
 
                    try
                    {
                        Int32.TryParse(tempInstanceName.Split(delimiter)[1], out _Port);
                        if (_Port > 65535) { throw new PSArgumentException("Failed to parse instance name: " + Name); }
                        tempInstanceName = tempInstanceName.Split(delimiter)[0];
                    }
                    catch
                    {
                        throw new PSArgumentException("Failed to parse instance name: " + Name);
                    }
                }
 
                // LocalDBs mostly ignore regular Instance Name rules, so that validation is only relevant for regular connections
                if (UtilityHost.IsLike(tempComputerName, "(localdb)") || (Utility.Validation.IsValidComputerTarget(tempComputerName) && Utility.Validation.IsValidInstanceName(tempInstanceName, true)))
                {
                    if (UtilityHost.IsLike(tempComputerName, "(localdb)"))
                        _ComputerName = "(localdb)";
                    else
                        _ComputerName = tempComputerName;
                    if ((tempInstanceName.ToLower() != "default") && (tempInstanceName.ToLower() != "mssqlserver"))
                        _InstanceName = tempInstanceName;
                }
 
                else
                {
                    throw new PSArgumentException(string.Format("Failed to parse instance name: {0}. Computer Name: {1}, Instance {2}", Name, tempComputerName, tempInstanceName));
                }
            }
 
            // Case: Bad input
            else { throw new PSArgumentException("Failed to parse instance name: " + Name); }
        }
 
        /// <summary>
        /// Creates a DBA Instance Parameter from an IPAddress
        /// </summary>
        /// <param name="Address"></param>
        public DbaInstanceParameter(IPAddress Address)
        {
            _ComputerName = Address.ToString();
            InputObject = Address;
        }
 
        /// <summary>
        /// Creates a DBA Instance Parameter from the reply to a ping
        /// </summary>
        /// <param name="Ping">The result of a ping</param>
        public DbaInstanceParameter(PingReply Ping)
        {
            _ComputerName = Ping.Address.ToString();
            InputObject = Ping;
        }
 
        /// <summary>
        /// Creates a DBA Instance Parameter from the result of a dns resolution
        /// </summary>
        /// <param name="Entry">The result of a dns resolution, to be used for targetting the default instance</param>
        public DbaInstanceParameter(IPHostEntry Entry)
        {
            _ComputerName = Entry.HostName;
            InputObject = Entry;
        }
 
        /// <summary>
        /// Creates a DBA Instance Parameter from an established SQL Connection
        /// </summary>
        /// <param name="Connection">The connection to reuse</param>
        public DbaInstanceParameter(System.Data.SqlClient.SqlConnection Connection)
        {
            InputObject = Connection;
            DbaInstanceParameter tempParam = new DbaInstanceParameter(Connection.DataSource);
 
            _ComputerName = tempParam.ComputerName;
            if (tempParam.InstanceName != "MSSQLSERVER")
            {
                _InstanceName = tempParam.InstanceName;
            }
            if (tempParam.Port != 1433)
            {
                _Port = tempParam.Port;
            }
            _NetworkProtocol = tempParam.NetworkProtocol;
        }
 
        /// <summary>
        /// Accept and understand discovery reports.
        /// </summary>
        /// <param name="Report">The report to interpret</param>
        public DbaInstanceParameter(Discovery.DbaInstanceReport Report)
            : this(Report.SqlInstance)
        {
            InputObject = Report;
        }
 
        /// <summary>
        /// Creates a DBA Instance parameter from any object
        /// </summary>
        /// <param name="Input">Object to parse</param>
        public DbaInstanceParameter(object Input)
        {
            InputObject = Input;
            PSObject tempInput = new PSObject(Input);
            string typeName = "";
 
            try { typeName = tempInput.TypeNames[0].ToLower(); }
            catch
            {
                throw new PSArgumentException("Failed to interpret input as Instance: " + Input);
            }
 
            typeName = typeName.Replace("Deserialized.", "");
 
            switch (typeName)
            {
                case "microsoft.sqlserver.management.smo.server":
                    try
                    {
                        if (tempInput.Properties["ServerType"] != null && (string)tempInput.Properties["ServerType"].Value.ToString() == "SqlAzureDatabase")
                            _ComputerName = (new DbaInstanceParameter((string)tempInput.Properties["Name"].Value)).ComputerName;
                        else
                        {
                            if (tempInput.Properties["NetName"] != null)
                                _ComputerName = (string)tempInput.Properties["NetName"].Value;
                            else
                                _ComputerName = (new DbaInstanceParameter((string)tempInput.Properties["DomainInstanceName"].Value)).ComputerName;
                        }
                        _InstanceName = (string)tempInput.Properties["InstanceName"].Value;
                        PSObject tempObject = new PSObject(tempInput.Properties["ConnectionContext"].Value);
 
                        string tempConnectionString = (string)tempObject.Properties["ConnectionString"].Value;
                        tempConnectionString = tempConnectionString.Split(';')[0].Split('=')[1].Trim().Replace(" ", "");
 
                        if (Regex.IsMatch(tempConnectionString, @",\d{1,5}$") && (tempConnectionString.Split(',').Length == 2))
                        {
                            try { Int32.TryParse(tempConnectionString.Split(',')[1], out _Port); }
                            catch (Exception e)
                            {
                                throw new PSArgumentException("Failed to parse port number on connection string: " + tempConnectionString, e);
                            }
                            if (_Port > 65535) { throw new PSArgumentException("Failed to parse port number on connection string: " + tempConnectionString); }
                        }
                    }
                    catch (Exception e)
                    {
                        throw new PSArgumentException("Failed to interpret input as Instance: " + Input + " : " + e.Message, e);
                    }
                    break;
                case "microsoft.sqlserver.management.smo.linkedserver":
                    try
                    {
                        _ComputerName = (string)tempInput.Properties["Name"].Value;
                    }
                    catch (Exception e)
                    {
                        throw new PSArgumentException("Failed to interpret input as Instance: " + Input, e);
                    }
                    break;
                case "microsoft.activedirectory.management.adcomputer":
                    try
                    {
                        _ComputerName = (string)tempInput.Properties["Name"].Value;
 
                        // We prefer using the dnshostname whenever possible
                        if (tempInput.Properties["DNSHostName"].Value != null)
                        {
                            if (!String.IsNullOrEmpty((string)tempInput.Properties["DNSHostName"].Value))
                                _ComputerName = (string)tempInput.Properties["DNSHostName"].Value;
                        }
                    }
                    catch (Exception e)
                    {
                        throw new PSArgumentException("Failed to interpret input as Instance: " + Input, e);
                    }
                    break;
                case "microsoft.sqlserver.management.registeredservers.registeredserver":
                    try
                    {
                        //Pass the ServerName property of the SMO object to the string constrtuctor,
                        //so we don't have to re-invent the wheel on instance name / port parsing
                        DbaInstanceParameter parm =
                            new DbaInstanceParameter((string) tempInput.Properties["ServerName"].Value);
                        _ComputerName = parm.ComputerName;
 
                        if (parm.InstanceName != "MSSQLSERVER")
                            _InstanceName = parm.InstanceName;
 
                        if (parm.Port != 1433)
                            _Port = parm.Port;
                    }
                    catch (Exception e)
                    {
                        throw new PSArgumentException("Failed to interpret input as Instance: " + Input, e);
                    }
                    break;
                default:
                    throw new PSArgumentException("Failed to interpret input as Instance: " + Input);
            }
        }
        #endregion Constructors
 
        /// <summary>
        /// Overrides the regular tostring to show something pleasant and useful
        /// </summary>
        /// <returns>The full SMO name</returns>
        public override string ToString()
        {
            return FullSmoName;
        }
    }
}